You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-sound-redesign.md

19 KiB

PLAN — Rediseño del sonido: $sound como ORQUESTADOR

Tipo: plan de diseño + ejecución por fases (process — efímero, NO fuente de verdad). Fecha: 2026-07-31 · Estado: GATE FIRMADO (D-SR.1…D-SR.12) · Entrega 1 en curso. La firma: el usuario aprobó el plan completo el 2026-07-31, con D-SR.4 y D-SR.7 respondidas explícitamente (transporte provisto por el servicio · dos entregas). Kickoff para sesión nueva: "Lee docs/process/PLAN-sound-redesign.md y continúa por la fase abierta — el gate YA está firmado, no se re-presenta."


1. El mandato (2026-07-31)

El usuario rechazó de raíz el gate del reproductor (D-AP.1…13, PLAN-audio-player.md, suspendido): el sistema de sonido es un frankenstein — nació de los diseños de otros módulos (sema primero, media-player después) y se parchea en función de lo que ya había. Nunca partió de su verdadera función: orquestador y gestor de TODOS los aspectos del sonido en el framework, al nivel de motion / timers / scene. Los módulos que usan sonido (players, semántica, estudio) usan el servicio y se adaptan a su filosofía, no al revés.

Método mandado: clean-room — el diseño parte de la función, sin que los consumidores actuales lo muevan; ellos se adaptan después.

Qué se conserva y qué cae. Los HECHOS medidos no se repudian: la invariante de UN AudioContext por documento (medida en Chrome en los dos modos de arranque), decode sin segundo contexto, los hallazgos del player (H-1, H-9, H-10/G-5, los gaps G-1/G-2/G-3 del Slider). Lo que cae es doctrina: PLAN-sound-engine.md §16.1 («la voz es del art, calibrada para sema; partir en dos cuando llegue un segundo consumidor») queda REVOCADA — la calibración es DATO del consumidor y el art es el orquestador desde ya; y el planteamiento del player («el player decide cuándo engancharse») se invierte: el player consume el transporte que el servicio provee.

2. La función — los 7 dominios (clean-room)

  1. Sustrato — el AudioContext único: creación, unlock por gesto, suspend/resume, SSR no-op, costuras de test, disposal. (Existe y está verificado — se conserva.)
  2. Mezcla — master → buses ui (earcons/semántica) y content (la obra). Palancas por bus: gain, mute, retenciones de duck (refcount, gana la más fuerte). Invariante estructural: prefs.sound gobierna SOLO ui; el volumen de la obra es de la fuente + política del bus content.
  3. Voces — la síntesis es MAQUINARIA del servicio; la calibración es DATO del consumidor (espejo de motion: los consumidores registran presets, el motor los ejecuta). registerVoice(name, spec); sema registra la suya. Las 4 constantes perceptivas (quinta 1.5×/0.3 · clamps 15 %/40 % · ±400 cents · mapeo AM) dejan de ser del art. Samples/preload/caché y decode se conservan como puertas.
  4. Ciudadanía de media — las fuentes de contenido pasan por el servicio: el transporte (play/pause/seek/volume/rate/snapshot/subscribe — la forma exacta del puerto MediaProvider del player) lo PROVEE el servicio (D-SR.4), que así sabe qué suena, aplica foco, proyecta MediaSession y gobierna el ducking.
  5. Política / arbitraje — foco entre fuentes (mixed / exclusive / duck, modelo AVAudioSession/AudioFocus) y la relación UI↔contenido: «la UI calla mientras suena la obra» (incongruencia, D.7) pasa de parche de cascada a relación entre buses (opt-in). Sema conserva su doctrina (firmas, tunings, cascada, migración de significado con off).
  6. Percepción — decode (existe); analyser por bus/fuente y picos para Waveform = v2 con disposición escrita.
  7. SO — MediaSession es singleton del documento → vive en el orquestador (proyecta la fuente ACTIVA); los players aportan metadata.

