From c691a88c1a36625157998150c99d3134ceca06ac Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 18 Aug 2026 20:15:04 +0200 Subject: [PATCH] uix(background): un fondo nunca suena, y no estaba resuelto sino evitado MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D-BG.19, planteada por el autor: qué pasa con un `Background.Video` cuyo clip trae pista de audio, y cómo se relaciona con el interruptor de sonido del framework o con un «no sound» de la app. La respuesta que había en el código era que no había respuesta. `muted` era una prop con default `true` y la razón escrita al lado no era doctrinal sino mecánica —un clip sin mutear tiene el autoplay rechazado en todos los navegadores—, de modo que el silencio era un efecto secundario de la política del navegador y no una decisión de nadie. Lo que eso significaba en cuanto alguien escribiera `muted={false}`: el clip sonaba FUERA del grafo de audio. `sound.buses.content.setMuted(true)` no lo silenciaba; no participaba del audio focus, así que hablaba por encima de un `MediaPlayer` en vez de cederle el paso; no atenuaba el bus `ui` mientras sonaba; no se proyectaba a MediaSession; y el slot `sound` de `$prefs` —el interruptor «sin sonido» de la app, el que se proyecta al DOM como `data-sound`— no lo alcanzaba nunca. Un `AudioContext` por documento es la razón de ser de ese art, y el componente no tenía ni una referencia a él. De WCAG 1.4.2 se salvaba de rebote: el control de pausa que el stack ya debía lo para, por carambola. Así que `muted` deja de ser prop y se escribe `true` incondicionalmente: la API ya no puede expresar un fondo audible. Tres razones, de más a menos vinculante. La doctrina propia del componente —toda capa es `aria-hidden` y la decoración no es contenido, así que un clip de aquí no puede portar significado que el audio entregue; una capa audible sería contenido vestido de fondo—. WCAG 1.4.2: una decoración no puede pedir un consentimiento que nadie le dio. Y la fuga de `arts/sound`, que es la parte que hace de esto un asunto de framework y no de gusto. Un clip que DEBE oírse es contenido: `MediaPlayer`, que ya es ciudadano de `sound.media()` y obtiene focus, ducking y MediaSession gratis, o montarlo por `Background.Layer` y registrarlo uno mismo. La alternativa —hacerlo ciudadano con `sound.media(el, { focus: 'duck' })` cuando el consumidor desmutea— queda rechazada por lo mismo que la hacía atractiva: abre la puerta a que un fondo compita con el contenido real, y ese territorio es de `Ambient` o de un reproductor. Verificado en Chrome con la capa en `video`: `el.muted === true`, `paused: false`, `currentTime` avanzando, `readyState: 4` —sigue reproduciéndose— y el control de pausa presente. Queda anotado en el código un detalle que invita a un arreglo equivocado: `hasAttribute('muted')` lee false mientras la propiedad es true, porque Svelte fija `muted` como PROPIEDAD sin reflejarlo al atributo. No es un agujero: la propiedad se escribe en el mismo efecto y dos líneas antes del `play()`, y no hay nada que pueda colarse entre las dos. Ningún consumidor pasaba `muted` —el `HeroSite` ya lo decía en prosa, «DECORATIVE — muted»—, así que el cambio de API no rompe a nadie. Gates: audit PASS 0/0 · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio · prettier limpio. Co-Authored-By: Claude Opus 5 --- docs/process/CONTINUE-background.md | 6 +++ docs/process/PLAN-background.md | 43 +++++++++++++++++++ src/uix/eidos/components/background/README.md | 24 +++++++++++ .../background/background-video.svelte | 39 ++++++++++++++--- src/uix/eidos/components/background/types.ts | 2 - 5 files changed, 106 insertions(+), 8 deletions(-) diff --git a/docs/process/CONTINUE-background.md b/docs/process/CONTINUE-background.md index eba3d8062..45d86fb2b 100644 --- a/docs/process/CONTINUE-background.md +++ b/docs/process/CONTINUE-background.md @@ -78,6 +78,12 @@ por la aritmética de ahora), el puntero entero, `attach='fixed'`, y RTL. - **Paseo A–H registrado**: añadidas al README la excepción `A2.3` y la sección `## Subset`; corregida una deriva de TRES sitios que llamaba `Toggle` al control de pausa (compone `IconButton`, evento `contact-activate`). +- **D-BG.19 · un fondo NUNCA suena** (§7.undecies, firmada por el autor): `muted` + deja de ser prop y se escribe `true` siempre, así que la API no puede expresar un + fondo audible. No estaba resuelto, estaba evitado: con `muted={false}` el clip + sonaba FUERA de `arts/sound` — ni bus `content`, ni audio focus, ni duck del bus + `ui`, ni MediaSession, ni el slot `sound` de `$prefs`. Un clip que debe oírse es + contenido: `MediaPlayer`, que ya es ciudadano de `sound.media()`. ### 2. Lo que queda de tus firmas: DOS diff --git a/docs/process/PLAN-background.md b/docs/process/PLAN-background.md index d53627a5b..205649b1d 100644 --- a/docs/process/PLAN-background.md +++ b/docs/process/PLAN-background.md @@ -987,6 +987,49 @@ un screenshot (era el `write(0,0)` de un `pointerleave`), dar por muerto el la precondición que necesitan (scrim encendido, capa que se mueva). **El instrumento miente antes que el código**, y aquí mintió tres veces. +### 7.undecies D-BG.19 — un fondo NUNCA suena (2026-08-18) + +**Planteada por el autor**: qué pasa con un `Background.Video` cuyo clip trae +pista de audio, y cómo se relaciona con el interruptor de sonido del framework o +con un «no sound» de la app. La respuesta, medida en el código, era que **no +estaba resuelto: estaba evitado**. `muted` era una prop con default `true`, y la +razón escrita no era doctrinal sino mecánica («un clip sin mutear tiene el +autoplay rechazado en todos los navegadores»). Nada conectaba el vídeo con +`arts/sound`: cero referencias en todo el componente. + +**Lo que eso significaba con `muted={false}`**: el clip sonaba FUERA del grafo. +`sound.buses.content.setMuted(true)` no lo silenciaba; no participaba del audio +focus, así que hablaba por encima de un `MediaPlayer` en vez de cederle el paso; +no atenuaba el bus `ui`; no se proyectaba a MediaSession; y el slot `sound` de +`` —el interruptor «sin sonido» de la app, proyectado como `data-sound`— +no lo alcanzaba. Un `AudioContext` por documento es la razón de ser de ese art, y +un fondo cantando por fuera es exactamente la fuga que existe para impedir. +De WCAG 1.4.2 se salvaba de rebote: el control de pausa lo para, por carambola. + +**FIRMADA (a): el fondo nunca suena.** `muted` deja de ser prop y se escribe +`true` incondicionalmente, así que **la API no puede expresar un fondo audible**. +Tres razones, de más a menos vinculante: la doctrina propia del componente (toda +capa es `aria-hidden`, la decoración no es contenido, así que un clip de aquí no +puede portar significado que el audio entregue); WCAG 1.4.2 (una decoración no +puede pedir un consentimiento que nadie le dio); y la fuga de `arts/sound` de +arriba, que es la parte de framework. Un clip que DEBE oírse es contenido: +`MediaPlayer`, que ya es ciudadano de `sound.media()`, o montarlo por +`Background.Layer` y registrarlo uno mismo. + +(b) rechazada —hacerlo ciudadano con `sound.media(el, { focus: 'duck' })` cuando +`muted === false`— por más potente y por eso mismo: abre la puerta a que un fondo +compita con el contenido real, y ese territorio es de `Ambient` o de un reproductor. + +**Verificado en Chrome**: con la capa en `video`, `el.muted === true`, +`paused: false`, `currentTime` avanzando y `readyState: 4` — sigue reproduciéndose +—, y el control de pausa presente. ⚠️ Anotado en el código: `hasAttribute('muted')` +lee **false** mientras la propiedad es true, porque Svelte lo fija como PROPIEDAD +sin reflejarlo; no es un agujero, porque la propiedad se escribe en el mismo +efecto y dos líneas antes del `play()`. Ningún consumidor pasaba `muted` (el +`HeroSite` ya lo decía en prosa: «DECORATIVE — muted»), así que el cambio de API +no rompe a nadie. Gates: audit PASS 0/0 · vitest eidos 434/435 (el de siempre) · +`check` sin errores propios · prettier limpio. + --- ## 8. Riesgos y cómo se acotan diff --git a/src/uix/eidos/components/background/README.md b/src/uix/eidos/components/background/README.md index 9d673da09..fdb8b0988 100644 --- a/src/uix/eidos/components/background/README.md +++ b/src/uix/eidos/components/background/README.md @@ -139,6 +139,30 @@ The element is driven by `play()` / `pause()` rather than the `autoplay` attribute, because four of the five are runtime state that flips both ways and the attribute is a one-shot at parse time. +**And a sixth rule that is a law, not a policy: a background never sounds.** +`muted` is not a prop — it is written `true` unconditionally, so the API cannot +express an audible background. Three reasons, in order of how much they bind: + +- **The component's own doctrine.** Every layer is `aria-hidden` and decoration is + never content, so a clip here cannot be carrying meaning that audio delivers. An + audible layer would be content wearing a background's clothes. +- **WCAG 1.4.2.** Audio that starts on its own and lasts more than three seconds + owes the reader a way to stop it. The pause control happens to provide one, but + a decoration should not be asking for consent it was never given in the first + place. +- **It would leak past `arts/sound`, and that is the framework part.** An audible + `