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

19 KiB

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.