Fuera, con disposición: spatial · cadenas DSP · grabación/captura · MIDI. El háptico NO entra (otro canal, D.8 — asimetría ya documentada).

3. Referencias del sector

Modelos (conocimiento estable; el cross-check API-fino se hace al abrir la Entrega 2, que es donde esas APIs muerden):

Modelo Aporta
iOS AVAudioSession UNA autoridad por app: categorías, mixWithOthers/duckOthers, interrupciones
Android AudioFocus las fuentes PIDEN foco (GAIN/TRANSIENT/MAY_DUCK); el árbitro aplica pérdidas
FMOD / Wwise jerarquía de buses (master → music/sfx/voice), ducking entre buses
Howler.js el singleton web: master volume/mute, autoUnlock, un contexto
Tone.js contexto compartido + Destination
Plataforma web cap de contextos, unlock por gesto, MediaSession API, trampa createMediaElementSource (irreversible; CORS → mudo)

Ninguna referencia web une mezcla por buses + foco + MediaSession + voces semánticas bajo una autoridad. Esa unión ES «gestor de todos los aspectos del sonido» — superación, no paridad.

4. La filosofía, en espejo de los arts hermanos

  • Como timers: nadie hace new AudioContext() ni Web Audio crudo fuera del art; todo pasa por uix.sound.
  • Como motion: el motor ejecuta, los consumidores REGISTRAN sus datos (presets ⇒ voces). Sema deja de estar horneada dentro del art.
  • Como scene: la ciudadanía (unlock, visibilidad, presupuesto, degradación, teardown) se hace UNA vez y se hereda.
  • Contrato de arts: Engine* sin runas · puertos SoundDom/SoundTimers · diagnostics con catálogo tipado (se adopta ahora — la decisión «espejo de scene, sin diagnostics» se revierte, declarado en D-SR.9).

5. La API (v2)

const sound = createEngineSound({ dom, timers, logger, audioContextFactory, fetcher, masterGain });

// Sustrato (se conserva)
sound.prime(); sound.suspend(); sound.resume(); sound.dispose();
sound.state; sound.context;

// Mezcla (nuevo)
sound.master.setGain(v);                    // política de documento (antes setMasterGain)
sound.bus('ui').setGain(v);
sound.bus('ui').setMuted(true);
const hold = sound.bus('ui').duck(0.2);     // refcount; gana la más fuerte; hold.release()

// Voces (nuevo)
sound.registerVoice('sema', voiceSpec);     // calibración = dato del consumidor
await sound.play(sig, { bus: 'ui', voice: 'sema', gainScale });

// Muestras / percepción (se conserva)
await sound.preload(urls); await sound.decode(bytes);

// Ciudadanía de media (Entrega 2)
const media = sound.media(el, { metadata, focus: 'may-duck' });
// → implementa el puerto MediaProvider; foco mixed/exclusive/duck;
//   duckUiWhileContent opt-in; MediaSession de la fuente activa; el grafo NO se
//   engancha por defecto (trampa CORS declarada); visibilidad content-aware
//   (jamás suspender el contexto con obra sonando — E-3, ahora estructural).

Sema tras la adaptación: resuelve firma + nivel (doctrina intacta) y llama play(sig, { bus: 'ui', voice: 'sema', gainScale }). Nada más.

6. Decisiones — FIRMADAS 2026-07-31

