refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
# PLAN — Reproductor de sonido (`AudioPlayer`)
> **Tipo**: plan de creación por fases (process — efímero, NO fuente de verdad).
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Fecha**: 2026-07-30 · **Estado**: **▶️ DESBLOQUEADO 2026-07-30** —
> [`PLAN-sound-engine.md`](./PLAN-sound-engine.md) cerró (F5), así que `$sound`
> **ya existe** y con él la invariante de un solo `AudioContext`. Decisiones
> **D-AP.1…D-AP.12 SIN FIRMAR**: el gate de §4 es lo primero de F0 y **no se
> escribe una línea de código antes**.
> **Kickoff para sesión nueva**: *"Lee `docs/process/PLAN-audio-player.md` y
> presenta el gate de §4."*
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
>
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> ### ✅ Correcciones incorporadas (sesión 2026-07-30)
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
>
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> Las 5 correcciones que colgaban de esta cabecera **ya están en el cuerpo**:
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
>
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> | # | Corrección | Dónde aterrizó |
> | --- | --- | --- |
> | 1 | Faltaba la reproducción de **segmentos** entera — y **Vidstack la tiene** (`clipStartTime`/`clipEndTime`), luego es **paridad pendiente, no superación** | fila nueva en la matriz §2 · **D-AP.12** · contrato §7.5 · §10 |
> | 2 | **G-2** (pista secundaria del `Slider`) sube a **BLOQUEANTE** de v1: tres consumidores (buffer · ventana de clip · capítulos) | §8 · F1 |
> | 3 | **D-AP.11** se reescribe contra el art: su premisa («no hay motor») era falsa | **D-AP.11** en §4 |
> | 4 | Dos invariantes al contrato: `prefs.sound` **nunca** toca el volumen del contenido · el silencio de UI debe alcanzar al `Slider` compuesto | §7.6 |
> | 5 | **D-AP.7** deja de ser regla de cascada y pasa a **ducking**, capacidad del motor | **D-AP.7** reescrita |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
>
> **Antes de escribir una línea de código**: presentar al usuario las decisiones
> de §4 y obtener firma. Lo de aquí son **propuestas razonadas, no decisiones
> tomadas** — regla A32 (`component-guide.md`) + §0.5 de
> [`component-audit.md`](../guides/component-audit.md) ("Get the user's sign-off
> on scope before writing any code").
>
> **Precondición cumplida**: corpus doctrinal leído completo antes de auditar
> (regla dura `feedback_read_full_doctrine_before_auditing`): README · overview ·
> active-architecture · CANON · morfo · sema · soma(+architecture) · eidos ·
> active-uix · active-app · packs · blocks · agent · TSC · recipe-contract ·
> vocabularies · component-guide · completion-checklist · component-audit ·
> demo-authoring · theming(reference/guide/notes/motion/motion-guide/channels/
> gradient-finish) · los 7 RFCs · book-deviations · design-text-effects ·
> design-chat-block · glossary · comparison · authoring · testing-and-tooling ·
> next-features · decisions · book-map · guía histórica · veredictos/_system/
> _naming/_cierre de la auditoría · `eidos/components/README.md` · `arts/README.md`.
---
## 0. Contexto — qué existe hoy y qué NO
El ecosistema **ya tiene un reproductor** : `media-player` , genérico
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`<video>` + `<audio>` , cerrado como PASS en la matriz de aceptación. Y desde el
2026-07-30 tiene además **motor de audio propio** — el art
[`$sound` ](../../src/arts/sound/README.md ), extraído de `sema/chans/sound.ts` ,
con la invariante de un solo `AudioContext` cerrada y medida. Eso responde una
pregunta que este plan traía mal planteada (D-AP.11) y **no** añade trabajo al
v1: el player decide *cuándo* engancharse, no *dónde vive* el motor.
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Capa | Estado | Evidencia |
| --- | --- | --- |
| morfo | 534 líneas · 17 partes · 12 eventos · `expression: 'pack'` · `apg: 'none — …'` | [`morfo/components/media-player.ts` ](../../src/uix/morfo/components/media-player.ts ) |
| soma | provider request-driven + **puerto `MediaProvider`** + adaptador nativo + 14 wrappers + test 9/9 | [`soma/components/media-player/` ](../../src/uix/soma/components/media-player/ ) |
| sema | pack «voz de transporte contenida» (3 reglas) | [`sema/components/media-player.ts` ](../../src/uix/sema/components/media-player.ts ) |
| eidos | recipe *Lumière* (11.7 KB) + 17 wrappers + `DefaultControls` | [`eidos/components/media-player/` ](../../src/uix/eidos/components/media-player/ ) |
| demo | 9 pestañas, grupo **Media** del sidebar | `web/routes/uix/components/media-player/+page.svelte` |
**El modo audio existe, pero es un reproductor de vídeo degenerado.** Todo el
soporte «audio» del catálogo son **tres reglas CSS** :
```
media-player.css:79 [data-media='audio'] [data-media-player-media] → height: 0
media-player.css:161 [data-media='audio'] [data-media-player-title] → position: static
media-player.css:191 [data-media='audio'] [data-media-player-controls] → position: static
```
Es decir: se colapsa el vídeo a cero y se desabsolutizan dos cajas. **No hay
nada de lo que hace que un reproductor de sonido sea un reproductor de sonido**:
identidad de la pieza (carátula · título · artista), waveform, velocidad de
reproducción visible, capítulos, formas compactas, integración con el SO.
### 0.1 Hallazgos de la auditoría del estado actual (evidencia leída)
| ID | Hallazgo | Evidencia | Consecuencia |
| --- | --- | --- | --- |
| **H-1** | **Regla sema probablemente muerta.** El pack construye su selector sobre `provider` (`semaSelector(morfo,'provider',{eventName:'commit-toggle-play'})` → `[data-media-player][data-event='commit-toggle-play']` ) pero el evento estampa en **su propio botón** (`target: v.partRef('play-button')`). | `sema/components/media-player.ts:23` vs `morfo/…/media-player.ts:95` | La firma de play/pause nunca se aplicaría. **Verificar en F0 con stamp real en navegador** ; si se confirma, es fix de una línea (`onPlayButton`). |
| **H-2** | `SettingsButton` y `Captions` **declarados e inertes** ("render inertly until implemented", README soma:99-100). | `soma/…/README.md:96` | El menú velocidad/pista es *justo* lo que más pesa en audio (podcast). Deuda declarada que este plan recoge. |
| **H-3** | ** `Slider` no tiene pista secundaria.** Partes: `Provider · Range · Thumb · Tick` . El buffer del player es un `div` eidos-only que lee `--media-buffered` . | `morfo/components/slider.ts` | Un waveform-seek necesita el mismo hueco. Gap de framework (§8). |
| **H-4** | ** `Slider` no declara `aria-valuetext` .** Solo `aria-valuemin/max/now` . | `morfo/components/slider.ts:107-109` | El lector de pantalla anuncia *«127»* , no *«2:07 de 41:15»* . Fallo de nivel de referencia en el control MÁS importante del player. |
| **H-5** | **Formato de tiempo hecho a mano.** `formatTime()` con `padStart` local, fuera de `uix.format` / `$libs/days` . | `soma/…/media-player-provider.svelte.ts:34-40` | Contradice el eje de servicios (i18n · dígitos no arábigos · RTL). |
| **H-6** | **No hay `--media-progress`.** Se eliminó por código muerto; solo sobrevive `--media-buffered` . | README eidos:47 | Un waveform necesita el progreso como var continua otra vez (o su propio mecanismo). Decisión, no accidente. |
| **H-7** | **Cero integración MediaSession** en todo el repo. | `grep -rn mediaSession src/` = 0 | Sin controles de pantalla de bloqueo / teclas de medios del SO. |
| **H-8** | La carátula (`Poster`) es `position:absolute; inset:0; object-fit:cover` sobre una caja `16/9` . En audio esa caja **no existe** (media a `height:0` ). | `media-player.css:85-93` | La carátula de un álbum no tiene hogar geométrico hoy. |
Ninguno de los ocho invalida el `media-player` : son exactamente la superficie
que un modo audio de nivel de referencia obliga a cerrar.
---
## 1. Qué es —de verdad— un reproductor de sonido
No es «un player sin imagen». Cambian tres cosas de raíz:
1. **La superficie deja de ser el contenido.** En vídeo el marco *es* la obra y
el chrome se esconde encima (scrim, auto-hide). En audio **no hay marco** : la
UI es lo único visible, así que pasa de *transparente* a *persistente* , y su
trabajo cambia de «no estorbar» a «identificar la pieza».
2. **La identidad sustituye al fotograma.** Carátula, título, artista/serie,
capítulo. Ninguna existe hoy más allá de un `Title` de una línea.
3. **La línea de tiempo se vuelve legible.** En vídeo el scrubber se previsualiza
con miniaturas; en audio la referencia del sector es la **onda** (SoundCloud,
wavesurfer) o los **capítulos** (podcast). La barra desnuda es el mínimo, no
el estándar.
Y aparecen tres afordancias que en vídeo son secundarias y aquí son de primera
fila: **velocidad** (podcast/curso), ** ±15/30 s** (ya existe como `SeekButton` )
y **continuidad con el SO** (pantalla de bloqueo, auriculares, coche).
---
## 2. Comparativa contra referencias (F-1.2 exige ≥3; aquí 8 + la plataforma)
Consultado 2026-07-30. La columna «UIX hoy» = `media-player` en `media="audio"` .
| Capacidad | Vidstack (`DefaultAudioLayout`) | Media Chrome (`< media-controller audio > `) | Plyr | Red Hat `rh-audio-player` | wavesurfer.js 7 | react-h5-audio-player | UIX hoy | Veredicto v1 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| play/pause · ±seek · mute · volumen · tiempo | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| **Layout de audio propio** (no vídeo colapsado) | ✅ `DefaultAudioLayout` | ✅ atributo `audio` | ✅ preset audio | ✅ `layout` | n/a | ✅ | ❌ 3 reglas CSS | **implementar** |
| **Formas / tamaños** (full · compact · mini · barra) | ⚠️ `smallLayoutWhen` (2 tamaños) | ⚠️ composición manual | ❌ | ✅ full/compact/mini | n/a | ⚠️ | ❌ | **implementar** |
| **Identidad** : carátula · título · artista | ⚠️ `Title` +`Poster` | ⚠️ slots | ⚠️ | ✅ series/title/poster | ❌ | ✅ header | ⚠️ solo `Title` | **implementar** |
| **Velocidad de reproducción** (UI) | ✅ menú + `SpeedSlider` | ✅ `media-playback-rate-button` | ✅ settings.speed | ✅ | n/a | ❌ | ❌ (`setPlaybackRate` existe, sin UI) | **implementar** (cierra H-2) |
| **Waveform** | ❌ | ❌ | ❌ | ❌ | ✅ (el núcleo) | ❌ | ❌ | **componente propio** (D-AP.4) |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **Segmentos** (reproducir `start…end` de una fuente) | ✅ `clipStartTime` / `clipEndTime` (recalcula el rango *seekable* ) | ? | ? | ? | ⚠️ Regions (plugin) | ? | ❌ | **implementar v1** (D-AP.12) |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **Capítulos** | ✅ `SliderChapters` + `ChapterTitle` + radio-group | ⚠️ vía cues | ✅ `markers` | ✅ transcript/cues | ✅ Regions | ❌ | ❌ | **diferir v2** |
| **Transcripción** | ⚠️ captions | ⚠️ | ❌ | ✅ `rh-transcript` +`rh-cue` | ❌ | ❌ | ❌ | **diferir v2** |
| **Cola / playlist** | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠️ skip prev/next | ❌ | **fuera** (D-AP.6) |
| **MediaSession (SO)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **superación** (D-AP.5) |
| **Ganancia >100 %** | ✅ `AudioGainSlider` | ❌ | ❌ | ❌ | ⚠️ Envelope | ❌ | ❌ | **diferir v2** |
| **Loop / descarga** | ⚠️ | ⚠️ | ✅ | ⚠️ | n/a | ✅ loop | ⚠️ nativo | **loop v1 (prop), descarga = app** |
| **Teclado completo** | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ (9 teclas) | ✅ |
| ** `aria-valuetext` hablado en el scrubber** | ✅ | ✅ | ✅ | ✅ | ❌ | ⚠️ | ❌ (H-4) | **gap de framework** (§8) |
| Motor enchufable (HLS/DASH/embed) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ puerto `MediaProvider` | ✅ **ya por encima** |
| Semántica perceptiva (familia · intent · sonido/háptica) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ pack sema | ✅ **exclusivo** |
| Contrato declarativo verificable (morfo + guards) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ **exclusivo** |
**Lectura.** El transporte ya está a la altura (y el puerto de motor, por
encima). Lo que falta es **todo lo que distingue audio de vídeo** : layout
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
propio, formas, identidad, velocidad, onda, **segmentos** . Dos capacidades que
ninguna referencia tiene ya son nuestras (sema + morfo), y una que **ninguna** de
las ocho implementa —MediaSession— es la palanca barata de superación.
Las celdas `?` de la fila **Segmentos** están **sin verificar** : sólo se
comprobó Vidstack (2026-07-30). Cerrarlas es tarea de F0 — y no cambian el
veredicto, porque con una referencia que lo trae ya es paridad pendiente.
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Fuentes**: [Vidstack components ](https://vidstack.io/player/components/ ) ·
[Vidstack Default Layout ](https://vidstack.io/docs/player/components/layouts/default-layout/ ) ·
[Media Chrome ](https://github.com/muxinc/media-chrome ) ·
[Plyr ](https://github.com/sampotts/plyr ) ·
[Red Hat audio player ](https://ux.redhat.com/elements/audio-player/ ) ·
[wavesurfer.js ](https://github.com/katspaugh/wavesurfer.js/ ) ·
[react-h5-audio-player ](https://github.com/lhz516/react-h5-audio-player ) ·
[MDN MediaSession ](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession ) ·
[web.dev Media Session ](https://web.dev/articles/media-session ) ·
[W3C WAI media players ](https://www.w3.org/WAI/media/av/player/ ).
---
## 3. La tensión arquitectónica (leerla antes de las decisiones)
Dos reglas del propio proyecto **apuntan en direcciones opuestas** :
- **P-4 (norma de pickers, `architecture/eidos.md` )**: *«There are no
per-variant components (`MonthPicker`, `HourPicker` ). Those forms are
`<X kind='month'>` »*. → un reproductor de audio es `<MediaPlayer media="audio">` .
- **Regla de admisión (packs/blocks)**: *lo que tiene superficie de contrato
propia se promueve a componente canónico y se compone.* → la onda, el menú de
velocidad y la identidad podrían querer casa propia.
La resolución no la dan las reglas sino **las dos referencias primarias, que
coinciden**: Vidstack mantiene UN `<media-player>` y cambia el **layout**
(`DefaultAudioLayout`); Media Chrome mantiene UN `<media-controller>` y cambia
la **composición** de la barra (atributo `audio` ). Ninguna bifurca el motor.
Y el catálogo ya tiene el precedente exacto para «una segunda raíz visual sobre
el mismo contrato headless»: **Toast / Toaster** — *«two independent roots (not
nested)… `Toaster` is NOT attached as `Toast.Toaster` because it is a competing
root, not a child»* (`architecture/eidos.md` §Special case). `DefaultControls`
ya es, hoy, una composición eidos-only del mismo tipo.
---
## 4. Decisiones (gate del usuario — SIN FIRMAR)
| ID | Cuestión | Opciones | Recomendación |
| --- | --- | --- | --- |
| **D-AP.1** | **Pertenencia** : ¿componente nuevo o modo del existente? | (a) **un motor, un contrato** : `media-player` conserva morfo/soma/sema y gana partes audio-only `optional: true` + un layout eidos · (b) `audio-player` nuevo que *compone* `media-player` (precedente picker/A27) · (c) fork con su propio provider | ** (a)**. Duplicar transporte + puerto + teclado + eventos es exactamente la reinvención que el §4 del component-guide prohíbe; las dos referencias primarias hacen (a); el morfo YA declara `data-media: ['video','audio']` y **ya** marca `optional: true` todas las partes no-núcleo (`PipButton`/`FullscreenButton` son video-only con ese mismo mecanismo — no hace falta inventar nada). (b) añade una capa de indirección sin contrato nuevo. |
| **D-AP.2** | **Puerta pública** : ¿cómo se pide un reproductor de sonido? | (a) `<MediaPlayer media="audio">` + `<MediaPlayer.AudioLayout/>` (espejo de `DefaultControls` ) · (b) **raíz competidora** `<AudioPlayer>` en eidos sobre el mismo soma (precedente Toast/Toaster) · (c) ambas | ** (c)**: `AudioLayout` como parte eidos-only (composición explícita, coherente con `DefaultControls` ) **y** `<AudioPlayer>` como raíz competidora exportada aparte —no como `MediaPlayer.AudioPlayer` — que la monta con `media="audio"` . El 90 % del uso es una etiqueta; el 10 % compone partes. Coste morfo/soma/sema: **cero** . |
| **D-AP.3** | **Formas y variaciones** (petición explícita del usuario) | (a) solo `size` canónico · (b) `variant` propio de componente · (c) `variant` + `size` + responsive | ** (c)**. `variant: 'card' \| 'row' \| 'bar' \| 'inline'` como **variante específica de componente** —vía legítima y con precedente escrito (`Banner: inline\|overlay\|persistent`, `Spinner: bars\|dots\|ring` , theming §19)— declarada en su `types.ts` , NO en `EIDOS_VARIANTS` . `size` = subconjunto canónico `xs..xl` (la recipe ya lo mapea). Ambas admiten `ResponsiveProp<T>` . Detalle en §6. |
| **D-AP.4** | **Waveform** | (a) componente canónico `Waveform` propio (ruta de 9 fases) · (b) parte eidos-only del player · (c) fuera | ** (a), pero en su propia iniciativa, DESPUÉS del v1**. Tiene superficie de contrato real (es un `role="slider"` con teclado, `data-*` y eventos `handle-*` ), y **sirve fuera del player** (nota de voz en `chat-message` , editor). Meterlo como parte eidos-only sería crecerle el privilegio al player — el antipatrón que la regla de admisión existe para impedir. Los picos: ** `peaks: number[]` que aporta la app** (ruta profesional: BBC `audiowaveform` /SoundCloud) con decodificador cliente **opcional** ; el renderer reutiliza `$libs/plots` (`scaleLinear` + `area` /`line`), no inventa matemáticas. |
| **D-AP.5** | **MediaSession (SO)** | (a) prop opt-in en el provider soma · (b) art nuevo `$media-session` · (c) app-land | ** (a) opt-in, apagado por defecto**, `mediaSession={{ title, artist, album, artwork }}` . Es *exactamente* el snapshot que el provider ya posee (`playbackState`, `setPositionState` , `seekto` ), y dejarlo fuera obliga a cada consumidor a re-derivarlo. **Riesgo declarado** : `navigator.mediaSession` es **singleton de documento** — dos players activos se pisan; el provider debe registrar solo mientras `!paused` y liberar en `pause` /`dispose`. (b) es sobreingeniería para una API sin estado propio. Ninguna de las 8 referencias lo trae → superación barata. |
| **D-AP.6** | **Cola / playlist** | (a) en el player · (b) block/app-land | ** (b), fuera de alcance**. Una cola es una composición de `Listbox` + estado de aplicación: cero contrato nuevo → tier `blocks` o app. El player expone `commit-complete` y ya. Se registra en `next-features.md` . |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.7** | **Firma sema en modo audio** — *reescrita 2026-07-30 (corrección 5)* | (a) heredar el pack actual · (b) silenciar el canal sonoro en audio · (c) **silencio mientras suena + ducking cuando no basta** | ** (c)**. El suelo sigue siendo (b) y su doctrina no cambia: D.7 (`book-deviations`) *«En eventos frecuentes, la prioridad es evitar fatiga. El silencio es una firma válida»* + antipatrón cap. 34 §10 «sonido decorativo» — si el contenido ES audio, un *tick* de UI compite con la obra. Regla del pack: selector `[data-media-player][data-media='audio']` → `channels: ['haptic']` , que **añade carácter sin tocar las primitivas del intent** . Lo que cambia es que el silencio ya **no tiene que ser total ni de cascada** : `$sound` es dueño del earcon, así que **atenuar la UI mientras suena la obra no necesita que el player consuma el grafo** — basta con que el provider baje el master (`uix.sound.setMasterGain`) mientras `!paused` y lo restaure al pausar. **Regla de reparto** : la cascada decide *cuándo callar* , el motor decide *cuánto* . Lo que **sí** exige el grafo (D-AP.11(b), diferido) es lo contrario: agachar **el contenido** bajo un earcon, o hacerlo con precisión de muestra. **A firmar con la decisión** : el master es global, así que hay que decidir **quién lo restaura** y qué pasa con dos players a la vez — si no se resuelve, el v1 se queda en (b) puro, que ya es correcto. |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.8** | **Capítulos + transcripción** | (a) v1 · (b) v2 · (c) fuera | ** (b) v2**, registrado con disposición en el README (F-1.4). Capítulos = pista secundaria del scrubber → **depende de D-AP.4/H-3** ; transcripción = superficie de contenido con su propia a11y (candidata a componente propio, patrón Red Hat). |
| **D-AP.9** | **Responsive del layout** | (a) `ResponsiveProp` (mecanismo del framework) · (b) container queries del TSC · (c) ambos | ** (a) en v1, (b) evaluado en F6**. El TSC ya tiene el eje `container: {}` documentado como ** «a themeable axis with 0 consumers today — an open cage»** (`canon/tsc.md`): un player que vive dentro de un aside, una tarjeta o un pie de página es el **primer consumidor natural** del catálogo, y resolvería mejor que el `smallLayoutWhen` de Vidstack (que depende del contenedor, no del viewport). Abrir la jaula es un entregable con valor propio — pero no debe bloquear el v1. |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-AP.10** | **Alcance del v1** | (a) paridad estricta con las referencias · (b) paridad + superación (MediaSession + silencio sema + container) | ** (a) + MediaSession + silencio sema + segmentos**; container y waveform como iniciativas hermanas. Criterio: el usuario pidió *«como mínimo al mismo nivel»* — la paridad es el suelo, no el techo, y las dos superaciones baratas caben. **Los segmentos entran por paridad** (D-AP.12), no por ambición. |
| **D-AP.11** | ** ¿El player consume `$sound` ?** — *escrita 2026-07-30 contra el art ya existente; su premisa anterior («no hay motor, ¿dónde lo ponemos?») era falsa* | (a) **no consumirlo en v1** : el player sigue sobre `<audio>` nativo y `$sound` sólo suena la UI · (b) **consumirlo desde v1** : colgar el `MediaElementAudioSourceNode` de `uix.sound.context` · (c) consumirlo sólo cuando el consumidor lo pida (prop opt-in) | ** (a) en v1, (b) como iniciativa hermana con `Waveform` .** El art existe y la invariante de contexto único está cerrada y medida ([`PLAN-sound-engine.md` §15](./PLAN-sound-engine.md)), así que la pregunta ya **no** es *dónde vive el motor* sino *cuándo engancharse* . Enganchar `<audio>` a Web Audio no es gratis: `createMediaElementSource` **redirige el audio del elemento al grafo de forma irreversible** , y con una fuente cross-origin sin CORS lo deja MUDO. Un v1 cuyo trabajo es *layout, identidad, formas, velocidad y segmentos* no necesita el grafo; quien lo necesita es la onda, el visualizador, la ganancia >1 y **el ducking del contenido** — todos diferidos. **Consecuencia declarada** : con (a), el ducking de D-AP.7 sólo puede atenuar la **UI** (`setMasterGain`), nunca la obra; y el punto de bucle del clip (D-AP.12) sigue atado a `timeupdate` . Lo que **sí** entra en v1 es la regla escrita: si algún día se engancha, es a `uix.sound.context` , jamás a un `new AudioContext()` propio. |
| **D-AP.12** | **Segmentos** (`clip`) — *fila nueva 2026-07-30 (corrección 1); es * *paridad**, Vidstack los trae* | (a) fuera de v1 · (b) ** `clip = { start, end, loop? }` en el provider** · (c) `regions` genéricas (n segmentos con estado propio) | ** (b)**, con este diseño ya acordado: capa de proyección **absoluto↔relativo** (dominio del slider `0…end− start` , `timeBase: 'clip' \| 'source'` en `Time` , **misma base** en `aria-valuetext` ) · **clamp en el provider, no en el puerto** (el puerto `MediaProvider` no se contamina: sigue hablando de la fuente entera) · `commit-complete` dispara en el fin del **clip** — mismo evento, frontera movida · **con `loop` NO hay `commit-complete`** : un bucle esperado no es una ocurrencia (regla B.5). **Limitación declarada** : `timeupdate` llega ~4 veces/s, así que sin Web Audio el punto de bucle es impreciso (~±125 ms) — se documenta, no se disimula. **Riesgo a probar en F0** : bug abierto de clip en Chrome/Safari móvil (vidstack#1195). (c) queda **descartado** y `chapters` **v2** , los dos por depender de G-2. |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 5. Arquitectura propuesta (tras firma de D-AP.1/2)
```
morfo/components/media-player.ts +6 partes optional (audio-only) · +2 eventos · +1 texto
│ (contrato ÚNICO — no se bifurca)
soma/components/media-player/ +sub-providers de las partes nuevas
│ +MediaSession opt-in en el provider raíz
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
│ +clip {start,end,loop} + proyección (D-AP.12)
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
│ +formatTime → uix.format (H-5)
sema/components/media-player.ts +regla de silencio en audio (D-AP.7) · fix H-1
│
eidos/components/media-player/ +audio-layout.svelte (composición, espejo de DefaultControls)
│ +recipe: variantes card/row/bar/inline
eidos/components/audio-player/ raíz competidora < AudioPlayer > (Toast/Toaster)
```
**Lo que NO se toca**: el puerto `MediaProvider` (ya es el acierto del
componente), el modo vídeo, el recipe Lumière existente, la demo de
`media-player` .
**Dogfooding obligatorio** (§4 component-guide): la barra compone `IconButton`
(ya lo hace), los sliders componen `Slider` (ya), el menú de velocidad compone
`DropdownMenu` , la carátula compone `Image` + `AspectRatio` , la identidad
compone `Text` /`Heading`, el estado vacío compone `EmptyState` . **Cero
primitivas inline.**
---
## 6. Formas y variaciones (el punto 3 del encargo, en detalle)
Cuatro formas, un solo contrato. La variante decide **qué partes monta el
layout y cómo se disponen**, nunca qué puede hacer el player.
| `variant` | Forma | Partes que monta | Contexto de uso | Referencia del sector |
| --- | --- | --- | --- | --- |
| ** `card` ** | Tarjeta vertical: carátula grande arriba, identidad, scrubber, transporte centrado | Artwork · Title · Artist · TimeSlider · Time× 2 · Play · Seek± · Mute · Volume · Rate | Página de episodio, ficha de álbum, landing | Apple Podcasts / Bandcamp |
| ** `row` ** | Fila horizontal: carátula pequeña a la izquierda, identidad + scrubber apilados | Artwork(sm) · Title · Artist · TimeSlider · Play · Seek± · Rate | Lista de episodios, resultado de búsqueda, feed | SoundCloud track |
| ** `bar` ** | Barra persistente a ancho completo (composición pensada para `sticky` /pie) | Artwork(xs) · Title · Play · Seek± · TimeSlider · Time · Mute · Volume · Rate | Reproductor global de la app | Spotify bottom bar |
| ** `inline` ** | Mínima, en línea con el texto: play + scrubber + tiempo | Play · TimeSlider · Time | Nota de voz en un chat, cita de audio en un artículo | mensaje de voz de WhatsApp |
Ejes ortogonales que **componen** con la variante:
- **`size`** (`xs..xl`, `ResponsiveProp` ) — densidad; la recipe ya mapea el
bundle `--size-{k}-*` . Regla dura: `md` no cambia de significado por viewport
(theming §5); lo que cambia es **qué size se elige** .
- **`color`** — el sistema completo (rol / intent / 33 escalas / CSS crudo =
`ComponentColorProp` ), sin subconjuntos: decisión «abrir la jaula del color»
(theming §25, guard en `recipe-css-contract.test.ts` ).
- **`variant` responsive** — `variant={{ base: 'inline', md: 'row', lg: 'card' }}` .
- **`data-depth`** — la barra persistente estampa su plano (`raised`/`overlay`),
no inventa sombras (contrato de recipe R-4.1).
**Regla de forma** (espejo de la que rige `blocks` ): la variante **no puede
añadir comportamiento**. Si una forma necesitase un evento, un `data-*` que el
CSS deba seleccionar o una obligación a11y nueva, eso se promueve al contrato
—no se le crece el privilegio al layout.
---
## 7. Contrato propuesto (borrador para F1 — se cierra con la firma)
### 7.1 Partes nuevas (todas `optional: true`, audio-only)
| Parte | kebab | archetype | Elemento | Rol |
| --- | --- | --- | --- | --- |
| `Artwork` | `artwork` | `image` | `img` | Carátula. Compone `Image` +`AspectRatio`; `alt` real (no decorativa como `Poster` , porque en audio **es** la única representación visual de la obra). |
| `Artist` | `artist` | `description` | `div` | Autoría / serie. |
| `RateButton` | `rate-button` | `trigger` | `button` | Abre el menú de velocidad (compone `DropdownMenu` ) o cicla el preset. Cierra H-2 junto a `SettingsButton` . |
| `Identity` | `identity` | `group` | `div` | Agrupador de Artwork+Title+Artist (le da a la variante un bloque que mover). |
| `Transport` | `transport` | `group` | `div` | Agrupador de los botones de transporte, hermano de `Controls` . |
| `LiveIndicator` | `live-indicator` | `indicator` | `div` | Emisión en directo (`data-live` ya existe en el provider). |
### 7.2 Eventos nuevos
| Evento | Familia · verbo | Intent | Target | `sequence` | Por qué |
| --- | --- | --- | --- | --- | --- |
| `commit-set-rate` | `commit` · `set` | `neutral` | `rate-button` | `post` | Fijar velocidad **es** un valor aplicado (`commit.set`, BOOK_CANON A.5: *«un valor, criterio o parámetro ha quedado aplicado»* — mismo caso que sort/slider). |
| `sustain-end` | `sustain` · `end` | — | `provider` | `coincident` | Cierre del proceso de buffering. **Solo si** F0 confirma que `sustain-loading` (`persistence: 'stateBound'`) no se limpia solo; si se limpia, **no se declara** (regla B.5: un cambio no perceptible no es evento). |
Nada más. `loop` es prop nativa sin evento (B.5). El cambio de carátula/título es
dato, no ocurrencia.
### 7.3 Teclado
Se hereda el contrato del provider (9 teclas). Se añaden, alineadas con el
sector (`j`/`l` de YouTube, `<` /`>` de velocidad):
| Tecla | Acción | Nota |
| --- | --- | --- |
| `j` / `l` | seek ∓ `seekStep` | alias de ←/→, convención del sector |
| `<` / `>` | bajar / subir velocidad | requiere `RateButton` montado |
| `0` – `9` | saltar al 0– 90 % | **solo cuando `duration` es finita** (no en directo) |
### 7.4 A11y — el listón
- **`aria-valuetext` hablado en el scrubber** (cierra H-4): *«2 minutos 7
segundos de 41 minutos»*, no «127». Es **gap de framework** (§8).
- `Artwork` con `alt` real; el resto de la identidad como texto real (nunca
`aria-label` sobre `div` s vacíos).
- `commit-fail` ya declara `requiresLiveRegion` + `reducedMotionFallback: 'text'`
— se conserva.
- Contraste: la barra `bar` sobre superficie arbitraria consume `data-depth` , no
color inventado (R-2.1/R-4.6).
- Sin robo de foco al cambiar de pista.
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.5 Segmentos (`clip`) — el contrato (D-AP.12)
| Pieza | Regla |
| --- | --- |
| Prop | `clip={{ start, end, loop? }}` en el **provider** soma. Ausente = fuente entera (comportamiento de hoy, sin cambios) |
| Puerto | ** `MediaProvider` no se toca**: sigue hablando de la fuente entera. El clamp y la proyección viven en el provider |
| Dominio del slider | `0…end− start` . Un scrubber que empieza en 0 es la única lectura honesta para quien no sabe que hay una fuente mayor detrás |
| `Time` | `timeBase: 'clip' \| 'source'` — y `aria-valuetext` usa **la misma base** que el `Time` visible. Dos bases distintas en la misma pantalla es un fallo de a11y, no una opción |
| Fin | `commit-complete` dispara al llegar a `end` — el mismo evento, la frontera movida |
| `loop` | **NO** dispara `commit-complete` : un bucle esperado no es una ocurrencia (B.5) |
| Pista secundaria | La ventana del clip se pinta en la pista secundaria de `Slider` — **G-2** , uno de sus tres consumidores |
| Limitación | `timeupdate` llega ~4 veces/s: sin Web Audio el punto de bucle es impreciso (~±125 ms). Se declara en el README, no se disimula |
### 7.6 Dos invariantes que este componente es el primero en necesitar
1. ** `prefs.sound` NUNCA toca el volumen del contenido.** Gobierna el sonido de
**UI** (sema: earcons, `off` / `reduce` ). Un reproductor de sonido es el
primer componente del catálogo donde «sonido» significa dos cosas, y
confundirlas silenciaría la obra al bajar una preferencia de interfaz. El
volumen del contenido es `volume` / `muted` del provider, y nada más.
2. **El silencio de UI en audio (D-AP.7) tiene que alcanzar al `Slider`
compuesto.** Existe `resolveSliderDragSound` : arrastrar el scrubber
sonorizaría **por encima** de la obra. **Verificar con stamp real en
navegador** (F0) y, si dispara, la regla es
`[data-media-player][data-media='audio'] [data-slider]` .
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 8. Gaps de framework a promover ANTES (regla de admisión)
Se construyen **primero** , en el canon, y el player los compone. Ninguno es un
parche local del player.
| # | Gap | Dónde | Justificación 2-de-3 | Coste |
| --- | --- | --- | --- | --- |
| **G-1** | `aria-valuetext` en el morfo de `Slider` (`v.propRef('valueText')`) + prop `valueText` en soma | `morfo/components/slider.ts` + provider | `aria` es campo morfo por definición (soma emite, eidos puede seleccionar) | bajo |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **G-2** 🔴 **BLOQUEANTE de v1** | Pista secundaria («buffered»/«secondary range») en `Slider` | `morfo` + `soma` + recipe | Lo consumen soma (valor) y eidos (pintura) → 2-de-3 ✓. Y ya tiene **tres** consumidores, no uno: el **buffer** , la **ventana del clip** (D-AP.12) y los **capítulos** (v2). Hoy lo simula un `div` eidos-only en media-player y lo simularía otro en `Waveform` — dos copias = la señal de que falta la pieza; tres = que bloquea | medio |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **G-3** | `formatDuration` en `$libs/days` (o `uix.format` ) | `$libs/days` | Regla A23: si es puro y reutilizable, va a la lib — nunca reimplementar dentro de soma | bajo |
| **G-4** | Verificar/arreglar H-1 (selector del pack sema) | `sema/components/media-player.ts` | fix de una línea si se confirma | trivial |
`Waveform` (D-AP.4) **no** está en esta tabla: no es un gap del player sino una
iniciativa hermana con su propia ruta de 9 fases.
---
## 9. Fases (tras el gate)
| Fase | Contenido | Verify |
| --- | --- | --- |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F0 — Verificación y firma** | Confirmar H-1 (stamp real en navegador) · **confirmar si el `Slider` compuesto sonoriza al arrastrar** (7.6.2) · confirmar si `sustain-loading` se auto-limpia (decide 7.2) · **cerrar las celdas `?` de la fila Segmentos** (§2) · **reproducir vidstack#1195** en Chrome/Safari móvil (decide si el clamp del clip necesita defensa propia) · leer `DefaultControls` , `Slider` provider y la recipe del scrubber ANTES de diseñar (regla `feedback_study_real_recipes_before_building` ) · presentar §4 y §6 al usuario y **firmar D-AP.1…D-AP.12** | tabla de decisiones firmada en el chat; este plan actualizado |
| **F1 — Gaps de framework** | G-1 · **G-2 (bloqueante)** · G-3 · G-4, cada uno con su test | `check` 0 propios · vitest del slider verde · `component:audit --only slider` PASS · `morfo:vocabulary` exit 0 |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F2 — Morfo** | 6 partes + 1– 2 eventos + `texts` + catálogo `langs/components/media-player.ts` | `validateMorfo` · test del morfo · `morfo:vocabulary` · `translations:check` |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F3 — Soma** | sub-providers de las partes nuevas · MediaSession opt-in (con guarda de singleton) · ** `clip` + la proyección absoluto↔relativo + el clamp (§7.5)** · `formatTime` →G-3 · tests del provider | test del provider verde (incl. clip: dominio, `commit-complete` en `end` , silencio del evento con `loop` ) · `check` 0 propios |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F4 — Sema** | regla de silencio en audio (D-AP.7) + fix H-1; `expression` sigue `'pack'` | `morfo:vocabulary` (coherencia pack↔expression, guard S11d) · stamp verificado en navegador |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F5 — Eidos** | `audio-layout.svelte` + raíz `<AudioPlayer>` + recipe de las 4 variantes + **la ventana del clip pintada en la pista secundaria de G-2** + tokens `--audio-player-*` en `lib/recipes/base.ts` | `generate:eidos-css` (TSC) · `recipe-css-contract` · `eidos-lint` 0 inválidos · `component-api-contract` |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F6 — Formas** | responsive de `variant` /`size`; evaluar container queries (D-AP.9) | prueba visual de las 4 variantes × light/dark × RTL × densidad, **con captura y mirada** (regla dura `feedback_verify_visually_before_showing` ) |
| **F7 — Demo + dossier + cierre** | demo v2 de 9 pestañas (cada prop = control vivo, paridad de chips D-7.4) · README eidos (Baseline/Comparativa≥3/Decisiones/Gaps con disposición) · README soma actualizado · `data-perm-step` | `component:audit --only media-player` **PASS 0 errores** · `smoke` · `perm:check` · `check` 0 propios · `docs:check` |
**Método**: no-cascada — una pieza, verificar, la siguiente (método canonizado
en [`_cierre.md` ](../audit/components/_cierre.md )). Gates completos al cerrar
cada fase.
---
## 10. Fuera de alcance
- **Cola / playlist** (D-AP.6) → `blocks` o app; se registra en `next-features.md` .
- **Waveform** (D-AP.4) → iniciativa hermana, ruta de 9 fases propia.
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`regions`** (n segmentos con estado propio) → **descartado** , no diferido: es
el modelo de wavesurfer y trae su propio contrato (selección, arrastre,
solapamiento). `clip` (D-AP.12) resuelve el caso real —reproducir un trozo— sin
abrirlo.
- **Enganchar el player a `uix.sound.context` ** (D-AP.11) → iniciativa hermana,
con `Waveform` y el visualizador. El v1 no toca Web Audio.
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Capítulos, transcripción, ganancia >100 %, ecualizador, grabación** → v2,
cada uno con disposición escrita en el `## Gaps` del README.
- **Vendorizar motores** (hls.js, Howler, wavesurfer) → el puerto `MediaProvider`
ya es la respuesta; doctrina de cero dependencias.
- **Visualizador/espectro** → si algún día se hace, su hogar es `$scene`
(`EngineScene` ya resuelve frame-loop, pausa fuera de vista, cap de DPR,
reduced-motion obligatoria, pérdida de contexto y presupuesto) o el tier
`packs` — **nunca** un canvas suelto dentro del recipe del player.
---
## 11. Riesgos declarados
| Riesgo | Mitigación |
| --- | --- |
| El morfo de `media-player` ya es el mayor del catálogo; crece más | Todas las partes nuevas `optional: true` ; si tras F2 el fichero pasa de ~700 líneas, reabrir D-AP.1(b) con datos, no con intuición |
| MediaSession es singleton de documento | Registro solo mientras suena + liberación en `pause` /`dispose`; documentado en el README como limitación conocida |
| La variante `bar` vive sobre fondos arbitrarios | `data-depth` + el layer de estado; nunca color crudo |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO.
Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana
una auditoría propia que lo contradice donde toca.
- **§15 — la invariante, cerrada y medida.** Incluye lo que la medición
DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema`
que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo
había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo.
- **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente:
`analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`);
el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está
cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira
en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`,
que es de UNA reproducción, se escribe en el master compartido.
- **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**.
La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el
mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema,
viviendo dentro de lo que el plan llama «maquinaria». El precedente de
`$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca
como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la
justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y
`preload(urls)` los esconde en una caché privada.
- **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA
y falso para el grafo de módulos.
- **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al
cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y
en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip,
capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen
del contenido; el silencio de UI debe alcanzar al `Slider` compuesto).
**D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y
**D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin
fila en §4. **D-AP.7 reescrita** como ducking.
- El handoff añade dos reglas que costaron caro: **un guard que no has visto
fallar no vale**, y acoplarse al dev server vivo del usuario en vez de
levantar otro.
D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una
línea de código del player antes.
`docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **El clip depende de `timeupdate`** (~4/s) | El bucle es impreciso (~±125 ms) y **se declara** en el README; la precisión de muestra necesitaría Web Audio, que el v1 no engancha (D-AP.11) |
| **Bug abierto de clip en Chrome/Safari móvil** (vidstack#1195) | Reproducirlo en F0 antes de firmar D-AP.12; si se confirma, el clamp del provider necesita defensa propia y se escribe como limitación conocida |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria
Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por
gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas
medidos: el import era estático, así que toda app con sema metía el
sintetizador en el bundle aunque el sonido estuviera apagado (que es el
default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de
inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio.
Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la
capa conserva sus DATOS y su doctrina.
- `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los
limitan y el gesto de desbloqueo es por contexto), síntesis de earcon,
samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con
la pestaña oculta es correcto para earcons y erróneo para contenido, así que
es decisión de quien compone— y aviso cuando un segundo contexto va vivo.
Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni
nada de `$uix/sema`, que es la prueba objetiva del corte.
- `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la
política de reducción y el reparto «el canal resuelve el NIVEL, el motor
aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto.
- `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped:
`ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable
`contracts.ts`, y `defineEngineSound()` para el camino de app.
- Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de
deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre
`emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites
previas no cubrían porque paraban en canales falsos.
Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí
todo fallo es degradación documentada, no error de programador.
Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73
errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era
el criterio de que el movimiento fue value-preserving.
Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión
adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del
reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md`
(handoff).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Sobrecarga de alcance | El v1 es la columna «implementar» de §2 y nada más; todo lo demás lleva disposición escrita |