feedBack/static/capabilities/audio-effects.js
Bret Mogilefsky af2949677a
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack*

* rename: slopsmith -> feedBack, byron -> got-feedBack

Renames across the entire codebase:
- slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack
- byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack
- /home/byron/ -> /opt/got-feedBack/
- byron@ougsoft.com -> hi@got-feedBack.org
- github.com/byrongamatos/ -> github.com/got-feedback/
- com.byron. -> com.got-feedback.
- SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback
- Protocol/storage strings migrated with read-old/write-new pattern
- window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias

Refs: #rename-slopsmith

* rename: complete regen against current main + fix backward-compat alias

Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of
current main (3 commits had landed since the branch: #572/#554/#574),
resolving the four content conflicts in favour of main's newer content
(autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge).

Completion fixes on top of the mechanical rename:
- Re-apply rename to post-branch content the original rename never saw:
  window.slopsmith(.Tour) consumers in lessons.js / notifications.js /
  onboarding-tour.js, and the matching JS + python tests (autoplay_exit,
  progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env
  vars now match server.py (which reads FEEDBACK_SYNC_STARTUP /
  FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real
  path again.
- Restore the window.slopsmith backward-compat alias dropped during conflict
  resolution, and move the bus aliases to AFTER the _feedBackExisting merge
  block so they reference the fully-assembled object (also fixes the
  loop_api.test.js API-surface regex, which the original PR latently broke).
- Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR)
  and gitignore it.
- Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped
  source labels.

Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669
passed / 1683 collected with 0 import errors; zero residual slopsmith/byron
except the two intentional window.slopsmith aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* rename: implement advertised backward-compat + prune dead community plugins

Address gaps where PR #537's "Backward compatibility" section was advertised
but not implemented, and clean up the community plugin list.

Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured):
- New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py
  (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and
  tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI /
  SLOPSMITH_PLUGINS_DIR / etc. deployments keep working.
- Fix the rename collapsing plugins/__init__.py and minigames/routes.py from
  `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant
  `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost).

Storage (app.js update-channel):
- Read feedBack-update-channel, fall back to legacy slopsmith-update-channel,
  and clear the legacy key on write — so a user's update-channel preference
  survives the rename instead of resetting to "stable".

Community plugin list (README): the rename rewrote third-party repo URLs we
don't own. Probed every one; their owners never renamed, so:
- Restore the 13 live community plugins to their real slopsmith-* names.
- Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex
  tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle).
- Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory).

Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors,
rename-sensitive + startup suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:01 +02:00

1437 lines
84 KiB
JavaScript