ID Decisión firmada
D-SR.1 $sound = EL orquestador (los 7 dominios). §16.1 revocada. Nombre/alias se quedan
D-SR.2 Buses master → ui + content (set fijo v1); gain/mute/duck por bus; prefs.sound ↔ ui SOLO
D-SR.3 Voces registradas; default del servicio = calibración actual (cero cambio audible); sema registra la suya encima
D-SR.4 El servicio PROVEE el transporte (el adaptador nativo se muda al servicio; el player consume el handle). El elemento DOM sigue siendo del componente; grafo no enganchado por defecto
D-SR.5 Foco default mixed; exclusive/duck config; duckUiWhileContent opt-in
D-SR.6 MediaSession del servicio (fuente activa, opt-in con metadata)
D-SR.7 Dos entregas: E1 = núcleo (F0–F2) con escucha A/B antes de seguir; E2 = media + MediaSession (F3–F4); F5 repartido
D-SR.8 Sin ActiveSound v1; handles con subscribe; disposición escrita
D-SR.9 Diagnostics contrato completo de arts (revierte «espejo de scene»)
D-SR.10 Player suspendido; re-plan posterior sobre el servicio (hereda hechos D-AP)
D-SR.11 prime/play/preload/decode/suspend/resume/state/context se conservan; setMasterGain → master.setGain con barrido en el mismo pase (sin shim); play gana {bus, voice} con defaults que preservan el comportamiento
D-SR.12 sounds.ts de sema intacto: SOUND_LIBRARY = recursos, SOUND_TUNINGS = canon (D.7)

7. El terreno — destino de cada pieza

Pieza Destino
Gobierno de contexto + guard de 2.º contexto + decode/preload/caché/fallback Se conserva (verificado)
gainScale en la envolvente (A-5) Se conserva — es la separación política/reproducción que los buses generalizan
Las 4 constantes de la voz Se mudan a SoundVoice (default = valores actuales; sema registra su voz)
setMasterGain → master.setGain (barrido: art + tests + fake del port; ningún consumidor de producción fuera)
autoSuspend Entrega 2: content-aware (no suspender con obra sonando)
SoundChannel Adelgaza: resolve → play({bus:'ui', voice:'sema', gainScale}); registra la voz de sema al construirse (idempotente); unión A/B intacta
Asserts que cambian sound.test.ts (harness de 1 nodo → nodos rastreados; createGain 1×→3×) y sound-port.test.ts (fake del engine + args de play) — cambio de contrato declarado, lo contrario del criterio value-preserving de la extracción, a propósito
sound-e2e.test.ts Sin cambios: pinea osciladores/contextos, no el árbol de gains
contracts.ts fila sound Sin tocar en E1: no pinea métodos y sus promesas siguen verdaderas; se revisará en E2 (el fichero además está contaminado por otra sesión)
Estudio /temas/sema Sin cambios en E1 (no usa setMasterGain); se revisita en E2
/temas/orquesta Legal (forma B); deuda de esa demo

8. Fases

