Releases: naomiaro/waveform-playlist
Release list
Declared track order preserved on load
@dawcore/components@0.0.37
Fixed
<daw-track> elements no longer re-order into decode-completion order during initial load (#625, PR #626)
Declaratively ordered <daw-track> elements (e.g. vocals grouped above guitars above drum stems) could end up re-sorted by whichever track's audio finished decoding first. Each track's decode completion calls engine.setTracks() with only the loaded subset, and the engine→DOM order sync (introduced with track reordering in 0.0.36) anchored that subset at the front of the light DOM — promoting early-finishing tracks above still-loading ones.
The sync now permutes engine-known elements into engine order among themselves and never moves elements the engine hasn't seen yet, so declared order is preserved no matter what order decodes finish in. Undo/redo of track reorders behaves exactly as before.
Thanks to @yorkshiretwist for the report and the reproduction page.
Track reordering with live drag preview
Vertical track reordering with live drag preview
Drag a track by its grip — or use the accessible move up/down buttons — to reorder it, in both the React and web-component stacks. Reordering is purely organizational: playback is never interrupted, and it's undoable.
While dragging, both the controls column and the waveform timeline preview the drop live: the dragged track's waveform slot-snaps into the hovered position and displaced tracks slide out of the way (150 ms, prefers-reduced-motion aware). Escape animates everything back. The dragged row floats fully opaque with a lifted shadow, and the dragged track's waveform carries a data-track-drag-source emphasis tint.
@waveform-playlist/engine 13.6.0
- New
PlaylistEngine.reorderTrack(trackId, toIndex)— clamps, no-ops on unknown id/same index, pushes an undo snapshot, emitsstatechange. Never touches the adapter (order isn't audible).
@waveform-playlist/browser 15.4.0
usePlaylistControls().reorderTrack(trackId, toIndex)and<Waveform trackReordering />(default off): channel-strip reorder rail with grip + move buttons on the default track control panel. Buttons work standalone; drag needsClipInteractionProvider.SortableTrackControlsexported for custom-controls consumers.- Live drop preview:
usePlaylistData().trackDragPreview({ trackId, toIndex } | null) publishes the in-progress reorder; both playlist columns render from a shared layout map so they can never drift mid-drag. - Mixer state (name/mute/solo/volume/pan) travels with its track on reorder.
- Fixed: the dragged row vanishing mid-drag (dnd-kit's top-layer popover was eventlessly dismissed by its sortable plugin's DOM splice; a drag-scoped observer now re-shows it same-frame).
- Fixed:
useAudioTracksimmediate mode no longer warns "Cannot create track" on every rebuild pass for tracks still awaiting their decode — only genuine misconfigurations warn now.
@waveform-playlist/ui-components 13.2.0
- New
ReorderRail, grip and move up/down icons for the track-controls panel.
@dawcore/components 0.0.36
<daw-editor track-reordering>: grip + move buttons on<daw-track-controls>, pointer-drag reordering (mouse/touch/pen) with the same live lane preview — displaced lanes slide, the dragged lane slot-snaps with a.track-row[data-track-drag-source]styling hook, and everything reverts on cancel.- Bidirectional DOM ↔ engine order sync: moving
<daw-track>elements in the DOM reorders the engine (one undo step per permutation); engine-initiated order changes (undo/redo) move the elements back. - File drop now creates real
<daw-track>/<daw-clip>light-DOM elements through theaddTrack()pipeline.
@waveform-playlist/annotations 12.0.7, @waveform-playlist/recording 13.0.4
- Dependency-pin refresh releases (no behavior changes).
New example
examples/react-starter/— the smallest correct setup for the full multitrack editor (WaveformPlaylistProvideron the default Tone.js engine, mixer controls, dark amber theme), with a README section explaining the harmless "AudioContext was prevented from starting" browser warning (#620). Run withpnpm example:react-starter.- Track reordering is enabled on the stem-tracks website example and both dawcore multiclip example pages.
Annotation suite: web components, tick positions, bounded playback
Annotation suite for the dawcore web components — three PRs (#607, #610, #611) closing epic #455 and follow-ups #608/#609.
Annotation web components (#607, closes #455)
<daw-annotation-track>,<daw-annotation>,<daw-annotation-list>— single-source-of-truth dual-view model: the<daw-annotation>elements' attributes and text ARE the data; the editor's timeline lane and the list panel are two synchronized views over them.- Editable timeline lanes inside
<daw-editor>: drag-to-resize boundaries with link-endpoints cascading and collision push (boundary math shared with the ReactuseAnnotationControlshook via@waveform-playlist/core), click-to-seek,pointercancel-safe. - Keyboard controls (
keyboard-controls): arrow navigation with wrapping, Home/End, Enter plays the selection and stops at its end,[]{}boundary nudges, Escape two-press (deselect, then stop), full remapping viaannotationShortcuts, capture-phase priority over<daw-keyboard-shortcuts>. editor.play(startTime?, endTime?)for bounded playback;indefinite-playbacklifts the annotation end-clamp and the timeline extends to cover annotations past the audio.
Musical (tick/PPQN) positions + display modes (#610)
start-tick/end-tickon<daw-annotation>— authoritative when both set (the clipstartTickpattern); the editor derives and maintains the seconds cache, re-deriving on BPM changes so annotations stay on their bars.- Tick-native boundary editing through the same shared core math (integer ticks, 128th-note minimum duration),
editor.snapTo-aware drags viasnapTickToGrid, mixed tick/seconds tracks supported. - Beats-mode lanes render with clip-identical tick math — grid-exact alignment.
box-labelon<daw-annotation-track>:text(default) /id(classic aeneas-style ids in the bars, full text in the list) /none(bare region bars);namerenders a lane label in the controls sidebar.time-display="bars"on<daw-annotation-list>— bar.beat ranges (multi-meter aware via the newticksToBarBeatcore helper).- Playback-following
playinghighlight in both views (boundary-crossing updates only; selection state untouched). - Label-less annotations and list-less tracks are contractual (data-only region marking).
Bounded-playback completion + write-back correctness (#611, closes #608 & #609)
- New optional
PlayoutAdapter.onPlaybackEnded(callback | null)hook: the Tone adapter reports duration-limitedplay(start, end)completion, and the engine stops itself (microtask-deferred out of Tone's tick chain; dispose-race guarded). The React provider and<daw-editor>both mirror engine-initiated stops (exactly one stop event; provider/editor stop-then-play transitions suppressed). - Annotation boundary write-back diffs against live element values by id — closes the one-frame skipped-write window under reordered drags.
- Incidental fix: seeking during bounded playback no longer leaves a stale end-time watermark.
Demos
examples/dawcore-native/: annotations.html, sonnet.html (Sonnet 1 parity with the classic React example), tick regions + bars-view list on beats-grid.html. examples/dawcore-tone/annotations.html: the Tone-adapter annotations demo, used to verify the bounded-stop chain live against a real Tone Transport.
Published packages
annotations 12.0.6 · browser 15.3.4 · core 12.6.1 · @dawcore/components 0.0.35 · @dawcore/midi 0.0.4 · @dawcore/spectrogram 0.0.7 · engine 13.5.1 · media-element-playout 12.3.2 · midi 13.0.3 · playout 12.5.4 · recording 13.0.3 · spectrogram 13.0.7 · ui-components 13.1.5 · webaudio-peaks 12.0.2
SoundFont envelope truncation: staccato click fix (playout 12.5.3)
Fixes the confirmed audible click on staccato SoundFont notes with long-decay patches (piano, strings).
@waveform-playlist/playout 12.5.3
SoundFontToneTrack scheduled the full AHD volume envelope and stepped to sustain at note-off. For staccato notes, the decay ramp's endpoint sorted after note-off in the automation timeline and never governed — the gain held at peak for the entire note, then stepped peak→sustain in a single sample (0.576 measured at velocity 0.8 via Chromium OfflineAudioContext gain-curve capture): a hard click on any sustained patch played staccato.
The envelope is now truncated at note-off: since the note duration is known at schedule time, each AHD phase is clipped and the schedule ends with a partial ramp to the analytically-computed envelope value at note-off. The partial ramp lies on the same decay line, so notes that outlive the decay are scheduled identically to before. Cross-browser (no cancelAndHoldAtTime).
Browser-verified: max gain-curve sample step 0.576 → 0.000026 (the ramp's own slope) across staccato, long-note, mid-attack, and hold-phase cases.
Pin-refresh cascade (no code changes)
ui-components@13.1.4, recording@13.0.2, browser@15.3.2, annotations@12.0.5 — republished so their exact workspace pins resolve to the new playout.
dawcore correctness audit
dawcore correctness audit — @dawcore/components@0.0.34, @waveform-playlist/browser@15.3.3
Six waves of correctness fixes from a full audit of packages/dawcore (~32 confirmed bugs, each fixed test-first and adversarially reviewed before merge): PRs #601, #602, #603, #604, #605, #606.
@dawcore/components 0.0.34 — highlights
Interactions & playback
pointercancelis handled in the timeline and clip drag handlers — a canceled drag (touch interruption, pen leaving range, OS gesture) no longer stays armed on hover, no longer strands an open undo transaction (which silently broke undo for every subsequent edit), and reverts trim CSS, waveform peaks, and the selection preview. Cancels from other pointers (palm rejection) are ignored.<daw-editor>.seekTo()dispatchesdaw-seekinstead of spuriousdaw-stop/daw-play, updatescurrentTimesynchronously, surfaces engine failures viadaw-error, rejectsNaN, and clamps negatives. Splitting during playback no longer leaks a backward-jumpingdaw-timeupdate.- Removing the last track rewinds the engine to 0 (display and engine previously disagreed, so the next play jumped to a stale position) and stops a still-rolling transport with a proper
daw-stop.
<daw-player>
play()resumes from the current position (pause→play and paused-seek→play no longer restart a 40-minute episode at 0:00); play-after-stop still starts at the top.- Survives DOM reparenting (framework re-renders, list reorders) instead of dying permanently; paused seeks update the playhead and
daw-timeupdateconsumers; a supersededpeaks-srcfailure no longer fires a prematuredaw-ready.
Recording
- Teardown hardening: the no-audio path restores the armed-track mute (previously the punch-in track stayed silently muted forever); a throw during stop finalization no longer wedges the recorder with a live mic; a stop or unmount during the async start window cancels the start instead of being lost;
daw-recording-errornever contradicts an already-delivereddaw-recording-complete. - Finalized recorded clips render at the correct width in beats mode, the live preview lands exactly where the finalized clip appears, and each take no longer triggers a redundant full peak-generation pass.
Loading & lifecycle
- Tracks removed (or editors detached) mid-load are cancelled cleanly: no resurrected ghost tracks, no forever-hanging
addTrack()/addClip()/loadMidi()promises. Cancellations reject withTrackLoadCancelledErrorand carrydetail.reason: 'removed' | 'disconnected'ondaw-track-errorso error UIs can filter them. - Reparenting
<daw-editor>tears down symmetrically and fails loudly until a fresheditor.adapteris set (a disposedNativePlayoutAdapterwould otherwise decode fine into a dead audio graph). Undoing a track removal now restores real controls andeditor.tracksmembership; redoing prunes them again. - One peaks-worker crash no longer poisons waveform generation for the editor's lifetime.
Effects
exportAudio()matches live playback: failed-plugin placeholders are skipped (previously a dead plugin URL could reject the whole export or render an effect absent from what you hear), and master volume is applied — in the same position as live playback (before the master chain), which matters for compressors.setEffectsState()survives failing native entries (unregistered custom types become round-trippable placeholders instead of destroying the chain mid-restore). Generic parameter panels show current values on reopen.daw-effect-reorderreports the clamped index.
Rendering & misc
- Zoom can no longer go finer than the peaks can render (worker-generated peaks now raise the zoom floor, and floor raises re-clamp the live zoom). Toggling
monore-extracts immediately. Bars straddling 1000px canvas-chunk boundaries no longer leave 1–2px gaps. Recreated spectrogram controllers render without waiting for a scroll. MIME-rejected file drops dispatchdaw-files-load-error.
@waveform-playlist/browser 15.3.3
- A single peaks-worker crash no longer poisons waveform generation for the provider's lifetime —
useWaveformDataCachedetects the crashed worker and spawns a fresh one (mirror of the dawcore fix; the bug was ported along with the code).
Correctness audit fixes: playout 12.5.2 + @dawcore/midi 0.0.3
Correctness-audit release: fixes from full audits of @dawcore/midi (#597) and @waveform-playlist/playout (#598, #599), plus the exact-pin dependent cascade.
@waveform-playlist/playout 12.5.2
Mixed audio+MIDI tracks (a track with both clip kinds becomes two playout tracks, id + id:midi):
- Runtime controls (mute / volume / pan / solo) now reach the
:midicompanion — previously muting a mixed track left its MIDI playing, and soloing it silenced its own MIDI half. - Deleting a mixed track's last MIDI clip removes the stale companion instead of leaving ghost MIDI playback.
Playback & lifecycle:
- Manual mute now routes through solo-aware muting: unmuting a non-soloed track during an active solo no longer makes it audible (the manual state applies when solo clears).
closeGlobalAudioContextresets the singleton even when the raw context was closed externally — previously every futuregetGlobalContext()returned a dead context forever.SoundFontCache.load/loadFromBufferinvalidate the per-sample AudioBuffer cache on hot-swap (stale entries paired the old font's buffers with the new font's pitch/envelope — mispitched audio). Failed reloads keep the old font and its still-valid cache.- SoundFont note callbacks contain their own failures — one throwing note can no longer abort every other same-tick event across all tracks (nothing in Tone.js's tick chain catches).
- Clips added or swapped in during playback get their fade envelopes scheduled immediately (previously flat gain until the next play).
MidiToneTrack.stopAllSourcesguards each synth individually — a melodic release failure can't leave percussion voices stuck.getMediaStreamSourcecreates sources on the global playout context and its automatic cleanup actually fires (the old listeners targeted events that don't exist onMediaStream; every entry leaked). Localtrack.stop()still requiresreleaseMediaStreamSource()(spec: noendedevent on local stop — now documented).configureGlobalContexthonorslatencyHintin the default (standardized-audio-context) path.durationgetters on all track classes return the max clip end instead of the array-last clip's end.- Fetch errors include the HTTP status code; missing-SoundFont-sample warnings rate-limit per track instance; all error logging is single-string (Chrome lazy-evaluation safe).
@dawcore/midi 0.0.3
getTrackDurationno longer crashes with aRangeErroron very dense MIDI files (~130k+ notes — theMath.max(...spread)argument-stack limit).parseMidiFilewraps invalid-input errors with contextual detail (byte count) instead of leaking raw@tonejs/midiinternals.parseMidiUrlincludes the HTTP status code in fetch errors (statusText is often empty on HTTP/2).flattenedge cases documented: zero note-bearing tracks → zero result tracks; the merged track's track-level metadata comes from the first track (per-notechanneldrives playback).
Pin-refresh cascade (no code changes)
midi@13.0.2, ui-components@13.1.3, recording@13.0.1, browser@15.3.1, annotations@12.0.4 — republished so their exact workspace pins resolve to the new playout/@dawcore/midi versions.
Punch-in recording + matched transport semantics
Punch-in recording and matched end-of-timeline transport semantics across the React and Web Components surfaces.
Highlights
Punch-in recording — recorded takes land exactly at the playhead and replace whatever clip content they overlap (carveClipRange in core: partial overlaps are trimmed, covered clips removed, spanning clips split). While a take is being captured, the recorded-over track is transiently muted — the doomed material never plays under the overdub — and its previous mute state is restored when the session ends. The live preview renders full clip chrome (header included) from the first captured samples, and takes can be named (RecordingOptions.clipName on <daw-editor>, track-derived names in the demos).
Matched end-of-timeline semantics — both surfaces now share one model:
- Default: playback auto-stops at the end of the timeline and the cursor returns to the play-start position (player style). New behavior for
<daw-editor>, which previously never auto-stopped. indefinitePlayback/indefinite-playback: roll DAW-style until an explicit stop (implies the fill-viewport layout).fillViewport/fill-viewport(new): layout only — the timeline fills the scroll container even when the audio is shorter.- Recording sessions suppress the auto-stop automatically, so punch-in takes run past the end of existing audio.
Breaking changes
@waveform-playlist/worklets@13.0.0— the meter processor postsFloat32Arraychannel data (new wire format). Consumers reading the meter port directly must update; the recording package handles it internally.@waveform-playlist/recording@13.0.0— punch-in placement: finalized takes now land at the record-start playhead position and replace overlapped clip content (previously takes could not overwrite existing clips).startRecording()now resolvesPromise<boolean>(truewhen the capture pipeline actually started) — backward compatible for callers ignoring the value.@dawcore/components@0.0.33—<daw-editor>gains the end-of-timeline auto-stop by default; setindefinite-playbackto keep the previous roll-forever behavior.indefinite-playbackno longer implies only layout — use the newfill-viewportfor the layout half alone.
New APIs
usePlaylistControls().setRecordingActive(active, armedTrackId?)(@waveform-playlist/browser) — recording-session auto-stop suppression + armed-track transient mute; auto-wired from the WaveformrecordingStateprop, call eagerly beforeplay()in overdub flows.fillViewportprovider prop /fill-viewporteditor attribute — layout-only viewport fill.RecordingOptions.clipName(<daw-editor>.startRecording) — names the live-preview header and the finalized clip.enumerateMicrophones()/watchMicrophoneDevices()(@waveform-playlist/core) — framework-agnostic, permission-free device listing.resolveRecordingOffsetSamples()(@waveform-playlist/core) — the single source of truth for recording latency compensation, shared by all consumers; configurable vialatencyOffset(seconds).
Fixes
- Recording example: the live preview now renders at the punch-in position (previously drawn at the end of existing clips, far ahead of the playhead), record start is re-entrancy-guarded, and Stop during startup cancels the pending session.
<daw-editor>: an in-frame stop can no longer leave a zombie rAF loop (AnimationControllerreschedule guard).- Recording pipeline hardening from the overhaul: transferable worklet buffers, stop handshake with queue drain, correct pause/resume, mic-unplug auto-stop, per-session entry guards.
- Resolves #569 wire format, #579 punch-in, #589/#590 end-of-audio behavior, #593 fill-viewport parity.
Published packages
worklets@13.0.0, recording@13.0.0, core@12.6.0, browser@15.3.0, @dawcore/components@0.0.33, ui-components@13.1.2, and pin-refresh patches: playout@12.5.1, media-element-playout@12.3.1, webaudio-peaks@12.0.1, @dawcore/midi@0.0.2, @dawcore/spectrogram@0.0.6, midi@13.0.1, annotations@12.0.3, spectrogram@13.0.6.
WAM 2.0 hosting on the Tone backend + WAM-aware WAV export
Five packages: WAM 2.0 plugin hosting on the Tone backend (React + Web Components), WAM-aware WAV export, and the <daw-player> element's first npm appearance.
@waveform-playlist/browser 15.1.0
WAM 2.0 plugin hosting in React (#541) — on the /tone surface:
useDynamicEffects().addWamEffect(url)/useTrackDynamicEffects().addWamEffectToTrack(trackId, url)host WAM 2.0 plugins from module URLs into the master / per-track chains, interleaved with the built-in Tone.js effects. Requires native-context mode:configureGlobalContext({ nativeAudioContext: true })before any audio init.WamEffectGuimounts a plugin's own GUI (falls back to the generic parameter panel);getWamPlugin/getTrackWamPluginexpose live plugin handles.@dawcore/wamis an optional peer, dynamic-imported on first use — non-WAM consumers load zero WAM bytes.
WAM-aware WAV export (#536) — exports now render WAM entries instead of skipping them with a console warning:
- Rendering happens on a native
OfflineAudioContextwrapped in a ToneOfflineContextwhen native-context mode is active; WAM plugins are re-instantiated on the offline context from their URL-cached factories with the live instance's state (getState()) transferred at export time. - Fail-loud: a WAM plugin that can't be re-instantiated fails the export rather than silently rendering without it. Bypassed WAM entries stay excluded (parity with live).
createOfflineEffectsFunction()/createOfflineTrackEffectsFunction()outputs are now always async (Promise-returning) — consumers passing them toexportWav/ExportWavButtonare unaffected.- New types on
/tone:OfflineEffectsCleanup,OfflineEffectsFunction,OfflineTrackEffectsFunction;ExportOptions.effectsFunction/createOfflineTrackEffectswidened (non-breaking — every legacyEffectsFunction/TrackEffectsFunctionremains assignable). - Exports are serialized internally; the previous global context is always restored, even when the offline build throws. Avoid mutating effect chains or adding tracks while an export is in flight (see the WAM guide's limitations).
@waveform-playlist/playout 12.5.0
- Native AudioContext mode (#541):
configureGlobalContext({ nativeAudioContext: true })builds the global Tone context around a nativeAudioContext— required for WAM worklets.isNativeGlobalContext()/supportsNativeContextMode()report capability; Firefox (missing AudioListener AudioParams, Tone.js #1457) falls back to standardized-audio-context with a warning. - Effects-chain transport surface for dawcore's
<daw-editor>on the Tone backend:adapter.transport(ToneEffectsTransport) with master/track output hooks;TonePlayoutmaster bus input node.
@dawcore/components 0.0.28
<daw-player>(#473): lightweight single-track HTMLMediaElement player — no Tone.js, no engine. Attributes:src,peaks-src(pre-computed BBC audiowaveform.dat/.json),timescale,mono,wave-height,bar-width/bar-gap,playback-rate. Click-to-seek, fit-to-width waveform, scrubber fallback,daw-ready/daw-timeupdate/daw-ended/daw-errorevents.- The wam-transport bridge skip is no longer silent (#546): a one-time console warning explains the missing transport query surface and points at
NativePlayoutAdapterfor transport sync (#537).
@waveform-playlist/media-element-playout 12.3.0
- Player-mode support consumed by
<daw-player>(#473) — published counterpart of the workspace version dawcore 0.0.28 pins.
@dawcore/wam 0.0.3
- Loader carries
/* webpackIgnore: true */alongside/* @vite-ignore */on the dynamic plugin import (#541) — variable-URL plugin loads now work under webpack/Rspack hosts (Docusaurus), not just Vite.
Docs
- New guide sections for WAM hosting in React and WAV export behavior (
wam-plugins.md), including the transport-sync limitation on the Tone backend (#537) and its native-backend workaround.
Spectrogram helper centralization
Centralizes two pieces of spectrogram logic that were duplicated across packages, so a format/math change can no longer silently diverge the two rendering pipelines (the #554/#556 drift bug class). Behavior-preserving refactor — no user-facing behavior changes. Resolves #560 (PR #564).
What changed
- Padded FFT sample-range math is now single-sourced in
computePaddedFftRange(@dawcore/spectrogram). BothSpectrogramOrchestratorand the ReactcomputeChunkSampleRangedelegate to it. - Canvas-ID
${clipId}-ch${channelIndex}-chunk${n}contract is now single-sourced inbuildSpectrogramCanvasId/parseSpectrogramCanvasId(@waveform-playlist/core) — the only home reachable by all four parsers and both builders. A build∘parse roundtrip test acts as the cross-package regression guard.
Minor internal tightening: extractChunkNumber and the SpectrogramChannel unmount parser now reject malformed IDs lacking a -ch{N}- segment. Real canvas IDs are always full-format, so no production path is affected.
Published packages
| Package | Version |
|---|---|
@waveform-playlist/core |
12.5.0 |
@dawcore/spectrogram |
0.0.5 |
@waveform-playlist/ui-components |
13.1.1 |
@waveform-playlist/spectrogram |
13.0.5 |
@dawcore/components |
0.0.32 |
New public API
@waveform-playlist/core now exports buildSpectrogramCanvasId, parseSpectrogramCanvasId, and the SpectrogramCanvasIdParts type.
Spectrogram correctness audit
Spectrogram correctness audit
A full correctness audit of the spectrogram pipeline (@dawcore/spectrogram, its worker pool, and the React provider), followed by a multi-angle adversarial review of the fixes. Eight issues found, fixed, and verified (#553–#558, #562); PRs #559, #561, #563.
Packages
| Package | Version |
|---|---|
@dawcore/spectrogram |
0.0.4 |
@waveform-playlist/spectrogram |
13.0.4 |
@dawcore/components |
0.0.31 |
Rendering correctness
- Stereo and multi-clip tracks render correctly (#553) — render groups are now partitioned by
(clipId, channelIndex)before contiguity grouping. Previously, stereo tracks left most of one channel's chunks black, and multi-clip tracks could paint one clip's audio with another clip's FFT data. - Clips placed anywhere on the timeline, and trimmed clips, display the right audio (#554) — the FFT sample-range math now uses clip-relative pixels, and the worker's pixel→frame mapping accounts for the clip's audio-file offset. Previously only untrimmed clips at timeline 0 rendered the correct region.
- The linear frequency scale honors
minFrequency/maxFrequencyand no longer leaves a 1px transparent stripe at the top of every canvas (#557). Frequency bands are sanitized at the render boundary (NaN/negative/inverted bands fall back safely).
Worker pool
- The pool grows to one worker per channel (#556) — surround/multi-channel audio no longer silently renders channel 0/1's data on the higher channels. Registered clip audio is replayed only into workers that can compute one of the clip's channels.
- Superseded FFT work actually cancels (#555) — the orchestrator's generation-abort was off by one and never cancelled the in-flight generation; rapid scroll/zoom no longer pays for stale multi-second FFTs.
- Lifecycle hardening — a terminated pool can no longer be resurrected by late calls (leaked workers); channel indices are validated; a canvas registration that can't be served no longer strands its transferred
OffscreenCanvas; empty channel data is rejected loudly instead of caching an all-NaN spectrogram (#558). - Worker crash recovery (#558) — an uncaught worker error no longer permanently bricks the client; pending operations are rejected and the API stays usable.
React provider
- Canvas-registration race fixed (#562) — per-channel canvas registries can briefly differ in size during mount/recording; chunk indices are now filtered per channel instead of throwing
Cannot read properties of undefined (reading 'match')and aborting the clip's render pass. - Abort noise removed (#562) —
SpectrogramAbortErroris treated as normal control flow in the pool's multi-channel fan-out: no moreadditional channel FFT failure: abortedwarnings, and a real error is never masked by a concurrent abort.
Internals
- New pure render-geometry helpers (
pixelColumnToFrame,pixelRowToBin) with the coordinate conventions documented; the row→bin mapping is computed once per render call instead of per pixel. - The chunk layout contract (
chunkIndex × MAX_CANVAS_WIDTH) is documented and validated at canvas registration. - Test suite grew from 180 to 226 tests in
@dawcore/spectrogram, including end-to-end pixel assertions through the real worker module.
Follow-up tracked in #560 (centralizing range math and canvas-ID parsing shared between the dawcore and React packages).