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

103 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# PLAN — `Waveform` (visualización interactiva de audio)
> **Tipo**: plan de creación por fases (process — efímero, NO fuente de verdad).
> **Fecha**: 2026-07-31 · **Estado**: **✅ INICIATIVA COMPLETA 2026-08-01** —
> gate firmado el 2026-07-31 («ok» tras la presentación única: las 8 con las
> recomendaciones) y **F1–F6 cerradas**, F6 con la mirada del usuario.
> **Origen**: iniciativa hermana FIRMADA en D-AP2.4 (`PLAN-audio-player-v2.md`):
> componente canónico propio, POST-v1 del player — «tiene superficie de contrato
> real… y sirve fuera del player». Los cimientos que este plan asume ya existen
> y están commiteados: `uix.sound.decode(bytes)` (la puerta de muestras),
> `Slider` con `aria-valuetext` (G-1) y `SecondaryRange` (G-2), y `$libs/plots`
> (`scaleLinear` + `area`/`line`, verificado en el índice).
> **Kickoff sesión nueva**: _"Lee `docs/process/PLAN-waveform.md`; si el gate
> está firmado, sigue por la fase abierta (releyendo `component-guide.md` al
> abrir F2)."_
---
## 0. Qué es y quién lo consume
La representación de una señal de audio como onda/barras, **interactiva** (el
sector la usa como scrubber: SoundCloud, wavesurfer) o pasiva. Consumidores
reales que la barra ≥2 exige:
| Consumidor | Qué necesita |
| -------------------------------------------------------------------- | ------------------------------------- |
| `media-player` (TimeSlider-onda, v2 del player — FUERA de este plan) | onda + seek + progreso |
| `chat-message` (nota de voz) | onda compacta + seek |
| Estudio sema / editor | onda pasiva de un buffer decodificado |
## 1. Referencias (F-1.2, ≥3)
| Ref | Modelo |
| ----------------------- | ---------------------------------------------------------------------------------------------- |
| **wavesurfer.js 7** | el canon del sector: peaks precomputados O decode cliente; render canvas; regions/plugins |
| **peaks.js (BBC)** | separación DATOS/render: `audiowaveform` (server, C++) genera los picos; el cliente solo pinta |
| **SoundCloud** | la UX de referencia: barras + dos colores (pasado/futuro) + seek |
| **WhatsApp voice note** | la forma compacta: barras + cursor, en una fila de chat |
**Lectura**: la ruta profesional NO decodifica en el cliente (coste + CORS);
los picos son un dato que la app provee. El decode cliente es la conveniencia
opcional para ficheros locales/pequeños — y nuestra puerta ya existe
(`uix.sound.decode`).
## 2. La observación que encoge el plan
**Un waveform interactivo ES un slider con otra piel.** El drag continuo, el
click-to-jump, el teclado (← → Home End PageUp/Down), `aria-valuemin/max/now`,
`aria-valuetext` (G-1) y el `commit-set` del release — TODO eso ya vive en el
`Slider` canónico y sus señales sema. Re-declararlo sería exactamente la
duplicación que D-AP2.13 acaba de retirar del player.
**Propuesta**: `Waveform` COMPONE `Slider` (el patrón del `TimeSlider` del
player), y su morfo declara solo la capa que el Slider no tiene: la onda.
Consecuencia: **cero eventos propios en v1** (las señales son las del Slider
embebido) y el foco del componente queda en su verdadero contrato — los datos
(`peaks`) y su render.
## 3. Decisiones (gate del usuario — SIN FIRMAR)
| ID | Cuestión | Recomendación |
| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **D-WF.1** | Membresía / composición | **Componer `Slider`** (§2): morfo propio con partes de onda + Slider embebido para toda la interacción. La alternativa (re-implementar drag/teclado/aria) duplica un contrato PASS sin ganar nada |
| **D-WF.2** | Los datos | **`peaks: number[]` (0..1) provistos por la app** (firmado ya en D-AP2.4; la ruta BBC/SoundCloud). Sin fetch ni decode implícitos en el componente |
| **D-WF.3** | Extractor de picos cliente (opcional) | **`sound.peaks(buffer, buckets)` en el art `$sound`** — es la disposición escrita del plan del motor («percepción: analyser/picos, v2») y el flujo natural es `decode → peaks` en la misma puerta. Alternativas: `$libs` (puro, pero AudioBuffer es plataforma) o el componente (privilegio local, el antipatrón). Superficie: ~20 líneas (min/max por bucket sobre `getChannelData`) |
| **D-WF.4** | Render | **SVG vía `$libs/plots`** (firmado en D-AP2.4): `scaleLinear` + generadores de path. Sin canvas (un canvas suelto exigiría la ciudadanía de `$scene`; para N picos estáticos el SVG sobra y hereda theming/tokens gratis) |
| **D-WF.5** | Formas del render | `shape: 'bars' \| 'wave'` — barras (SoundCloud, default) y área continua espejada (wavesurfer). Ambas son un path distinto sobre los mismos picos; coste marginal |
| **D-WF.6** | Progreso (dos colores) | **`clipPath` por fracción de progreso** sobre un segundo path coloreado — cero re-render por frame (el clip se mueve con una CSS var/attr del provider, como el rAF del player ya alimenta el value del Slider) |
| **D-WF.7** | Interactividad | Interactivo por defecto (seek); `readonly` lo vuelve pasivo (el Slider ya tiene la prop — se reenvía). Sin `disabled` propio: el del Slider |
| **D-WF.8** | Sema | **Sin pack propio en v1**: cero eventos propios (D-WF.1) → las firmas son las del Slider embebido. Dentro del player, el silencio por descendencia YA lo cubre (D-AP2.7 v2). `expression` del morfo acorde (sin pack, guard S11d en paz) |
## 4. Contrato propuesto (borrador para F2 — se cierra con la firma)
- **Partes**: `Provider` (group; dueño de props/geometría) · `Wave` (el `<svg>`;
`data-shape`) · `Slider` embebido (montado por el wrapper soma, como
TimeSlider) · `Cursor` (indicator, opcional — la posición actual cuando se
quiere además del clip de color).
- **Props soma**: `peaks: number[]` · `value`/`onValueChange`/`onValueCommit` ·
`min`/`max` (dominio real: segundos) · `shape` · `readonly` · `valueText`
(reenviado a G-1) · `secondaryValue` (reenviado a G-2: buffer/preload sobre
la onda).
- **Eidos**: tokens `--waveform-*` (color pasado/futuro/cursor, bar-width/gap,
radius); tamaño por `data-size` como el slider.
- **A11y**: la del Slider embebido (aria completo + valuetext); el `Wave` es
`aria-hidden` (presentacional — el valor accesible es el slider).
## 5. Fases (tras la firma; método no-cascada; releer `component-guide.md` al abrir F2)
| Fase | Contenido | Verify |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **F1 — `sound.peaks`** ✅ **2026-07-31** | Extractor max-abs por bucket sobre TODOS los canales, normalizado por el bucket más alto (documentado: una grabación floja pinta onda completa; silencio total queda plano); puro, sin contexto ni guard disposed (nada que fugar) | ✅ tests (mono · multicanal con valores float32-exactos · silencio · degenerados) — 42/42 con el port |
| **F2 — Morfo** ✅ **2026-07-31** | 2 partes (provider con `data-shape`/`data-readonly` · `wave` svg presentacional aria-hidden) · **SIN campo `events`** · `expression: 'delegated'` (la firma perceptiva es del Slider embebido) · el part `Cursor` del borrador PODADO como especulativo (el thumb del Slider ES el playhead) | ✅ 4/4 (validate lanza-o-no, no lista · pin de cero eventos vía `'events' in`) · vocabulario limpio |
| **F3 — Soma** ✅ **2026-07-31** | `WaveformProvider` (geometría TESTEABLE en la clase: `buildWavePath` — bars = trazos verticales con FLOOR perceptivo 0.5 y ancho vía `stroke-width` de la recipe; wave = `area()` de plots espejada — · `progressFraction` clampada · label localizado para el Slider) · `WaveformWaveProvider` (renderProps) · wrapper componiendo **Slider completo** (SecondaryRange+Range+Thumb; reenvíos de `readonly`/`disabled`/`step`/`valueText`/`secondaryValue`) · Wave svelte con el **clip de progreso** (2 copias del MISMO path; `data-waveform-played`/`-remaining` hooks eidos-only) | ✅ 4/4 (contrato+geometría exacta `M25.00 15.5V16.5M75.00 1V31` · área cerrada+clamp · Wave presentacional · degradación sin picos) — lección re-aplicada: data reactivos del morfo se miden EN EL ELEMENTO tras tick (syncAttrs) |
| **F4 — Eidos** ✅ **2026-08-01** | Wrapper raíz (carga `slider.css`+`waveform.css`; `data-size` resuelto por ActiveEidos) · `Wave` passthrough · recipe: bars = stroke en UNIDADES DE VIEWBOX (el svg estira con `preserveAspectRatio="none"` → ciclo barra/hueco constante) / wave = fill sin stroke · RTL espejo CSS (`[dir='rtl']` → `scaleX(-1)`, mismo borde que el thumb) · readonly cursor · disabled vía `:has` (la onda vive FUERA del slider) · Slider en scope: track/range transparentes, secondary = velo 14%, thumb = playhead `2px×100%` (scale-active 1: una línea a alto completo no puede crecer sin desbordar), hit = caja completa (specificidad 3-attr, independiente del orden de carga) · tokens `waveform:{height-xs…xl, bar-width, color, color-played, playhead-width}` + generate — `playhead-width` AÑADIDO sobre la lista firmada: el thumb cuadrado (`--slider-thumb-size-*`) no expresa una línea · guards: fila en `component-visual-attrs` + sufijos `played`/`remaining` en la allowlist de `eidos-lint-all` | ✅ `recipe-css-contract` PASS · `eidos-lint` 0 inválidos (4 morfo-backed) · `component-api-contract` PASS · `check` 0 propios |
| **F5 — Demo + dossier** ✅ **2026-08-01** | Demo con picos REALES (fetch `/sounds/*.wav` → `uix.sound.decode` → `uix.sound.peaks`; 5 señales conmutables + `buckets` vivo + degradación declarada si no hay contexto) · cada prop = control vivo (shape/value/step/secondaryValue/readonly/disabled/dir/size) · trace sema del Slider embebido · 6 tabs · READMEs soma+eidos (comparativa wavesurfer/peaks.js/SoundCloud/WhatsApp; `## Passive justification` = delegación D-WF.1/D-WF.8) · **fix F3 declarado**: `dir` NO se reenviaba al Slider embebido — completado el patrón de reenvíos (types+wrapper soma; verificado en vivo: thumb ancla `right:0%` en RTL) | ✅ `component:audit --only waveform` PASS · `docs:check` 0 · `check` 0 propios · sonda en Chrome vivo (5173): picos reales pintados, clip↔thumb lockstep exacto, shape/size/RTL/readonly vivos |
| **F6 — Mirada** ✅ **2026-08-01** | **FIRMADA por el usuario** («podemos darlo por bueno el waveform»), contrastando el render contra el reproductor de **Suno AI** — que valida dos decisiones del gate: **barras** (D-WF.5, lectura SoundCloud) y los dos colores reproducido/pendiente como lo esencial (Suno ni siquiera pinta buffered; en el nuestro es opcional y arranca apagado). La mirada costó DOS correcciones que ninguna sonda había visto: (1) el **anillo del `commit`** enmarcando el control entero — firma global de la familia, exenta ahora en `slider.css`/`media-player.css` (commit `df86a4e50`); (2) el **buffered**, rechazado dos veces — primero por color (velo neutro = mismo token que la onda) y luego por FORMA (copia del path = ilegible por construcción, «confunde visualmente»): acabó en banda recta con acento lavado (`905012507`). Ojo para el futuro: con el panel oculto las CSSTransitions quedan `running` congeladas y `getComputedStyle` da valores INTERMEDIOS — medir la cascada (custom props), no los usados | ✅ mirada del usuario |
## 6. Fuera de alcance (con disposición)
Regions/selección (el modelo wavesurfer-plugin — el clip v2 del player tiene su
propio diseño congelado) · zoom · render en vivo del micrófono (eso es
analyser + `$scene`, la otra disposición del motor) · minimap.

Powered by TurnKey Linux.