Fase Contenido Verify
F0 — Documento ✅ Este plan · CONTINUE-sound-engine.md reescrito (pivote) · cabecera de PLAN-audio-player.md (suspensión, ya hecha) docs:check limpio en lo propio
F1 — Motor v2 (E1) ✅ HECHA 2026-07-31 Buses (master→ui/content, gain/mute/duck refcount, política pre-contexto) + voces (registerVoice, default = calibración histórica, aviso una vez por voz desconocida) + play({bus,voice}) (defaults ui/default) + master.setGain (barrido de setMasterGain: art+tests+fake; ningún consumidor de producción fuera) + diagnostics.ts (catálogo tipado, D-SR.9); invariantes de contexto INTACTAS ✅ suite del art 19/19 (14 originales — 2 asserts de contrato declarados: createGain 1×→3× y harness multi-nodo — + 5 nuevos de buses/voces) · ✅ check 73 = baseline exacta, 0 propios
F2 — Sema se adapta (E1) ✅ HECHA 2026-07-31 SEMA_SOUND_VOICE(+NAME) en sounds.ts (tipo importado de $sound, dirección permitida) · el canal registra la voz al construirse (idempotente; en AMBAS formas A/B) y toca play({bus:'ui', voice:'sema', gainScale}) · sound-port.test.ts (fake al contrato nuevo + pin de registro) · sound.test.ts (harness multi-nodo + createGain 3×, declarado) · e2e y active-uix SIN tocar · docs de la entrega: README del art reescrito (identidad orquestador) + fila/grafo de arts/README.md + architecture/sema.md §SoundChannel ✅ 18 suites / 237 tests verdes (sema completa + art + active-uix; e2e de contexto único intacto) · ✅ check 73/0 propios · ✅ escucha A/B del usuario: «suena igual» (2026-07-31) — ENTREGA 1 CERRADA
F3 — Ciudadanía de media (E2) ✅ HECHA 2026-07-31 src/arts/sound/media.ts: sound.media(el, {focus?, metadata?}) → handle con la forma EXACTA del puerto MediaProvider (adaptador nativo mudado: flag waiting, bufferedAhead, clamps; kind por tagName, no instanceof — cross-realm-safe) · foco mixed/exclusive/duck (servicio + override por fuente; duck sobre el.volume con snapshot().volume = volumen del DUEÑO) · duckUiWhileContent (hold refcount sobre el bus ui) · visibilidad content-aware (fuente sonando veta el suspend) · attach() opt-in al bus content (irreversible, CORS declarado) · manager perezoso (sin media no se paga) · cross-check MediaSession contra MDN hecho al abrir ✅ 10 tests nuevos con elementos fake (transporte · exclusive · duck+restore · override · duckUi pre-contexto · visibilidad · attach al contexto COMPARTIDO · sesión · sin-metadata no toca · dispose) · ⚠️ la medición en Chrome del escenario mixto queda para el re-plan del player: ninguna página consume media() aún — no hay nada real que medir
F4 — MediaSession (E2) ✅ HECHA 2026-07-31 (dentro de media.ts) Proyección de la fuente ACTIVA (opt-in por metadata): MediaMetadata + playbackState + play/pause/seekto (un try por acción — un nombre no soportado no tumba al resto) + setPositionState en timeupdate/duration/rate · liberación en destroy/dispose (metadata null, handlers null) · promoción a otra fuente sonante con metadata al morir la activa · feature-detected, todo absorbido con diagnóstico ✅ tests con navigator.mediaSession fake (proyección, tecla pause → handle, liberación, opt-in) · ⚠️ prueba manual de teclas de medios: necesita una página consumidora — llega con el re-plan del player
F5 — Barrido + docs ✅ HECHA 2026-07-31 (con 2 exclusiones declaradas) defineEngineSound(options) acepta la rodaja de política (EngineSoundPolicyOptions) · README del art (media citizenship) · arts/README.md · docblock del motor · contracts.ts NO tocado (no pinea métodos, sus promesas siguen verdaderas, y el fichero está contaminado por otra sesión) · estudio sin cambios (no usa la superficie nueva) ✅ 27 suites / 301 tests verdes (sema+sound+active-uix+active-app) · ✅ check 73 = baseline, 0 propios (verificado con el patrón \\\\ correcto — el filtro anterior no casaba, la trampa documentada)
Después (iniciativa aparte) Re-plan del reproductor sobre el servicio su propio gate

Método no-cascada. Sin commits salvo petición expresa (árbol compartido).

9. Auditoría post-entrega (2026-07-31) — AU-1…AU-9, TODAS RESUELTAS

Auditoría clean-room de las dos entregas (pedida por el usuario; orden de cierre firmado con «ok»). 9 hallazgos, ninguno invalidó el rediseño; resoluciones:

AU Hallazgo Resolución
AU-1 El foco duck era starter-céntrico: una fuente que arrancaba DESPUÉS del ducker no quedaba atenuada; divergía del modelo de referencia (duckOthers persistente) sin documentar ✅ Semántica reescrita y FIRMADA: estado derivado del conjunto sonante (refreshDucking): mientras suene un ducker, toda otra fuente sonante queda atenuada — llegadas tardías incluidas; entre duckers simultáneos, el más reciente retiene la palabra. exclusive sigue siendo acción del starter (sin re-reclamo, documentado). Test nacido en ROJO contra el código viejo y verde tras el fix
AU-2 media()/attach() sin guard post-dispose: attach podía abrir un AudioContext HUÉRFANO en un engine disposed; preload() (preexistente v1) igual de desguardado ✅ Guard disposed en ensureContext() (cierra TODAS las vías) + en preload() · dispose() limpia el manager FUERA del guard de idempotencia (un segundo dispose libera un registro post-dispose) · comportamiento post-dispose documentado en types.ts · test: attach post-dispose → null, factory jamás llamada
AU-3 Doble registro del mismo elemento: listeners duplicados + auto-duck ✅ byElement: re-registrar RETIRA la inscripción obsoleta (un remonte nunca apila); el elemento destruido sale de la mezcla al volumen del DUEÑO. Test
AU-4 La invariante «prefs.sound ↔ bus ui» enunciada como mecanismo, sin productor (la reducción sigue siendo gainScale del canal) ✅ Texto corregido (README del art + sema.md), patrón A-2: lo garantizado por construcción es el NEGATIVO (nada de UI toca content); el bus ui es la palanca que una app PUEDE cablear. El cableado directo queda como patrón de app, no del framework
AU-5 Duplicación estructural handle↔puerto MediaProvider sin guard de deriva hasta el re-plan ✅ soma/components/media-player/media-provider-drift.test.ts (lado soma — la dirección de import permitida): Exact en snapshot+eventos, asignabilidad unidireccional del handle. Falla check si derivan
AU-6 Verifies prometidos sin ejecutar: docs:check (F0) y lint ✅ Medidos: docs:check 0 errores/555 docs · prettier fallaba en 5 ficheros míos → formateados, --check limpio
AU-7 Verify de F3 reescrito al marcarlo (cross-check solo MediaSession) · promesa «media tree-shakeable + medir» incumplida ✅ Declarado aquí: AVAudioSession/AudioFocus quedan como modelos de diseño (no hay código contra ellas) · media es alcanzable estáticamente A PROPÓSITO (es la puerta del servicio) · bundle MEDIDO: art completo 16,2 KB min · 5,6 KB gzip (era 2,2 gz; la ciudadanía entera cuesta ~3,4 KB gz). La fila de riesgo de §10 queda enmendada por esta
AU-8 Guards nuevos sin ver fallar (regla dura del hilo) ✅ Tres verificados: persistencia del duck (test nacido rojo) · veto de visibilidad (mutación → rojo → revertida) · duckUiWhileContent (mutación → rojo → revertida)
AU-9 Declaraciones a medias: standalone sin acceso a política; acciones MediaSession omitidas sin disposición ✅ Disposiciones escritas en el README: standalone se cablea cuando exista el primer consumidor (no crecer superficie sin uso); stop/seekforward/seekbackward/tracks llegan con el re-plan del player si su UX las pide

Notas menores registradas sin acción (deliberado): fallbacks defensivos inalcanzables en play()/attach() · carrera teórica de volumechange async (irrelevante en fakes, ventana microscópica en navegador) · attach()+ autoSuspend puede silenciar el elemento adjunto con pestaña oculta (combinación opt-in+opt-in, se documentará si aparece el caso) · preload() puede pender en resume() fuera de gesto (preexistente v1, hoy además guardado por disposed) · exclusive no re-arranca a los desplazados (modelo AudioFocus, documentado).

Gate tras la auditoría: 28 suites / 305 tests verdes (incl. drift guard) · check 73 = baseline, 0 propios (verificado con patrón \\\\; los 4 de uix\soma que caza el filtro ampliado son de file-upload, ajenos y dentro de la baseline) · docs:check 0 · prettier limpio en lo propio.

10. Riesgos

Riesgo Acotación
Regresión perceptiva (los tests no oyen) Voz default = calibración actual → cero cambio audible en E1; escucha A/B del usuario = verify de F2
Churn de tests Solo asserts cuyo contrato cambió, declarados fichero a fichero (§7)
Trampa createMediaElementSource El grafo no se engancha por defecto; attach opt-in con limitación declarada
Bundle Núcleo 2,2 KB gz hoy; media en módulo tree-shakeable; medir en F5
Sobre-alcance 7 dominios diseñados, implementación escalonada; percepción avanzada v2 con disposición

Powered by TurnKey Linux.