// Core audio-effects capability domain host.
(function () {
'use strict';
window.feedBack = window.feedBack || {};
const capabilities = window.feedBack.capabilities;
if (!capabilities || capabilities.version !== 1) return;
if (window.feedBack.audioEffects && window.feedBack.audioEffects.version === 1) return;
const SCHEMA = 'feedBack.audio_effects.diagnostics.v1';
const PLAN_SCHEMA = 'feedBack.audio_effects.chain_plan.v1';
const OWNER_ID = 'core.audio.effects';
const DEFAULT_ROUTE_KEY = 'desktop-main';
const DEFAULT_TIMEOUT_MS = 2000;
const MAPPING_ENDPOINT = '/api/audio-effects/mappings';
const MAX_PROVIDERS = 50;
const MAX_EXECUTORS = 20;
const MAX_ROUTES = 20;
const MAX_OUTCOMES = 120;
const MAX_BRIDGES = 60;
const MAX_STAGES = 24;
const MAX_SEGMENTS = 80;
const MAX_SAFE_ARRAY = 40;
const MAX_REASON = 240;
const VALID_KINDS = new Set(['nam', 'ir', 'vst', 'utility', 'bypass']);
const VALID_ROLES = new Set(['input', 'pre-pedal', 'pedal', 'amp', 'post-pedal', 'rack', 'cab', 'master-pre', 'master-post', 'utility', 'unknown']);
const VALID_OUTCOMES = new Set(['handled', 'denied', 'degraded', 'failed', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled']);
const LOAD_AUTHORIZATIONS = new Set(['user-action', 'restore-selection', 'playback-session']);
const RAW_KEY_RE = /(^|_)(asset|path|filepath|file|filename|url|uri|token|secret|password|api|apikey|key|native|preset|model|ir|vststate|stateblob|raw|buffer|sample|waveform|audio|handle|callback|function|node|element)(_|$)/i;
let sequence = 0;
const providers = new Map();
const executors = new Map();
const routes = new Map();
const bridges = new Map();
// Snapshot of each plugin's audio-effects participant declaration as it existed before the
// host first registered a runtime provider/executor for it (i.e. anything the plugin declared
// in its own manifest). The host overlay is rebuilt by unregister-then-register, so this lets
// it clear its own stale roles without clobbering the plugin's manifest declaration.
const manifestParticipants = new Map();
const recentOutcomes = [];
function _now() { return new Date().toISOString(); }
function _id(prefix) {
sequence += 1;
return `${prefix}-${sequence}`;
}
function _string(value, fallback = '') {
const normalized = String(value == null ? '' : value).trim();
return normalized || fallback;
}
// Select the first snake/camel alias the caller actually supplied, by key presence rather
// than truthiness, so a present-but-falsey non-string value (false/0) is forwarded to the
// server for validation instead of being silently swallowed by an `|| ''` chain.
function _aliasValue(payload, ...keys) {
if (payload && typeof payload === 'object') {
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key];
}
}
return undefined;
}
function _number(value, fallback = 0) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
}
function _bool(value, fallback = false) {
if (value === true || value === false) return value;
return fallback;
}
function _plainObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
}
function _asArray(value) {
return Array.isArray(value) ? value : [];
}
function _safeId(value, fallback = 'unknown') {
const raw = _string(value, fallback);
return raw.replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || fallback;
}
function _safeRoute(value) {
return _safeId(value || DEFAULT_ROUTE_KEY, DEFAULT_ROUTE_KEY);
}
function _safeKeyName(key) {
return _string(key).replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').toLowerCase();
}
function _redactString(value) {
return _string(value)
.replace(/(?:\/Users\/|\/home\/|\/root\b\/?)[^\r\n\t"'`,;(){}\[\]<>|]*/g, '[path]')
.replace(/[A-Za-z]:\\[^\r\n\t"'`,;(){}\[\]<>|]*/g, '[path]')
.replace(/https?:\/\/[^\s?#]+[^\s]*/gi, '[url]')
.replace(/file:\/\/[^\s]+/gi, '[path]')
.replace(/\b(token|secret|password|api[_-]?key|key)=([^\s&]+)/gi, '$1=[redacted]')
.replace(/\b[^\s]+\.(archive|sloppak|wem|ogg|mp3|wav|flac|nam|vst3|component|dll|json|db)\b/gi, '[file]')
.replace(/\b(raw[-_ ]?audio|audio[-_ ]?buffer|sample[s]?|waveform[s]?|recording[s]?|native[-_ ]?preset|model[-_ ]?file|ir[-_ ]?file|vst[-_ ]?state)\b/gi, '[private]');
}
function _boundedReason(value) {
return _redactString(value).replace(/\s+/g, ' ').slice(0, MAX_REASON);
}
function _safeValue(value, depth = 0, seen = null) {
if (typeof value === 'string') return _boundedReason(value);
if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
if (typeof value === 'function') return undefined;
if (depth > 6) return '[truncated]';
if (typeof value === 'object') {
const refs = seen || new WeakSet();
if (refs.has(value)) return '[circular]';
refs.add(value);
if (Array.isArray(value)) return value.slice(0, MAX_SAFE_ARRAY).map(item => _safeValue(item, depth + 1, refs)).filter(item => item !== undefined);
const out = {};
for (const [key, item] of Object.entries(value).slice(0, 40)) {
if (RAW_KEY_RE.test(_safeKeyName(key))) continue;
const safe = _safeValue(item, depth + 1, refs);
if (safe !== undefined) out[_safeId(key, 'field')] = safe;
}
return out;
}
return '';
}
function _clone(value) {
try { return JSON.parse(JSON.stringify(value)); }
catch (_) { return null; }
}
function _queryParam(params, name, value) {
// Absent or empty -> no filter; a present non-empty value (including a falsey non-string)
// is forwarded so the server, not the client, decides whether it is valid.
if (value === undefined || value === null) return;
const text = _string(value);
if (text) params.push(`${name}=${encodeURIComponent(text)}`);
}
function _mappingQuery(payload = {}) {
const params = [];
_queryParam(params, 'song_key', _aliasValue(payload, 'song_key', 'songKey'));
_queryParam(params, 'filename', _aliasValue(payload, 'filename', 'fileName'));
_queryParam(params, 'tone_key', _aliasValue(payload, 'tone_key', 'toneKey'));
_queryParam(params, 'provider_id', _aliasValue(payload, 'provider_id', 'providerId'));
const query = params.join('&');
return query ? `${MAPPING_ENDPOINT}?${query}` : MAPPING_ENDPOINT;
}
async function _mappingFetch(url, options = {}) {
if (typeof window.fetch !== 'function') return _unavailable('Audio-effects mapping API requires fetch');
let response;
// Bound the request so a stalled network call can't hang mapping flows
// indefinitely; abort after DEFAULT_TIMEOUT_MS and fail deterministically.
const controller = (typeof AbortController === 'function') ? new AbortController() : null;
const timer = controller ? setTimeout(() => { try { controller.abort(); } catch (_) {} }, DEFAULT_TIMEOUT_MS) : null;
try {
response = await window.fetch(url, controller ? { ...options, signal: controller.signal } : options);
} catch (error) {
if (timer) clearTimeout(timer);
if (error && error.name === 'AbortError') return _failed(`Audio-effects mapping request timed out after ${DEFAULT_TIMEOUT_MS} ms`);
return _failed(error && error.message ? error.message : 'Audio-effects mapping request failed');
}
if (timer) clearTimeout(timer);
let body = null;
try { body = await response.json(); }
catch (_) { body = {}; }
if (!response.ok || body && body.error) {
return _failed(body && body.error ? body.error : `Audio-effects mapping request failed (${response.status})`);
}
return { outcome: 'handled', status: 'handled', reason: 'Audio-effects mapping request handled', payload: body || {} };
}
function _operationHandlers(source) {
const handlers = {};
const input = _plainObject(source.operationHandlers || source.handlers || source.providerOperations);
for (const [key, handler] of Object.entries(input)) {
if (typeof handler === 'function') handlers[key] = handler;
}
return handlers;
}
function _executorHandlers(source) {
const handlers = _operationHandlers(source);
for (const name of ['loadChainPlan', 'releaseRoute', 'activateSegment', 'setStageBypass', 'setStageParameter', 'setRouteGain']) {
if (typeof source[name] === 'function') handlers[name] = source[name];
}
return handlers;
}
function _providerKey(pluginId, providerId) {
return `${pluginId}::${providerId}`;
}
function _executorKey(pluginId, executorId) {
return `${pluginId}::${executorId}`;
}
function _selectedProviderId(routeKey) {
const route = routes.get(routeKey);
return route && route.providerId ? route.providerId : '';
}
function _providerForRoute(routeKey, requestedProviderId = '') {
const requested = _safeId(requestedProviderId || _selectedProviderId(routeKey), '');
const available = Array.from(providers.values())
.filter(provider => provider.enabled && provider.availability !== 'unavailable' && provider.availability !== 'disabled')
.filter(provider => provider.routeKey === routeKey || provider.routeKey === DEFAULT_ROUTE_KEY || provider.routeKey === 'default')
.sort((a, b) => b.priority - a.priority || a.providerId.localeCompare(b.providerId));
if (requested) return available.find(provider => provider.providerId === requested || provider.pluginId === requested) || null;
return available[0] || null;
}
function _publicProvider(provider) {
if (!provider) return null;
return {
providerId: provider.providerId,
pluginId: provider.pluginId,
routeKey: provider.routeKey,
label: provider.label,
enabled: !!provider.enabled,
availability: provider.availability,
sourceMode: provider.sourceMode,
priority: provider.priority,
operations: provider.operations.slice(),
requests: (provider.requests || []).slice(),
capabilities: provider.capabilities.slice(),
dependencies: _clone(provider.dependencies) || {},
updatedAt: provider.updatedAt,
};
}
function _publicRoute(route) {
if (!route) return null;
return {
routeKey: route.routeKey,
chainId: route.chainId,
providerId: route.providerId,
executorId: route.executorId || '',
state: route.state,
bypassed: !!route.bypassed,
activePlanId: route.activePlanId || '',
activeSegmentId: route.activeSegmentId || '',
planSummary: _safeValue(route.planSummary || {}),
dependencies: _safeValue(route.dependencies || {}),
selectedAt: route.selectedAt,
updatedAt: route.updatedAt,
lastOutcome: route.lastOutcome ? _safeValue(route.lastOutcome) : null,
};
}
function _planSummary(plan) {
const stages = _asArray(plan && plan.stages);
const segments = _asArray(plan && plan.segments);
const kinds = Array.from(new Set(stages.map(stage => stage && stage.kind).filter(Boolean))).slice(0, 12);
const roles = Array.from(new Set(stages.map(stage => stage && stage.role).filter(Boolean))).slice(0, 12);
return _safeValue({ stageCount: stages.length, segmentCount: segments.length, kinds, roles });
}
function _clampedGain(value, fallback = Number.NaN) {
const numeric = _number(value, fallback);
return Number.isFinite(numeric) ? Math.max(0, Math.min(32, numeric)) : Number.NaN;
}
function _safeGainMap(source = {}) {
const input = _plainObject(source);
const gains = {};
for (const key of ['input', 'chain']) {
const value = _clampedGain(input[key]);
if (Number.isFinite(value)) gains[key] = value;
}
return gains;
}
function _safeExecutorOptions(source = {}) {
const input = _plainObject(source);
const options = {};
const rawPreload = _plainObject(input.preloadMute || input.preLoadMute || input.loadMute || input.preload || {});
if (input.preloadMute === true || input.loadMute === true || Object.keys(rawPreload).length) {
const targetGain = _clampedGain(rawPreload.targetGain ?? rawPreload.restoreGain ?? rawPreload.chainGain, 1);
options.preloadMute = {
enabled: rawPreload.enabled !== false && input.preloadMute !== false && input.loadMute !== false,
dryDuringLoad: rawPreload.dryDuringLoad !== false,
targetGain: Number.isFinite(targetGain) ? targetGain : 1,
holdMs: Math.max(0, Math.min(5000, Math.round(_number(rawPreload.holdMs, 0)))),
};
}
const gains = _safeGainMap(input.gains || input.gain || {});
if (Object.keys(gains).length) options.gains = gains;
if (input.startAudio === true) options.startAudio = true;
return options;
}
function _publicExecutor(executor) {
if (!executor) return null;
return {
executorId: executor.executorId,
pluginId: executor.pluginId,
routeKey: executor.routeKey,
label: executor.label,
enabled: !!executor.enabled,
availability: executor.availability,
sourceMode: executor.sourceMode,
priority: executor.priority,
providerIds: executor.providerIds.slice(),
supportedKinds: executor.supportedKinds.slice(),
maxStages: executor.maxStages,
operations: executor.operations.slice(),
updatedAt: executor.updatedAt,
};
}
function _decision(outcome, reason, payload = {}) {
const normalized = VALID_OUTCOMES.has(outcome) ? outcome : 'failed';
return { outcome: normalized, status: normalized, reason: _boundedReason(reason), payload: _safeValue(payload) };
}
function _handled(payload = {}, reason = 'Handled') { return _decision('handled', reason, payload); }
function _failed(reason, payload = {}) { return _decision('failed', reason, payload); }
function _unsupported(reason, payload = {}) { return _decision('unsupported-command', reason, payload); }
function _noTarget(reason, payload = {}) { return _decision('no-target', reason, payload); }
function _noHandler(reason, payload = {}) { return _decision('no-handler', reason, payload); }
function _unavailable(reason, payload = {}) { return _decision('unavailable', reason, payload); }
function _recordOutcome(entry = {}) {
const outcome = {
operation: _safeId(entry.operation || entry.command || 'unknown', 'unknown'),
outcome: VALID_OUTCOMES.has(entry.outcome) ? entry.outcome : (VALID_OUTCOMES.has(entry.status) ? entry.status : 'handled'),
status: _safeId(entry.status || entry.outcome || 'handled', 'handled'),
routeKey: entry.routeKey ? _safeRoute(entry.routeKey) : undefined,
providerId: entry.providerId ? _safeId(entry.providerId) : undefined,
pluginId: entry.pluginId ? _safeId(entry.pluginId) : undefined,
reason: _boundedReason(entry.reason || ''),
details: _safeValue(entry.details || entry.summary || {}),
timestamp: _now(),
};
recentOutcomes.push(outcome);
while (recentOutcomes.length > MAX_OUTCOMES) recentOutcomes.shift();
_contributeDiagnostics();
return outcome;
}
function _touchRoute(route, outcome = null) {
route.updatedAt = _now();
if (outcome) route.lastOutcome = outcome;
}
function _emit(event, detail) {
try {
if (window.feedBack && typeof window.feedBack.emit === 'function') window.feedBack.emit(event, detail);
} catch (_) { /* best effort */ }
}
function _invokeWithTimeout(handler, request, timeoutMs = DEFAULT_TIMEOUT_MS) {
return new Promise(resolve => {
let settled = false;
const finish = value => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({ timedOut: false, value });
};
const timer = setTimeout(() => {
if (settled) return;
settled = true;
resolve({ timedOut: true, value: null });
}, Math.max(1, _number(timeoutMs, DEFAULT_TIMEOUT_MS)));
try { Promise.resolve(handler(request)).then(finish, error => finish({ outcome: 'failed', reason: error && error.message ? error.message : String(error || 'handler failed') })); }
catch (error) { finish({ outcome: 'failed', reason: error && error.message ? error.message : String(error || 'handler failed') }); }
});
}
function _desktopExecutor() {
const desktop = window.feedBackDesktop && window.feedBackDesktop.audioEffects;
if (!desktop || typeof desktop.loadChainPlan !== 'function') return null;
const handlers = { loadChainPlan: desktop.loadChainPlan.bind(desktop) };
if (typeof desktop.releaseRoute === 'function') handlers.releaseRoute = desktop.releaseRoute.bind(desktop);
if (typeof desktop.activateSegment === 'function') handlers.activateSegment = desktop.activateSegment.bind(desktop);
if (typeof desktop.setStageBypass === 'function') handlers.setStageBypass = desktop.setStageBypass.bind(desktop);
if (typeof desktop.setStageParameter === 'function') handlers.setStageParameter = desktop.setStageParameter.bind(desktop);
if (typeof desktop.setRouteGain === 'function') handlers.setRouteGain = desktop.setRouteGain.bind(desktop);
return {
executorId: 'desktop-native',
pluginId: 'feedBack-desktop',
routeKey: DEFAULT_ROUTE_KEY,
label: 'FeedBack Desktop native audio',
enabled: true,
availability: 'available',
sourceMode: 'native',
priority: 100,
providerIds: [],
supportedKinds: [],
maxStages: MAX_STAGES,
operations: Object.keys(handlers),
operationHandlers: handlers,
registeredAt: '',
updatedAt: '',
};
}
function _executorSupports(executor, routeKey, providerId, requestedExecutorId = '') {
if (!executor || !executor.enabled || executor.availability === 'unavailable' || executor.availability === 'disabled') return false;
if (requestedExecutorId && executor.executorId !== requestedExecutorId && executor.pluginId !== requestedExecutorId) return false;
if (executor.routeKey !== routeKey && executor.routeKey !== DEFAULT_ROUTE_KEY && executor.routeKey !== 'default') return false;
if (executor.providerIds.length && !executor.providerIds.includes(providerId)) return false;
return true;
}
function _executorForRoute(routeKey, providerId, requestedExecutorId = '') {
const requested = _safeId(requestedExecutorId || '', '');
const candidates = Array.from(executors.values());
const desktop = _desktopExecutor();
if (desktop) candidates.push(desktop);
return candidates
.filter(executor => _executorSupports(executor, routeKey, providerId, requested))
.sort((a, b) => b.priority - a.priority || a.executorId.localeCompare(b.executorId))[0] || null;
}
function _executorPlanSupport(executor, plan) {
if (!executor) return { ok: false, reason: 'No compatible audio-effects executor is available' };
const stages = _asArray(plan && plan.stages);
if (executor.maxStages > 0 && stages.length > executor.maxStages) {
return { ok: false, reason: `Executor supports at most ${executor.maxStages} stages` };
}
const supportedKinds = new Set(_asArray(executor.supportedKinds));
if (supportedKinds.size) {
const unsupported = stages.find(stage => !supportedKinds.has(stage.kind));
if (unsupported) return { ok: false, reason: `Executor does not support ${unsupported.kind} stages` };
}
return { ok: true, reason: '' };
}
function _executorForPlan(routeKey, providerId, plan, requestedExecutorId = '') {
const requested = _safeId(requestedExecutorId || '', '');
const candidates = Array.from(executors.values());
const desktop = _desktopExecutor();
if (desktop) candidates.push(desktop);
return candidates
.filter(executor => _executorSupports(executor, routeKey, providerId, requested))
.filter(executor => _executorPlanSupport(executor, plan).ok)
.sort((a, b) => b.priority - a.priority || a.executorId.localeCompare(b.executorId))[0] || null;
}
function _captureManifestParticipant(id) {
// Record the plugin's pre-host declaration exactly once, before the host registers any
// runtime overlay for it, so later rebuilds can restore it. Stored value is null when the
// plugin never declared audio-effects itself.
if (manifestParticipants.has(id)) return;
let snapshot = null;
try {
const pipeline = typeof capabilities.inspect === 'function' ? capabilities.inspect('audio-effects') : null;
const existing = pipeline && Array.isArray(pipeline.participants)
? pipeline.participants.find(item => item && item.pluginId === id)
: null;
if (existing) {
snapshot = {
roles: _asArray(existing.roles),
operations: _asArray(existing.operations),
requests: _asArray(existing.requests),
events: _asArray(existing.events),
description: existing.description || '',
mode: existing.mode || 'active',
compatibility: existing.compatibility,
ownership: existing.ownership,
safety: existing.safety,
runtime: !!existing.runtime,
version: existing.version || 1,
};
}
} catch (_err) {
snapshot = null;
}
manifestParticipants.set(id, snapshot);
}
function _syncCapabilityParticipant(pluginId) {
if (!capabilities || typeof capabilities.registerParticipant !== 'function') return;
const id = _safeId(pluginId || '', '');
if (!id) return;
_captureManifestParticipant(id);
const ownedProviders = Array.from(providers.values()).filter(provider => provider.pluginId === id);
const ownedExecutors = Array.from(executors.values()).filter(executor => executor.pluginId === id);
// capabilities.registerParticipant merges declarations by union and never drops
// roles/operations, so refreshing in place would keep stale provider state after an
// executor-only unregister. Clear the prior declaration first so the rebuilt overlay is a
// true replacement, and so removing the last provider/executor also removes the host
// overlay instead of leaving it registered forever. Restore the plugin's own manifest
// declaration (if any) so only the host's runtime overlay is rebuilt, never clobbered.
if (typeof capabilities.unregisterParticipant === 'function') {
capabilities.unregisterParticipant(id, 'audio-effects');
}
const manifest = manifestParticipants.get(id);
if (manifest) {
capabilities.registerParticipant(id, { 'audio-effects': { ...manifest } });
}
if (!ownedProviders.length && !ownedExecutors.length) return;
const operations = Array.from(new Set([
...ownedProviders.flatMap(provider => _asArray(provider.operations)),
...ownedExecutors.flatMap(executor => _asArray(executor.operations)),
...(ownedExecutors.length ? ['executor.load-chain-plan'] : []),
].map(item => _safeId(item, '')).filter(Boolean))).slice(0, 40);
const requests = Array.from(new Set([
...ownedProviders.flatMap(provider => _asArray(provider.requests)),
...ownedExecutors.flatMap(executor => _asArray(executor.requests)),
].map(item => _safeId(item, '')).filter(Boolean))).slice(0, 40);
const events = ['provider-registered', 'executor-registered', 'route-selected', 'plan-resolved', 'changed', 'fallback', 'bridge-hit'];
capabilities.registerParticipant(id, {
'audio-effects': {
roles: [
...(ownedProviders.length ? ['provider'] : []),
...(ownedExecutors.length ? ['executor'] : []),
...(requests.length ? ['requester'] : []),
],
requests,
operations,
events,
description: 'Runtime audio-effects provider/executor participant registered through the core audio-effects host.',
mode: ownedProviders.some(provider => provider.enabled !== false) || ownedExecutors.some(executor => executor.enabled !== false) ? 'active' : 'disabled',
compatibility: 'shim-allowed',
ownership: 'multi-provider',
safety: 'sensitive',
runtime: true,
version: 1,
},
});
}
function registerProvider(input = {}) {
const source = _plainObject(input);
const providerId = _safeId(source.providerId || source.participantId || source.id || source.pluginId, 'provider');
const pluginId = _safeId(source.pluginId || source.ownerPluginId || source.ownerId || providerId, providerId);
const key = _providerKey(pluginId, providerId);
if (providers.size >= MAX_PROVIDERS && !providers.has(key)) return _failed('audio-effects provider registry is full');
const routeKey = _safeRoute(source.routeKey || source.route || DEFAULT_ROUTE_KEY);
const operationHandlers = _operationHandlers(source);
const operations = Array.from(new Set([
..._asArray(source.operations || source.commands || source.capabilities).map(item => _safeId(item, '')).filter(Boolean),
...Object.keys(operationHandlers),
])).slice(0, 30);
const requests = Array.from(new Set(_asArray(source.requests || source.requestedCommands || source.requested_commands)
.map(item => _safeId(item, ''))
.filter(Boolean))).slice(0, 30);
const provider = {
providerId,
pluginId,
routeKey,
label: _boundedReason(source.label || source.name || providerId).slice(0, 96),
enabled: source.enabled !== false,
availability: _safeId(source.availability || 'available', 'available'),
sourceMode: source.sourceMode === 'compatibility' || source.compatibilityMode === 'compatibility' ? 'compatibility' : 'native',
priority: _number(source.priority, 0),
operations,
requests,
capabilities: operations.slice(),
dependencies: _safeValue(source.dependencies || {}),
operationHandlers,
registeredAt: _now(),
updatedAt: _now(),
};
providers.set(key, provider);
_syncCapabilityParticipant(pluginId);
const outcome = _recordOutcome({ operation: 'register-provider', outcome: 'handled', providerId, pluginId, routeKey });
_emit('audio-effects:provider-registered', { provider: _publicProvider(provider) });
_contributeDiagnostics();
return _handled({ provider: _publicProvider(provider), outcome }, 'Audio-effects provider registered');
}
function unregisterProvider(input = {}) {
const providerId = _safeId(input.providerId || input.participantId || input.id || '', '');
const pluginId = _safeId(input.pluginId || input.ownerPluginId || input.ownerId || '', '');
let key = '';
if (providerId && pluginId) key = _providerKey(pluginId, providerId);
else if (providerId) {
key = Array.from(providers.keys()).find(item => item.endsWith(`::${providerId}`)) || '';
}
if (!key || !providers.has(key)) return _noTarget('Audio-effects provider was not registered');
const provider = providers.get(key);
providers.delete(key);
for (const route of routes.values()) {
if (route.providerId === provider.providerId) {
route.state = 'provider-unavailable';
route.activePlanId = '';
route.activeSegmentId = '';
route.executorId = '';
route.planSummary = {};
_touchRoute(route);
}
}
_syncCapabilityParticipant(provider.pluginId);
const outcome = _recordOutcome({ operation: 'unregister-provider', outcome: 'handled', providerId: provider.providerId, pluginId: provider.pluginId, routeKey: provider.routeKey });
_emit('audio-effects:provider-unregistered', { providerId: provider.providerId, pluginId: provider.pluginId });
_contributeDiagnostics();
return _handled({ providerId: provider.providerId, outcome }, 'Audio-effects provider unregistered');
}
function registerExecutor(input = {}) {
const source = _plainObject(input);
const executorId = _safeId(source.executorId || source.id || source.pluginId, 'executor');
const pluginId = _safeId(source.pluginId || source.ownerPluginId || source.ownerId || executorId, executorId);
const key = _executorKey(pluginId, executorId);
if (executors.size >= MAX_EXECUTORS && !executors.has(key)) return _failed('audio-effects executor registry is full');
const routeKey = _safeRoute(source.routeKey || source.route || DEFAULT_ROUTE_KEY);
const operationHandlers = _executorHandlers(source);
const operations = Array.from(new Set([
..._asArray(source.operations || source.commands || source.capabilities).map(item => _safeId(item, '')).filter(Boolean),
...Object.keys(operationHandlers),
])).slice(0, 20);
const providerIds = _asArray(source.providerIds || source.supportedProviders || source.providers)
.map(item => _safeId(item, ''))
.filter(Boolean)
.slice(0, MAX_PROVIDERS);
const supportedKinds = _asArray(source.supportedKinds || source.stageKinds || source.kinds)
.map(item => _safeId(item, ''))
.filter(kind => VALID_KINDS.has(kind))
.slice(0, VALID_KINDS.size);
const maxStages = Math.max(0, Math.min(MAX_STAGES, _number(source.maxStages || source.stageLimit, 0)));
const mode = _safeId(source.sourceMode || source.mode || 'native', 'native');
const executor = {
executorId,
pluginId,
routeKey,
label: _boundedReason(source.label || source.name || executorId).slice(0, 96),
enabled: source.enabled !== false,
availability: _safeId(source.availability || 'available', 'available'),
sourceMode: mode === 'compatibility' || mode === 'browser' || mode === 'native' ? mode : 'native',
priority: _number(source.priority, 0),
providerIds,
supportedKinds,
maxStages,
operations,
operationHandlers,
registeredAt: _now(),
updatedAt: _now(),
};
executors.set(key, executor);
_syncCapabilityParticipant(pluginId);
const outcome = _recordOutcome({ operation: 'register-executor', outcome: 'handled', pluginId, routeKey, details: { executorId, providerIds, supportedKinds, maxStages } });
_emit('audio-effects:executor-registered', { executor: _publicExecutor(executor) });
_contributeDiagnostics();
return _handled({ executor: _publicExecutor(executor), outcome }, 'Audio-effects executor registered');
}
function unregisterExecutor(input = {}) {
const executorId = _safeId(input.executorId || input.id || '', '');
const pluginId = _safeId(input.pluginId || input.ownerPluginId || input.ownerId || '', '');
let key = '';
if (executorId && pluginId) key = _executorKey(pluginId, executorId);
else if (executorId) key = Array.from(executors.keys()).find(item => item.endsWith(`::${executorId}`)) || '';
if (!key || !executors.has(key)) return _noTarget('Audio-effects executor was not registered');
const executor = executors.get(key);
executors.delete(key);
for (const route of routes.values()) {
if (route.executorId === executor.executorId) {
route.executorId = '';
route.state = 'executor-unavailable';
_touchRoute(route);
}
}
_syncCapabilityParticipant(executor.pluginId);
const outcome = _recordOutcome({ operation: 'unregister-executor', outcome: 'handled', pluginId: executor.pluginId, routeKey: executor.routeKey, details: { executorId: executor.executorId } });
_emit('audio-effects:executor-unregistered', { executorId: executor.executorId, pluginId: executor.pluginId });
_contributeDiagnostics();
return _handled({ executorId: executor.executorId, outcome }, 'Audio-effects executor unregistered');
}
function listExecutors(payload = {}) {
const routeKey = payload.routeKey || payload.route ? _safeRoute(payload.routeKey || payload.route) : '';
const providerId = payload.providerId ? _safeId(payload.providerId, '') : '';
const desktop = _desktopExecutor();
const items = Array.from(executors.values())
.concat(desktop ? [desktop] : [])
.filter(executor => (!routeKey || executor.routeKey === routeKey || executor.routeKey === DEFAULT_ROUTE_KEY || executor.routeKey === 'default')
&& (!providerId || !executor.providerIds.length || executor.providerIds.includes(providerId)))
.map(_publicExecutor);
return _handled({ executors: items }, 'Listed audio-effects executors');
}
function listProviders(payload = {}) {
const routeKey = payload.routeKey || payload.route ? _safeRoute(payload.routeKey || payload.route) : '';
const items = Array.from(providers.values())
.filter(provider => !routeKey || provider.routeKey === routeKey || provider.routeKey === DEFAULT_ROUTE_KEY || provider.routeKey === 'default')
.map(_publicProvider);
return _handled({ providers: items }, 'Listed audio-effects providers');
}
function selectChain(payload = {}, requester = '') {
const routeKey = _safeRoute(payload.routeKey || payload.route);
if (payload.authorization !== 'user-action' && payload.authorization !== 'restore-selection') {
return _decision('user-action-required', 'Selecting an audio-effects chain requires visible user action or restored selection', { routeKey });
}
const provider = _providerForRoute(routeKey, payload.providerId || payload.participantId);
if (!provider) return _decision('provider-selection-required', 'No enabled audio-effects provider is available for route', { routeKey });
const route = {
routeKey,
chainId: _id('effects'),
providerId: provider.providerId,
state: 'selected',
bypassed: false,
activePlanId: '',
activeSegmentId: '',
planSummary: _safeValue(payload.chainSummary || payload.planSummary || payload.summary || {}),
dependencies: _safeValue(payload.dependencies || provider.dependencies || {}),
selectedAt: _now(),
updatedAt: _now(),
selectedBy: _safeId(requester || 'unknown', 'unknown'),
lastOutcome: null,
};
routes.set(routeKey, route);
const outcome = _recordOutcome({ operation: 'select-chain', outcome: 'handled', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, details: { planSummary: route.planSummary } });
_touchRoute(route, outcome);
_emit('audio-effects:route-selected', { route: _publicRoute(route), provider: _publicProvider(provider) });
_contributeDiagnostics();
return _handled({ route: _publicRoute(route), provider: _publicProvider(provider) }, 'Audio-effects chain selected');
}
function _validateRef(value, field, errors) {
const text = _string(value);
if (!text) return '';
if (/^(?:https?:|file:)/i.test(text) || /(?:^\/Users\/|^\/home\/|^\/root\b|^[A-Za-z]:\\)/.test(text) || /\.(nam|wav|flac|vst3|component|dll)(?:$|[?#])/i.test(text)) {
errors.push(`${field} must be an opaque provider or host reference, not a path or URL`);
return '';
}
return _boundedReason(text).slice(0, 160);
}
function _validatePlan(rawPlan, provider, routeKey) {
const errors = [];
const source = _plainObject(rawPlan);
const schema = _string(source.schema || source.version, PLAN_SCHEMA);
if (schema !== PLAN_SCHEMA && schema !== '1') errors.push('Unsupported chain plan schema');
const planRoute = _safeRoute(source.routeKey || source.route || routeKey);
if (planRoute !== routeKey) errors.push('Chain plan route does not match selected route');
const providerId = _safeId(source.providerId || provider.providerId, provider.providerId);
if (providerId !== provider.providerId) errors.push('Chain plan provider does not match selected provider');
const stages = _asArray(source.stages || source.chain);
if (!stages.length) errors.push('Chain plan must include at least one stage');
if (stages.length > MAX_STAGES) errors.push(`Chain plan exceeds maximum stage count ${MAX_STAGES}`);
const safeStages = [];
stages.slice(0, MAX_STAGES).forEach((stage, index) => {
const item = _plainObject(stage);
const kind = _safeId(item.kind || item.stageKind || (item.type === 1 ? 'nam' : item.type === 2 ? 'ir' : item.type === 0 ? 'vst' : ''), '');
if (!VALID_KINDS.has(kind)) errors.push(`Stage ${index} has unsupported kind`);
const rawRole = _safeId(item.role || item.slot || item.category || 'unknown', 'unknown');
const role = VALID_ROLES.has(rawRole) ? rawRole : 'unknown';
const stageId = _safeId(item.stageId || item.id || `${kind || 'stage'}-${index}`, `stage-${index}`);
const assetRef = _validateRef(item.assetRef || item.modelRef || item.irRef || item.pluginRef || item.ref, `stage ${index} assetRef`, errors);
const stateRef = _validateRef(item.stateRef || item.paramsRef || item.parameterRef, `stage ${index} stateRef`, errors);
if ((kind === 'nam' || kind === 'ir' || kind === 'vst') && !assetRef) errors.push(`Stage ${index} requires an opaque assetRef`);
safeStages.push({
stageId,
kind: VALID_KINDS.has(kind) ? kind : 'utility',
role,
assetRef,
stateRef,
bypassed: _bool(item.bypassed, false),
gainDb: _number(item.gainDb, 0),
summary: _safeValue(item.summary || {}),
});
});
const rawSegments = _asArray(source.segments || source.toneSegments || source.segmentMap).slice(0, MAX_SEGMENTS);
const segments = rawSegments.map((segment, index) => {
const item = _plainObject(segment);
const rawStageBypass = _plainObject(item.stageBypass || item.stageBypasses || item.bypassByStage || {});
const stageBypass = {};
for (const [stageId, bypassed] of Object.entries(rawStageBypass).slice(0, MAX_STAGES)) {
const safeStageId = _safeId(stageId, '');
if (safeStageId) stageBypass[safeStageId] = _bool(bypassed, false);
}
return {
segmentId: _safeId(item.segmentId || item.toneKey || item.id || `segment-${index}`, `segment-${index}`),
stageIds: _asArray(item.stageIds || item.stages).map(value => _safeId(value, '')).filter(Boolean).slice(0, MAX_STAGES),
stageBypass,
summary: _safeValue(item.summary || {}),
};
});
const plan = {
schema: PLAN_SCHEMA,
planId: _safeId(source.planId || source.chainId || _id('plan'), 'plan'),
routeKey: planRoute,
providerId,
stages: safeStages,
segments,
summary: _safeValue(source.summary || source.chainSummary || {}),
};
return { ok: errors.length === 0, plan, errors };
}
async function resolvePlan(payload = {}, requester = '') {
const routeKey = _safeRoute(payload.routeKey || payload.route);
let route = routes.get(routeKey);
const provider = _providerForRoute(routeKey, payload.providerId || payload.participantId || (route && route.providerId));
if (!provider) return _decision('provider-selection-required', 'No enabled audio-effects provider is available for route', { routeKey });
if (!route || route.providerId !== provider.providerId) {
route = {
routeKey,
chainId: _id('effects'),
providerId: provider.providerId,
state: 'resolving',
bypassed: false,
activePlanId: '',
activeSegmentId: '',
planSummary: {},
dependencies: _safeValue(provider.dependencies || {}),
selectedAt: _now(),
updatedAt: _now(),
selectedBy: _safeId(requester || 'unknown', 'unknown'),
lastOutcome: null,
};
routes.set(routeKey, route);
}
const handler = provider.operationHandlers['chain.resolve'] || provider.operationHandlers['resolve-plan'];
if (typeof handler !== 'function') {
const outcome = _recordOutcome({ operation: 'resolve-plan', outcome: 'no-handler', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: 'Selected provider does not support chain.resolve' });
route.state = 'fallback';
_touchRoute(route, outcome);
return _noHandler('Selected audio-effects provider does not support chain.resolve', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
route.state = 'resolving';
_touchRoute(route);
const request = {
routeKey,
requesterId: _safeId(requester || 'unknown', 'unknown'),
target: _clone(payload.target || {}) || {},
planRequest: _clone(payload.planRequest || payload.summary || {}) || {},
authorization: _safeId(payload.authorization || '', ''),
};
const invoked = await _invokeWithTimeout(handler, request, payload.timeoutMs);
if (invoked.timedOut) {
const outcome = _recordOutcome({ operation: 'resolve-plan', outcome: 'failed', status: 'timeout', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: 'Provider chain resolution timed out' });
route.state = 'fallback';
_touchRoute(route, outcome);
return _failed('Provider chain resolution timed out', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
const result = _plainObject(invoked.value);
const outcomeName = _string(result.outcome || result.status || 'handled', 'handled');
if (outcomeName !== 'handled') {
const outcome = _recordOutcome({ operation: 'resolve-plan', outcome: VALID_OUTCOMES.has(outcomeName) ? outcomeName : 'failed', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: result.reason || 'Provider did not resolve a plan', details: result.summary || {} });
route.state = outcome.outcome === 'no-target' ? 'fallback' : 'degraded';
_touchRoute(route, outcome);
return _decision(outcome.outcome, result.reason || 'Provider did not resolve a plan', { route: _publicRoute(route), provider: _publicProvider(provider), providerResult: _safeValue(result.summary || {}) });
}
const rawPlan = result.plan || result.chainPlan || result.payload || invoked.value;
const validation = _validatePlan(rawPlan, provider, routeKey);
if (!validation.ok) {
const outcome = _recordOutcome({ operation: 'resolve-plan', outcome: 'failed', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: validation.errors.join('; ') });
route.state = 'fallback';
_touchRoute(route, outcome);
return _failed('Provider returned an invalid audio-effects chain plan', { route: _publicRoute(route), provider: _publicProvider(provider), errors: validation.errors });
}
route.state = 'resolved';
route.activePlanId = validation.plan.planId;
route.planSummary = _planSummary(validation.plan);
route.dependencies = _safeValue(result.dependencies || route.dependencies || {});
const outcome = _recordOutcome({ operation: 'resolve-plan', outcome: 'handled', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, details: { planId: validation.plan.planId, stageCount: validation.plan.stages.length, segmentCount: validation.plan.segments.length } });
_touchRoute(route, outcome);
_emit('audio-effects:plan-resolved', { route: _publicRoute(route), planSummary: route.planSummary });
_contributeDiagnostics();
return { outcome: 'handled', status: 'handled', reason: 'Audio-effects chain plan resolved', payload: { route: _publicRoute(route), provider: _publicProvider(provider), plan: validation.plan } };
}
async function loadPlan(payload = {}, requester = '') {
const authorization = _safeId(payload.authorization || '', '');
const routeKey = _safeRoute(payload.routeKey || payload.route);
if (!LOAD_AUTHORIZATIONS.has(authorization)) {
return _decision('user-action-required', 'Loading an audio-effects plan requires user-action, restore-selection, or playback-session authorization', { routeKey });
}
let route = routes.get(routeKey);
const explicitProviderId = payload.providerId || payload.participantId || '';
const selectedProviderId = route && route.providerId || '';
let provider = _providerForRoute(routeKey, explicitProviderId || selectedProviderId);
if (!provider) return _decision('provider-selection-required', 'No enabled audio-effects provider is available for route', { routeKey });
let executor = _executorForRoute(routeKey, provider.providerId, payload.executorId || route && route.executorId || '');
if (!executor && !explicitProviderId && payload.fallbackProviderId) {
const fallbackProvider = _providerForRoute(routeKey, payload.fallbackProviderId);
const fallbackExecutor = fallbackProvider ? _executorForRoute(routeKey, fallbackProvider.providerId, payload.executorId || '') : null;
if (fallbackProvider && fallbackExecutor) {
provider = fallbackProvider;
executor = fallbackExecutor;
}
}
if (!route || route.providerId !== provider.providerId) {
route = {
routeKey,
chainId: _id('effects'),
providerId: provider.providerId,
state: 'resolving',
bypassed: false,
activePlanId: '',
activeSegmentId: '',
planSummary: {},
dependencies: _safeValue(provider.dependencies || {}),
selectedAt: _now(),
updatedAt: _now(),
selectedBy: _safeId(requester || 'unknown', 'unknown'),
lastOutcome: null,
};
routes.set(routeKey, route);
}
if (!executor || typeof executor.operationHandlers.loadChainPlan !== 'function') {
const outcome = _recordOutcome({ operation: 'load-plan', outcome: 'unavailable', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: 'No compatible audio-effects executor is available' });
route.state = 'fallback';
_touchRoute(route, outcome);
return _unavailable('No compatible audio-effects executor is available', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
const handler = provider.operationHandlers['chain.resolve'] || provider.operationHandlers['resolve-plan'];
if (typeof handler !== 'function') {
const outcome = _recordOutcome({ operation: 'load-plan', outcome: 'no-handler', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: 'Selected provider does not support chain.resolve' });
route.state = 'fallback';
_touchRoute(route, outcome);
return _noHandler('Selected audio-effects provider does not support chain.resolve', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
const request = {
routeKey,
requesterId: _safeId(requester || 'unknown', 'unknown'),
target: _clone(payload.target || {}) || {},
planRequest: _clone(payload.planRequest || payload.summary || {}) || {},
authorization,
};
route.state = 'resolving';
_touchRoute(route);
const invoked = await _invokeWithTimeout(handler, request, payload.timeoutMs);
if (invoked.timedOut) {
const outcome = _recordOutcome({ operation: 'load-plan', outcome: 'failed', status: 'timeout', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: 'Provider chain resolution timed out' });
route.state = 'fallback';
_touchRoute(route, outcome);
return _failed('Provider chain resolution timed out', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
const result = _plainObject(invoked.value);
const outcomeName = _string(result.outcome || result.status || 'handled', 'handled');
if (outcomeName !== 'handled') {
const outcome = _recordOutcome({ operation: 'load-plan', outcome: VALID_OUTCOMES.has(outcomeName) ? outcomeName : 'failed', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: result.reason || 'Provider did not resolve a plan', details: result.summary || {} });
route.state = outcome.outcome === 'no-target' ? 'fallback' : 'degraded';
_touchRoute(route, outcome);
return _decision(outcome.outcome, result.reason || 'Provider did not resolve a plan', { route: _publicRoute(route), provider: _publicProvider(provider), providerResult: _safeValue(result.summary || {}) });
}
const rawPlan = result.plan || result.chainPlan || result.payload || invoked.value;
const validation = _validatePlan(rawPlan, provider, routeKey);
if (!validation.ok) {
const outcome = _recordOutcome({ operation: 'load-plan', outcome: 'failed', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: validation.errors.join('; ') });
route.state = 'fallback';
_touchRoute(route, outcome);
return _failed('Provider returned an invalid audio-effects chain plan', { route: _publicRoute(route), provider: _publicProvider(provider), errors: validation.errors });
}
const planExecutor = _executorForPlan(routeKey, provider.providerId, validation.plan, payload.executorId || route.executorId || '');
if (!planExecutor) {
const support = _executorPlanSupport(executor, validation.plan);
if (!explicitProviderId && payload.fallbackProviderId && provider.providerId !== _safeId(payload.fallbackProviderId, '')) {
return loadPlan({ ...payload, providerId: payload.fallbackProviderId, fallbackProviderId: '' }, requester);
}
const outcome = _recordOutcome({ operation: 'load-plan', outcome: 'unavailable', providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: support.reason || 'No compatible audio-effects executor can load this chain plan' });
route.state = 'fallback';
_touchRoute(route, outcome);
return _unavailable('No compatible audio-effects executor can load this chain plan', { route: _publicRoute(route), provider: _publicProvider(provider), reason: support.reason });
}
executor = planExecutor;
const assets = result.assets || result.trustedAssets || result.assetMap || {};
const states = result.states || result.trustedStates || result.stateMap || undefined;
let executorResult;
{
// Bound the executor call so a hung promise can't stall load-plan; the
// arrow preserves `this` for plugin-provided handlers. _invokeWithTimeout
// also catches handler throws (value -> {outcome:'failed',reason}).
const _exReq = {
authorization,
routeKey,
plan: validation.plan,
assets,
states,
options: _safeExecutorOptions(payload.options || payload.executorOptions || payload.loadOptions || {}),
target: request.target,
planRequest: request.planRequest,
};
const _ex = await _invokeWithTimeout((r) => executor.operationHandlers.loadChainPlan(r), _exReq, payload.timeoutMs);
executorResult = _ex.timedOut
? { outcome: 'failed', status: 'timeout', reason: 'Executor loadChainPlan timed out' }
: _ex.value;
}
const executorOutcome = _plainObject(executorResult);
const executorOutcomeName = _string(executorOutcome.outcome || executorOutcome.status || 'failed', 'failed');
const normalizedOutcome = VALID_OUTCOMES.has(executorOutcomeName) ? executorOutcomeName : 'failed';
route.activePlanId = validation.plan.planId;
route.executorId = executor.executorId;
route.planSummary = _planSummary(validation.plan);
route.dependencies = _safeValue(result.dependencies || route.dependencies || {});
route.state = normalizedOutcome === 'handled' ? 'loaded' : (normalizedOutcome === 'degraded' ? 'degraded' : 'fallback');
const outcome = _recordOutcome({
operation: 'load-plan',
outcome: normalizedOutcome,
status: _safeId(executorOutcome.status || normalizedOutcome, normalizedOutcome),
providerId: provider.providerId,
pluginId: provider.pluginId,
routeKey,
reason: executorOutcome.reason || 'Audio-effects executor completed',
details: { executorId: executor.executorId, result: executorOutcome.payload || executorOutcome.summary || {} },
});
_touchRoute(route, outcome);
_emit('audio-effects:changed', { route: _publicRoute(route), operation: 'load-plan', outcome: normalizedOutcome });
_contributeDiagnostics();
return _decision(normalizedOutcome, executorOutcome.reason || 'Audio-effects executor completed', {
route: _publicRoute(route),
provider: _publicProvider(provider),
executor: _publicExecutor(executor),
result: _safeValue(executorOutcome.payload || executorOutcome.summary || {}),
});
}
async function _providerOperation(operation, payload = {}, requester = '') {
const routeKey = _safeRoute(payload.routeKey || payload.route);
const route = routes.get(routeKey);
if (!route) return _noTarget('No active audio-effects route was selected', { routeKey });
const provider = _providerForRoute(routeKey, route.providerId);
if (!provider) return _unavailable('Selected audio-effects provider is unavailable', { route: _publicRoute(route) });
const handler = provider.operationHandlers[operation];
if (typeof handler !== 'function') return _noHandler(`Selected audio-effects provider does not support ${operation}`, { route: _publicRoute(route), provider: _publicProvider(provider) });
const request = {
routeKey,
requesterId: _safeId(requester || 'unknown', 'unknown'),
planId: _safeId(payload.planId || route.activePlanId || '', ''),
segmentId: _safeId(payload.segmentId || payload.toneKey || payload.activeSegmentId || '', ''),
stageId: _safeId(payload.stageId || '', ''),
bypassed: _bool(payload.bypassed, false),
parameterId: _safeId(payload.parameterId || payload.paramId || '', ''),
value: typeof payload.value === 'number' ? payload.value : undefined,
summary: _safeValue(payload.summary || {}),
};
const invoked = await _invokeWithTimeout(handler, request, payload.timeoutMs);
const outcomeName = invoked.timedOut ? 'failed' : _string(invoked.value && (invoked.value.outcome || invoked.value.status), 'handled');
let finalOutcomeName = VALID_OUTCOMES.has(outcomeName) ? outcomeName : 'failed';
let finalReason = invoked.timedOut ? 'Provider operation timed out' : (invoked.value && invoked.value.reason) || '';
let finalDetails = invoked.value && invoked.value.summary || {};
const executor = _executorForRoute(routeKey, provider.providerId, route.executorId || '');
const executorMethod = operation === 'segment.activate' ? 'activateSegment'
: operation === 'stage.set-bypass' ? 'setStageBypass'
: operation === 'stage.set-parameter' ? 'setStageParameter'
: '';
if (finalOutcomeName === 'handled' && executor && executorMethod && typeof executor.operationHandlers[executorMethod] === 'function') {
const _ex = await _invokeWithTimeout((r) => executor.operationHandlers[executorMethod](r), request);
if (_ex.timedOut) {
finalOutcomeName = 'failed';
finalReason = `Executor ${executorMethod} timed out`;
} else {
const executorResult = _plainObject(_ex.value);
finalOutcomeName = VALID_OUTCOMES.has(_string(executorResult.outcome || executorResult.status, 'handled'))
? _string(executorResult.outcome || executorResult.status, 'handled')
: 'failed';
finalReason = executorResult.reason || finalReason;
finalDetails = executorResult.payload || executorResult.summary || finalDetails;
}
}
const outcome = _recordOutcome({ operation, outcome: finalOutcomeName, providerId: provider.providerId, pluginId: provider.pluginId, routeKey, reason: finalReason, details: finalDetails });
if (operation === 'segment.activate' && outcome.outcome === 'handled') route.activeSegmentId = request.segmentId;
_touchRoute(route, outcome);
_emit('audio-effects:changed', { route: _publicRoute(route), operation, outcome: outcome.outcome });
return _decision(outcome.outcome, outcome.reason || 'Provider operation completed', { route: _publicRoute(route), provider: _publicProvider(provider), result: _safeValue(finalDetails || {}) });
}
function setRouteBypass(payload = {}, bypassed) {
const routeKey = _safeRoute(payload.routeKey || payload.route);
const route = routes.get(routeKey);
if (!route) return _noTarget('No active audio-effects route was selected', { routeKey });
if (payload.authorization !== 'user-action' && payload.authorization !== 'restore-selection') {
return _decision('user-action-required', 'Bypass changes require visible user action or restored selection', { route: _publicRoute(route) });
}
route.bypassed = !!bypassed;
route.state = bypassed ? 'bypassed' : (route.executorId ? 'loaded' : (route.activePlanId ? 'resolved' : 'selected'));
const outcome = _recordOutcome({ operation: bypassed ? 'bypass' : 'restore', outcome: 'handled', routeKey, providerId: route.providerId });
_touchRoute(route, outcome);
_emit('audio-effects:changed', { route: _publicRoute(route), operation: bypassed ? 'bypass' : 'restore' });
return _handled({ route: _publicRoute(route) }, bypassed ? 'Audio-effects route bypassed' : 'Audio-effects route restored');
}
async function setRouteGain(payload = {}) {
const routeKey = _safeRoute(payload.routeKey || payload.route);
const route = routes.get(routeKey);
if (!route) return _noTarget('No active audio-effects route was selected', { routeKey });
if (!LOAD_AUTHORIZATIONS.has(_safeId(payload.authorization || '', ''))) {
return _decision('user-action-required', 'Route gain changes require user-action, restore-selection, or playback-session authorization', { route: _publicRoute(route) });
}
const gains = _safeGainMap(payload.gains || (payload.which ? { [payload.which]: payload.value } : {}));
if (!Object.keys(gains).length) return _failed('No valid route gain was provided', { route: _publicRoute(route) });
const provider = _providerForRoute(routeKey, route.providerId);
const executor = _executorForRoute(routeKey, route.providerId, route.executorId || '');
if (!executor || typeof executor.operationHandlers.setRouteGain !== 'function') {
const outcome = _recordOutcome({ operation: 'route.set-gain', outcome: 'no-handler', routeKey, providerId: route.providerId, reason: 'Selected executor does not support route gain' });
_touchRoute(route, outcome);
return _noHandler('Selected audio-effects executor does not support route gain', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
let executorResult;
{
const _exReq = { routeKey, authorization: _safeId(payload.authorization || '', ''), gains, summary: _safeValue(payload.summary || {}) };
const _ex = await _invokeWithTimeout((r) => executor.operationHandlers.setRouteGain(r), _exReq, payload.timeoutMs);
executorResult = _ex.timedOut
? { outcome: 'failed', status: 'timeout', reason: 'Executor route gain timed out' }
: _ex.value;
}
const result = _plainObject(executorResult);
const outcomeName = _string(result.outcome || result.status || 'failed', 'failed');
const normalizedOutcome = VALID_OUTCOMES.has(outcomeName) ? outcomeName : 'failed';
const outcome = _recordOutcome({ operation: 'route.set-gain', outcome: normalizedOutcome, routeKey, providerId: route.providerId, pluginId: provider && provider.pluginId, reason: result.reason || 'Route gain operation completed', details: result.payload || result.summary || {} });
_touchRoute(route, outcome);
_emit('audio-effects:changed', { route: _publicRoute(route), operation: 'route.set-gain', outcome: outcome.outcome });
return _decision(outcome.outcome, outcome.reason || 'Route gain operation completed', { route: _publicRoute(route), provider: _publicProvider(provider), result: _safeValue(result.payload || result.summary || {}) });
}
async function releaseRoute(payload = {}, requester = '') {
const routeKey = _safeRoute(payload.routeKey || payload.route);
const route = routes.get(routeKey);
if (!route) return _noTarget('No active audio-effects route was selected', { routeKey });
const authorization = _safeId(payload.authorization || '', '');
if (!LOAD_AUTHORIZATIONS.has(authorization)) {
return _decision('user-action-required', 'Releasing an audio-effects route requires user-action, restore-selection, or playback-session authorization', { route: _publicRoute(route) });
}
const provider = _providerForRoute(routeKey, route.providerId);
const executor = _executorForRoute(routeKey, route.providerId, route.executorId || '');
if (!executor || typeof executor.operationHandlers.releaseRoute !== 'function') {
const outcome = _recordOutcome({ operation: 'release-route', outcome: 'no-handler', routeKey, providerId: route.providerId, reason: 'Selected executor does not support route release' });
_touchRoute(route, outcome);
return _noHandler('Selected audio-effects executor does not support route release', { route: _publicRoute(route), provider: _publicProvider(provider) });
}
let executorResult;
{
const _exReq = { routeKey, authorization, requesterId: _safeId(requester || 'unknown', 'unknown'), planId: route.activePlanId || '', summary: _safeValue(payload.summary || {}) };
const _ex = await _invokeWithTimeout((r) => executor.operationHandlers.releaseRoute(r), _exReq, payload.timeoutMs);
executorResult = _ex.timedOut
? { outcome: 'failed', status: 'timeout', reason: 'Executor route release timed out' }
: _ex.value;
}
const result = _plainObject(executorResult);
const outcomeName = _string(result.outcome || result.status || 'failed', 'failed');
const normalizedOutcome = VALID_OUTCOMES.has(outcomeName) ? outcomeName : 'failed';
const outcome = _recordOutcome({ operation: 'release-route', outcome: normalizedOutcome, routeKey, providerId: route.providerId, pluginId: provider && provider.pluginId, reason: result.reason || 'Route release operation completed', details: result.payload || result.summary || {} });
if (normalizedOutcome === 'handled') {
routes.delete(routeKey);
_contributeDiagnostics();
}
else _touchRoute(route, outcome);
_emit('audio-effects:changed', { route: _publicRoute(routes.get(routeKey)), operation: 'release-route', outcome: outcome.outcome });
if (normalizedOutcome === 'handled') _emit('audio-effects:released', { routeKey, providerId: route.providerId });
return _decision(outcome.outcome, outcome.reason || 'Route release operation completed', { route: _publicRoute(routes.get(routeKey)), provider: _publicProvider(provider), result: _safeValue(result.payload || result.summary || {}) });
}
function fallback(payload = {}) {
const routeKey = _safeRoute(payload.routeKey || payload.route);
const route = routes.get(routeKey) || {
routeKey,
chainId: _id('effects'),
providerId: _safeId(payload.providerId || '', ''),
state: 'fallback',
bypassed: false,
activePlanId: '',
activeSegmentId: '',
planSummary: {},
dependencies: {},
selectedAt: _now(),
updatedAt: _now(),
lastOutcome: null,
};
route.state = 'fallback';
const outcome = _recordOutcome({ operation: 'fallback', outcome: 'degraded', routeKey, providerId: route.providerId, reason: payload.reason || 'Audio-effects fallback requested' });
_touchRoute(route, outcome);
routes.set(routeKey, route);
_emit('audio-effects:fallback', { route: _publicRoute(route) });
return _decision('degraded', payload.reason || 'Audio-effects fallback recorded', { route: _publicRoute(route) });
}
function inspectRoute(payload = {}) {
const routeKey = _safeRoute(payload.routeKey || payload.route);
return _handled({ route: _publicRoute(routes.get(routeKey)), provider: _publicProvider(_providerForRoute(routeKey, payload.providerId)), conflicts: [] }, 'Inspected audio-effects route');
}
async function listMappings(payload = {}) {
return _mappingFetch(_mappingQuery(payload));
}
async function upsertMapping(payload = {}) {
return _mappingFetch(MAPPING_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload || {}),
});
}
async function deleteMapping(payload = {}) {
const mappingId = _safeId(payload.mappingId || payload.mapping_id || payload.id || '', '');
if (!mappingId) return _failed('mappingId is required');
const params = [];
_queryParam(params, 'provider_id', _aliasValue(payload, 'provider_id', 'providerId'));
const suffix = params.length ? `?${params.join('&')}` : '';
return _mappingFetch(`${MAPPING_ENDPOINT}/${encodeURIComponent(mappingId)}${suffix}`, { method: 'DELETE' });
}
async function activateMapping(payload = {}) {
const mappingId = _safeId(payload.mappingId || payload.mapping_id || payload.id || '', '');
if (!mappingId) return _failed('mappingId is required');
const providerId = _aliasValue(payload, 'provider_id', 'providerId');
return _mappingFetch(`${MAPPING_ENDPOINT}/${encodeURIComponent(mappingId)}/activate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Forward the raw value (null/undefined -> '') so a non-string provider_id is rejected
// by the server rather than silently coerced into an unscoped activate.
body: JSON.stringify({ provider_id: providerId == null ? '' : providerId }),
});
}
async function clearActiveMapping(payload = {}) {
const songKeyRaw = _aliasValue(payload, 'song_key', 'songKey');
const songKey = songKeyRaw == null ? '' : _string(songKeyRaw);
if (!songKey) return _failed('songKey is required');
const params = [`song_key=${encodeURIComponent(songKey)}`];
const toneKeyRaw = _aliasValue(payload, 'tone_key', 'toneKey');
params.push(`tone_key=${encodeURIComponent(toneKeyRaw == null ? '' : _string(toneKeyRaw))}`);
return _mappingFetch(`/api/audio-effects/active-mapping?${params.join('&')}`, { method: 'DELETE' });
}
function recordBridgeHit(input = {}) {
const routeKey = _safeRoute(input.routeKey || input.route);
const bridgeId = _safeId(input.bridgeId || 'audio-effects.legacy-unknown', 'audio-effects.legacy-unknown');
const pluginId = _safeId(input.pluginId || input.source || 'legacy', 'legacy');
const key = `${routeKey}::${bridgeId}::${pluginId}`;
const previous = bridges.get(key);
const bridge = {
bridgeId,
routeKey,
pluginId,
expected: input.expected !== false,
status: _safeId(input.status || input.outcome || 'used', 'used'),
legacySurface: _boundedReason(input.legacySurface || input.sourceSurface || '').slice(0, 120),
hitCount: previous ? previous.hitCount + 1 : 1,
firstHitAt: previous ? previous.firstHitAt : _now(),
lastHitAt: _now(),
reason: _boundedReason(input.reason || ''),
};
bridges.set(key, bridge);
while (bridges.size > MAX_BRIDGES) bridges.delete(bridges.keys().next().value);
if (capabilities && typeof capabilities.registerCompatibilityShim === 'function') {
capabilities.registerCompatibilityShim({
shimId: bridgeId,
capability: 'audio-effects',
source: pluginId,
legacySurface: bridge.legacySurface || bridgeId,
status: 'used',
hit: true,
hitCount: bridge.hitCount,
reason: bridge.reason || 'Audio-effects legacy bridge used',
ownerPluginId: pluginId,
});
} else if (capabilities && typeof capabilities.recordLegacyHit === 'function') {
capabilities.recordLegacyHit({
shimId: bridgeId,
capability: 'audio-effects',
domain: 'audio-effects',
routeKey,
pluginId,
source: pluginId,
status: 'used',
hit: true,
hitCount: bridge.hitCount,
reason: bridge.reason || 'Audio-effects legacy bridge used',
legacySurface: bridge.legacySurface || bridgeId,
});
}
const outcome = _recordOutcome({ operation: 'record-bridge-hit', outcome: 'handled', routeKey, pluginId, details: { bridgeId } });
_emit('audio-effects:bridge-hit', { bridgeId, routeKey, pluginId });
return _handled({ bridge, outcome }, 'Audio-effects bridge hit recorded');
}
function snapshot() {
return {
schema: SCHEMA,
generatedAt: _now(),
routeCount: routes.size,
providerCount: providers.size,
executorCount: executors.size + (_desktopExecutor() ? 1 : 0),
providers: Array.from(providers.values()).slice(0, MAX_PROVIDERS).map(_publicProvider),
executors: Array.from(executors.values()).concat(_desktopExecutor() ? [_desktopExecutor()] : []).slice(0, MAX_EXECUTORS).map(_publicExecutor),
routes: Array.from(routes.values()).slice(0, MAX_ROUTES).map(_publicRoute),
bridges: Array.from(bridges.values()).slice(-MAX_BRIDGES).map(bridge => ({ ...bridge })),
recentOutcomes: recentOutcomes.slice(-MAX_OUTCOMES).map(item => ({ ...item })),
limits: { maxProviders: MAX_PROVIDERS, maxExecutors: MAX_EXECUTORS, maxRoutes: MAX_ROUTES, maxStages: MAX_STAGES, maxSegments: MAX_SEGMENTS, maxSnapshotBytes: 100 * 1024 },
redactionNotes: ['raw-chain-plans-omitted', 'paths-redacted', 'filenames-redacted', 'model-and-ir-names-omitted', 'vst-state-omitted', 'callbacks-omitted', 'handles-omitted'],
};
}
function inspect(payload = {}) {
return _handled({ ...snapshot(), options: _safeValue(payload) }, 'Inspected audio-effects domain');
}
function _payload(ctx = {}) {
const payload = _plainObject(ctx.payload || ctx.args || ctx.target);
return { ...payload, ..._plainObject(ctx.target) };
}
async function _command(commandName, ctx = {}) {
const payload = _payload(ctx);
if (commandName === 'inspect') return inspect(payload);
if (commandName === 'list-providers') return listProviders(payload);
if (commandName === 'list-executors') return listExecutors(payload);
if (commandName === 'register-provider' || commandName === 'register-participant') return registerProvider(payload.provider || payload.participant || payload);
if (commandName === 'register-executor') return registerExecutor(payload.executor || payload);
if (commandName === 'unregister-provider' || commandName === 'unregister-participant') return unregisterProvider(payload);
if (commandName === 'unregister-executor') return unregisterExecutor(payload);
if (commandName === 'select-chain') return selectChain(payload, ctx.requester || ctx.source);
if (commandName === 'resolve-plan') return resolvePlan(payload, ctx.requester || ctx.source);
if (commandName === 'load-plan' || commandName === 'execute-plan') return loadPlan(payload, ctx.requester || ctx.source);
if (commandName === 'release-route' || commandName === 'unload-plan' || commandName === 'clear-route') return releaseRoute(payload, ctx.requester || ctx.source);
if (commandName === 'inspect-route') return inspectRoute(payload);
if (commandName === 'list-mappings') return listMappings(payload);
if (commandName === 'upsert-mapping') return upsertMapping(payload);
if (commandName === 'delete-mapping') return deleteMapping(payload);
if (commandName === 'activate-mapping') return activateMapping(payload);
if (commandName === 'clear-active-mapping') return clearActiveMapping(payload);
if (commandName === 'bypass') return setRouteBypass(payload, true);
if (commandName === 'restore') return setRouteBypass(payload, false);
if (commandName === 'fallback') return fallback(payload);
if (commandName === 'activate-segment') return _providerOperation('segment.activate', payload, ctx.requester || ctx.source);
if (commandName === 'set-stage-bypass') return _providerOperation('stage.set-bypass', payload, ctx.requester || ctx.source);
if (commandName === 'set-stage-parameter') return _providerOperation('stage.set-parameter', payload, ctx.requester || ctx.source);
if (commandName === 'set-route-gain') return setRouteGain(payload);
if (commandName === 'record-bridge-hit') return recordBridgeHit(payload);
return _unsupported(`Unsupported audio-effects command: ${commandName}`);
}
function _registerDomain() {
capabilities.registerOwner('audio-effects', {
pluginId: OWNER_ID,
kind: 'provider-coordinator',
commands: ['inspect', 'list-providers', 'list-executors', 'register-provider', 'register-participant', 'unregister-provider', 'unregister-participant', 'register-executor', 'unregister-executor', 'select-chain', 'resolve-plan', 'load-plan', 'execute-plan', 'release-route', 'unload-plan', 'clear-route', 'inspect-route', 'list-mappings', 'upsert-mapping', 'delete-mapping', 'activate-mapping', 'clear-active-mapping', 'bypass', 'restore', 'fallback', 'activate-segment', 'set-stage-bypass', 'set-stage-parameter', 'set-route-gain', 'record-bridge-hit'],
operations: ['chain.resolve', 'chain.inspect', 'mapping.list', 'mapping.upsert', 'mapping.delete', 'mapping.activate', 'mapping.clear-active', 'executor.load-chain-plan', 'executor.release-route', 'executor.activate-segment', 'executor.set-stage-bypass', 'executor.set-stage-parameter', 'executor.set-route-gain', 'segment.activate', 'stage.set-bypass', 'stage.set-parameter', 'route.bypass', 'route.restore', 'route.release', 'route.set-gain'],
events: ['provider-registered', 'provider-unregistered', 'executor-registered', 'executor-unregistered', 'route-selected', 'plan-resolved', 'changed', 'released', 'fallback', 'bridge-hit'],
safety: 'sensitive',
description: 'Coordinates audio-effects route provider selection, compatible executor selection, constrained chain-plan resolution/loading, route bypass/fallback, and redaction-safe diagnostics.',
handlers: {
inspect: ctx => _command('inspect', ctx),
'list-providers': ctx => _command('list-providers', ctx),
'list-executors': ctx => _command('list-executors', ctx),
'register-provider': ctx => _command('register-provider', ctx),
'register-participant': ctx => _command('register-participant', ctx),
'register-executor': ctx => _command('register-executor', ctx),
'unregister-provider': ctx => _command('unregister-provider', ctx),
'unregister-participant': ctx => _command('unregister-participant', ctx),
'unregister-executor': ctx => _command('unregister-executor', ctx),
'select-chain': ctx => _command('select-chain', ctx),
'resolve-plan': ctx => _command('resolve-plan', ctx),
'load-plan': ctx => _command('load-plan', ctx),
'execute-plan': ctx => _command('execute-plan', ctx),
'release-route': ctx => _command('release-route', ctx),
'unload-plan': ctx => _command('unload-plan', ctx),
'clear-route': ctx => _command('clear-route', ctx),
'inspect-route': ctx => _command('inspect-route', ctx),
'list-mappings': ctx => _command('list-mappings', ctx),
'upsert-mapping': ctx => _command('upsert-mapping', ctx),
'delete-mapping': ctx => _command('delete-mapping', ctx),
'activate-mapping': ctx => _command('activate-mapping', ctx),
'clear-active-mapping': ctx => _command('clear-active-mapping', ctx),
bypass: ctx => _command('bypass', ctx),
restore: ctx => _command('restore', ctx),
fallback: ctx => _command('fallback', ctx),
'activate-segment': ctx => _command('activate-segment', ctx),
'set-stage-bypass': ctx => _command('set-stage-bypass', ctx),
'set-stage-parameter': ctx => _command('set-stage-parameter', ctx),
'set-route-gain': ctx => _command('set-route-gain', ctx),
'record-bridge-hit': ctx => _command('record-bridge-hit', ctx),
},
});
}
function _registerBridgeMetadata() {
if (typeof capabilities.registerCompatibilityShim !== 'function') return;
const shims = [
{ shimId: 'audio-effects.legacy-tone-controls', capability: 'audio-effects', source: OWNER_ID, legacySurface: 'legacy tone/effect controls', reason: 'Legacy tone controls are attributed until providers use native audio-effects route operations.' },
{ shimId: 'audio-effects.legacy-nam-routing', capability: 'audio-effects', source: OWNER_ID, legacySurface: 'NAM tone native-preset route interception', reason: 'Legacy NAM/Rig Builder route interception remains a migration bridge until providers resolve chain plans natively.' },
{ shimId: 'audio-effects.legacy-native-load', capability: 'audio-effects', source: OWNER_ID, legacySurface: 'window.feedBackDesktop.audio.loadPreset', reason: 'Direct Desktop native preset loading is attributed until providers route physical loads through compatible audio-effects executors.' },
{ shimId: 'audio-effects.legacy-tone-db', capability: 'audio-effects', source: OWNER_ID, legacySurface: 'nam_tone.db tone_mappings', reason: 'Legacy NAM tone database mapping access is attributed until providers use the core audio-effects mapping index exclusively.' },
{ shimId: 'audio-effects.legacy-midi-amp', capability: 'audio-effects', source: OWNER_ID, legacySurface: 'MIDI amp/external effect handoff', reason: 'Legacy external effect handoffs are attributed until provider route operations land.' },
];
for (const shim of shims) capabilities.registerCompatibilityShim({ ...shim, status: 'active' });
}
function _contributeDiagnostics() {
const diagnostics = window.feedBack && window.feedBack.diagnostics;
if (diagnostics && typeof diagnostics.contribute === 'function') {
try { diagnostics.contribute('audio-effects', snapshot()); }
catch (_) { /* diagnostics must not break playback */ }
}
}
const api = {
version: 1,
planSchema: PLAN_SCHEMA,
registerProvider,
unregisterProvider,
listProviders,
registerExecutor,
unregisterExecutor,
listExecutors,
selectChain,
resolvePlan,
loadPlan,
releaseRoute,
inspectRoute,
listMappings,
upsertMapping,
deleteMapping,
activateMapping,
clearActiveMapping,
bypass: payload => setRouteBypass(payload, true),
restore: payload => setRouteBypass(payload, false),
fallback,
activateSegment: payload => _providerOperation('segment.activate', payload),
setStageBypass: payload => _providerOperation('stage.set-bypass', payload),
setStageParameter: payload => _providerOperation('stage.set-parameter', payload),
setRouteGain,
recordBridgeHit,
recordOutcome: entry => _clone(_recordOutcome(entry)),
snapshot,
getDiagnostics: snapshot,
};
window.feedBack.audioEffects = api;
_registerDomain();
_registerBridgeMetadata();
_contributeDiagnostics();
})();