Releases: naomiaro/waveform-playlist
MIDI Tracks via Tone Adapter (Piano-Roll)
@dawcore/components 0.0.15 — programmatic MIDI tracks, rendered as piano-roll, played back via TonePlayoutAdapter.
What's new
<daw-piano-roll>— new visual element. Shadow DOM, chunked canvas, virtual scroll. Auto-fits pitch range, velocity → opacity, theme-aware.<daw-clip>MIDI surface —midiNotesJS property +midi-channel(0-15) andmidi-program(0-127) reflected attributes with validatednoAccessorsetters.<daw-track render-mode>—'waveform' \| 'piano-roll'reflected attribute, switches the per-clip renderer.editor.addTrack({ midi: { notes, channel?, program? } })— convenience sugar that creates a piano-roll track with one MIDI clip.- CSS theming —
--daw-piano-roll-note-color,--daw-piano-roll-selected-note-color,--daw-piano-roll-background. - Demo —
examples/dawcore-tone/midi.html(programmatic C major scale via PolySynth).
Behavior
- A clip is treated as MIDI iff
clip.midiNotes != null. MIDI clips skip audio fetch + decode + peak generation. - Trim handles and split-at-playhead are inert on MIDI clips (note slicing deferred to a future engine PR). Move drag works.
- Playback requires the Tone.js adapter — the native
Transportadapter renders piano-roll but is silent for MIDI.
Defensive guarantees
- MIDI note arrays are validated at the boundary; NaN/Infinity / out-of-range entries are dropped with a
console.warn. - Audio→MIDI clip transitions purge stale per-clip caches (
_clipBuffers,_peaksData,_clipOffsets). - The
syncPeaksForChangedClipsfilter skips piano-roll tracks (no more "no AudioBuffer" warnings on MIDI tracks).
Out of scope (future)
editor.loadMidi(url)and.midfile parsing.midfile-drop routing- Native
TransportMIDI scheduling (PolySynth-equivalent inNativePlayoutAdapter) - MIDI clip trim/split (needs note-array slicing in
@waveform-playlist/engine) - MIDI clip late-append via
_loadAndAppendClip
Install
npm install @dawcore/components@0.0.15PR: #385
Tone Playhead Lookahead Fix
Fixes a ~100ms playhead-vs-audio drift when using the Tone.js playout adapter. Native adapter is unaffected. PR #384.
What changed
Tone.Transport.seconds is the scheduling position — lookAhead (default 0.1s) ahead of what the listener actually hears. Tone uses this to give Web Audio scheduling headroom; sources started at Transport.seconds reach the speaker lookAhead later. Visual consumers (playhead, progress fill, recording preview) had been positioning DOM elements at the scheduling position, so the playhead led audio by ~100ms with the Tone adapter.
The fix subtracts engine.lookAhead (in addition to existing outputLatency) so all visual elements track audible playback.
Highlights
engine.lookAhead— new getter onPlaylistEngineproxies the adapter's lookahead value (Tone ~0.1s, native 0). Consumers no longer need to know which adapter is in use.- Dual-ref pattern in React —
currentTimeRefstores raw scheduling time,visualTimeRefstores audible time. Storage stays raw soplay()resumes from the correct position; display gets the audible-time version. NewsetCurrentTimeRefs()helper enforces the pairing at every pause/seek/stop/loop site. PlayheadProps.visualTimeRef— new optional prop on the<Playhead>component for custom playheads. Fallback chain preserves backwards compatibility.- Recording live preview — strips leading-silence peaks (
outputLatency + lookAhead) so the preview's right edge lands at audible time, matching the playhead. MirrorsuseIntegratedRecording's finalization compensation. audibleLatencySamples()— new helper in@waveform-playlist/coreis the single source of truth for(outputLatency + lookAhead) * sampleRate → samples. Used by both the recording finalizer and the live preview.<daw-editor>—_startPlayhead/_stopPlayheadsubtractoutputLatency + engine.lookAhead._currentTimeitself is never overwritten so playback position stays accurate.
Package versions
| Package | Version |
|---|---|
@waveform-playlist/core |
12.0.0 → 12.1.0 |
@waveform-playlist/engine |
13.1.0 → 13.2.0 |
@waveform-playlist/playout |
12.2.0 → 12.3.0 |
@waveform-playlist/ui-components |
12.0.0 → 12.1.0 |
@waveform-playlist/browser |
12.0.0 → 12.1.0 |
@waveform-playlist/recording |
12.0.1 → 12.0.2 |
@dawcore/components |
0.0.13 → 0.0.14 |
Migration
No breaking changes. All additions are opt-in or transparent:
- React consumers: pick up the fix by upgrading
@waveform-playlist/browserand its peer packages. No code changes required. <daw-editor>consumers: pick up the fix by upgrading@dawcore/components. No code changes required.- Custom playhead authors: pass
visualTimeRefthrough to the underlying<Playhead>for perfect alignment with audible output. The playhead falls back to existing behavior if omitted.
Programmatic Track & Clip API
Summary
Adds an imperative API on <daw-editor> for tracks and clips, wires <daw-clip> lifecycle events so dynamic DOM mutation works alongside the imperative methods, and adds indefinite-playback so the ruler renders before any audio is loaded.
Closes #381.
Published Packages
| Package | Version | Change |
|---|---|---|
@dawcore/components |
0.0.13 | Programmatic track/clip API + indefinite-playback + helper refactor |
Programmatic API
| Method | Behavior |
|---|---|
editor.ready() |
Builds engine eagerly without requiring a track |
editor.addTrack(config) |
Builds and appends a <daw-track>, returns the element when loaded |
editor.updateTrack(id, partial) |
Mutates reflected attrs on the <daw-track> (or descriptor + engine for non-DOM tracks) |
editor.removeTrack(id) |
Removes via MutationObserver, falls back for non-DOM tracks |
editor.addClip(trackId, config) |
Validates src, builds and appends a <daw-clip>, returns the clipId |
editor.updateClip(trackId, clipId, partial) |
Sets reflected props on the <daw-clip> |
editor.removeClip(trackId, clipId) |
Removes the <daw-clip> DOM element |
Both declarative DOM mutation and these methods feed the same load pipeline — descriptors, peaks, and clip buffers populate identically.
Declarative DOM parity
<daw-clip> now dispatches daw-clip-connected (deferred via setTimeout(0)) and daw-clip-update (post-render, includes trackId resolved at dispatch). The editor handles both events plus clip-removal via the existing MutationObserver. Engine clip ids are aligned with <daw-clip>.clipId so DOM and engine refer to the same id.
indefinite-playback for empty editors
New boolean attribute. When set, the timeline fills the visible viewport even with no clips. The ruler covers max(naturalDuration, viewportWidth) so a short clip on an empty editor doesn't shrink the ruler. An empty controls-column placeholder prevents horizontal shift when the first track loads. ViewportController gains a ResizeObserver for window-resize support.
New Example
examples/dawcore-native/programmatic.html— ImperativeaddTrack/addClip/updateClip/removeClipplus declarative DOM mutation, side-by-side. Usesindefinite-playbackso the ruler renders from the start.
Usage
import '@dawcore/components';
import { NativePlayoutAdapter } from '@dawcore/transport';
const editor = document.querySelector('daw-editor');
editor.adapter = new NativePlayoutAdapter(new AudioContext());
// Build engine before any track exists — useful for wiring analyzers,
// effects, or master taps before content arrives.
await editor.ready();
// Imperative track creation
const track = await editor.addTrack({
name: 'Drums',
volume: 0.8,
clips: [{ src: '/audio/drums.opus', start: 0 }],
});
// Imperative clip mutation
const clipId = await editor.addClip(track.trackId, {
src: '/audio/snare.opus',
start: 4,
});
editor.updateClip(track.trackId, clipId, { start: 6, gain: 0.5 });
editor.removeClip(track.trackId, clipId);
// indefinite-playback in HTML
// <daw-editor timescale indefinite-playback>...</daw-editor>Internal Refactor
- Extracted
_resolvePeaksand_finalizeAudioCliphelpers to consolidate the per-clip load pipeline previously duplicated between_loadTrackand_loadAndAppendClip._finalizeAudioClipself-purges its caches ongeneratePeaksfailure so callers don't leak. ClipDescriptoris now a discriminated union:DomClipDescriptor (kind: 'dom', clipId)|DropClipDescriptor (kind: 'drop'). Replaces the optionalclipId?field. New exportedisDomClip(c)type predicate.- Per-clip isolation in
_loadTrack: a single bad clip dispatchesdaw-clip-errorand skips, rather than aborting the whole track and leaking earlier clips' cache writes. - Many silent paths now warn (unknown
removeTrack/removeClip/updateClipids, DOM/engine clip-id misalignment, etc.).
Breaking Changes (@dawcore/components 0.0.x line)
ClipConfig.srcis now required.ClipDescriptor.clipId?replaced with discriminated union (kind: 'dom' | 'drop').
Test Plan
pnpm typecheck✓pnpm lint(0 errors) ✓cd packages/dawcore && npx vitest run— 374/374 tests pass (was 320 before, +54 new test cases)
Documentation
- Spec at
docs/specs/web-components-migration.mdupdated with the full Programmatic Track Mutation section. - README updated with the imperative API summary and a link to the spec.
PRs
- #382 — programmatic track & clip API + indefinite-playback
- #383 — extract clip-load helpers + discriminated ClipDescriptor
Deferred
- Engine-side fade application from
<daw-clip>fadeIn/fadeOut/fadeTypeproperties — needs adapter-side investigation to know ifFadeobjects are honored byNativePlayoutAdapterandTonePlayoutAdapter.
Master Output Node
Summary
Exposes masterOutputNode on both adapters — a serial tap point in the audio signal chain for connecting analyzers, effects, recorders, or any AudioNode.
Published Packages
| Package | Version | Change |
|---|---|---|
@waveform-playlist/engine |
13.1.0 | Optional masterOutputNode on PlayoutAdapter interface |
@waveform-playlist/playout |
12.2.0 | Serial Gain tap node, eager playout creation, masterOutputNode getter |
@dawcore/transport |
0.0.10 | Transport.masterOutputNode getter |
New Examples
examples/dawcore-native/analyser.html— Spectrum analyser with Native Web Audioexamples/dawcore-tone/analyser.html— Spectrum analyser with Tone.js
Usage
// Parallel — connect analyser, audio still flows to speakers
const analyser = audioContext.createAnalyser();
adapter.masterOutputNode.connect(analyser);
// Serial — disconnect speakers, route through effect chain
adapter.masterOutputNode.disconnect(audioContext.destination);
adapter.masterOutputNode.connect(reverb);
reverb.connect(audioContext.destination);Signal Chain
- Native:
masterGain → destination(masterOutputNode = masterGain) - Tone.js:
masterVolume → [effects] → tap → destination(masterOutputNode = tap)
Closes part of #378 (spectrum analyzer output routing)
Adapter-Pluggable daw-editor
Summary
<daw-editor> is now adapter-agnostic — consumers provide their own PlayoutAdapter (NativePlayoutAdapter or TonePlayoutAdapter). No default adapter or AudioContext created internally.
Published Packages
| Package | Version | Change |
|---|---|---|
@waveform-playlist/engine |
13.0.0 | Breaking: ppqn required on PlayoutAdapter, adapter is source of truth |
@waveform-playlist/playout |
12.1.0 | TonePlayoutAdapter gains tempo/meter, cross-context worklet bridge |
@waveform-playlist/worklets |
12.1.0 | addRecordingWorkletModule / addMeterWorkletModule (SAC pattern) |
@waveform-playlist/recording |
12.0.1 | Internal: uses new worklet helpers |
@dawcore/components |
0.0.12 | Breaking: adapter required, transport getter removed, sample-rate attribute removed |
@dawcore/transport |
0.0.9 | Exposes audioContext and ppqn getters |
Highlights
- Adapter pluggability — choose between Native Web Audio (multi-tempo, multi-meter, metronome) and Tone.js (effects, MIDI synths)
- PPQN driven by adapter — adapter is the source of truth for tick resolution;
setPpqn?()lets the editor request, adapter decides - Cross-context worklet support (SAC pattern) — recording works with both adapters via callback injection for
addModuleand optionalcreateAudioWorkletNode/createMediaStreamSourceon the adapter - Examples restructured —
examples/dawcore-native/andexamples/dawcore-tone/with basic, multiclip, beats-grid, and recording pages - Independent versioning — packages now follow their own semver based on actual changes
Breaking Changes (dawcore 0.0.x)
editor.adapter = new NativePlayoutAdapter(new AudioContext())required before useeditor.transportremoved — useadapter.transportdirectlyeditor.audioContextsetter removed — context comes from adaptersample-rateattribute removed — rate determined by adapter's AudioContext
Migration
// Before
import '@dawcore/components';
// editor auto-created AudioContext + NativePlayoutAdapter
// After
import '@dawcore/components';
import { NativePlayoutAdapter } from '@dawcore/transport';
const editor = document.querySelector('daw-editor');
editor.adapter = new NativePlayoutAdapter(new AudioContext());Closes #378
v12.0.0
Breaking Changes
- Recording utilities moved to core —
generatePeaks,appendPeaks,createAudioBuffer,concatenateAudioDataremoved from@waveform-playlist/recording. Import from@waveform-playlist/coreinstead. - Cross-package re-exports removed — Import types from their canonical source:
Peaks,Bits,PeakData→@waveform-playlist/core(was also inwebaudio-peaks)applyFadeIn,applyFadeOut,FadeConfig,FadeType→@waveform-playlist/core(was also inplayout)AnnotationData,AnnotationAction, etc. →@waveform-playlist/core(was also inannotations)
Fixes
- Actionable error when worklets missing — If
@waveform-playlist/workletsis not installed and recording is attempted, the error message now includes installation instructions.
Install
npm install @waveform-playlist/browser@12.0.0dawcore
@dawcore/components@0.0.11 · @dawcore/transport@0.0.7
Breaking
- No longer requires React —
@waveform-playlist/recordingremoved as peer dep. Worklets loaded via dynamicimport()only when recording starts. (closes #373)
Features
- Variable tempo grid rendering —
AudioClip.startTickas authoritative timeline position, per-segment waveform rendering (#369) - Multi-meter support —
MeterEntrytype,detectMeterChanges(),computeMusicalTicksrefactored for multiple time signatures (#375) - Transport adapter updates — Variable tempo clip player, tick-space matching in generate/onPositionJump
Install
npm install @dawcore/components@0.0.11 @dawcore/transport@0.0.7v11.3.1
Bug Fixes
- Fix stereo channel preservation in WAV export — Tone.js
Pannerdefaulted tochannelCount: 1, forcing stereo-to-mono downmix during offline export. Channels are now derived from source material viatrackChannelCount(). Mono tracks export as mono, stereo as stereo. (#371, closes #370)
Improvements
- Unified WAV export to single Tone.Offline path — Removed the native
OfflineAudioContextfallback. Export now mirrors the live playback graph exactly, preventing future divergence bugs. - Shared
gainToDb()utility — Extracted to@waveform-playlist/core, replacing 5 duplicate private methods across playout and browser packages. - Shared
trackChannelCount()utility — Derives max channel count from a track's clips. Used for both PannerchannelCountand offline output channels. - Export fade curves use core functions —
applyFadeIn/applyFadeOutfrom core replace duplicate implementations, ensuring consistent fade shapes between playback and export. - Individual track export ignores solo state — Exporting a single track no longer silently produces silence when another track is soloed.
Install
npm install @waveform-playlist/browser@11.3.1v11.2.0
What's New
Pre-computed Peaks (.dat) — Sample Rate Matching
- Sample rate mismatch detection — when
.datsample rate doesn't match the AudioContext hardware rate, falls back to worker-generated peaks with a per-clip warning sampleRateprop onWaveformPlaylistProviderandsample-rateattribute on<daw-editor>for rate comparison and early mismatch warningsconfigureGlobalContext({ sampleRate })in@waveform-playlist/playout— compares requested rate against hardware rate before first audio operation- Peak extraction rate conversion (browser) — converts offsets when pre-computed peaks rate differs from decoded audio rate, showing approximate preview until worker replaces
Dawcore (.dat File Support)
- Peaks-first rendering for
<daw-editor>—.datwaveform data renders immediately while audio decodes in the background .datURL detection — usesURLconstructor to handle query strings, fragments, and case-insensitive extensions- Peaks URL cache — deduplicates in-flight and repeated fetches for the same
.dat/.jsonURL
Documentation
- Recommend Opus format (always 48000 Hz) for pre-computed peaks workflows
- WAV/FLAC at 48000 Hz as uncompressed/lossless alternatives
- Sample rate matching guidance with
ffmpegresample commands
Known Limitations
sampleRateprop is compare-and-warn only — Tone.js 15.1.22 doesn't passsampleRatethrough tostandardized-audio-context. Upstream fix exists but is unreleased.
Full Changelog: v11.1.0...v11.2.0
v11.1.0
New Features
Undo/Redo
- Snapshot-based undo/redo stack on
PlaylistEnginewith configurableundoLimit(default 100) - Transactions for grouping drag operations into single undo steps (
beginTransaction/commitTransaction/abortTransaction) useUndoStatehook —canUndo/canRedoinusePlaylistState(),undo/redoinusePlaylistControls()<KeyboardShortcuts undo />enables Cmd/Ctrl+Z (undo) and Cmd/Ctrl+Shift+Z (redo)- Clip drag handlers wrapped in transactions — move and trim are each one undo step
Keyboard Shortcuts
KeyboardShortcuttype,handleKeyboardEvent(), andgetShortcutLabel()moved to@waveform-playlist/core— shared across packagesKeyboardShortcutscomponent gainsundoprop
Engine Improvements
moveClipreturns constrained delta (number) for accurate cumulative trackinggetClipBounds()andconstrainTrimDelta()for UI-level constraint queriesskipAdapterparameter onmoveClip/trimClipfor deferred adapter sync during drag
Bug Fixes
- playout:
ToneTrack.replaceClipsforce-reschedules all Transport events whentrack.startTimechanges (fixes ghost audio after clip moves) - engine:
commitTransactionskips push when no mutations occurred (prevents phantom undo entries from no-op drags)
v11.0.1
Bug Fix
- fix(playout): Reschedule all Transport events when
track.startTimechanges inToneTrack.replaceClips. Previously, clips with unchanged relativestartTimekept stale Transport events at the old absolute position, causing audio to play from the wrong timeline position after moving the first clip on a track.
Engine Additions (non-breaking)
PlaylistEngine.getClipBounds(trackId, clipId)— returns clip offset, duration, start, and source durationPlaylistEngine.constrainTrimDelta(trackId, clipId, boundary, delta)— constrains a trim delta using collision/bounds logicskipAdapteroptional parameter onmoveClipandtrimClip— defers adapter sync for performance during drag operations