@dawcore/components 0.0.5 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # @dawcore/components
2
+
3
+ Framework-agnostic Web Components for multi-track audio editing. Drop `<daw-editor>` into any HTML page — no React, no build step required.
4
+
5
+ ## Features
6
+
7
+ - **Pure Web Components** — Works with vanilla HTML, React, Vue, Svelte, or any framework
8
+ - **Declarative tracks** — `<daw-track>` and `<daw-clip>` elements define your timeline in HTML
9
+ - **Canvas waveforms** — Chunked rendering with virtual scrolling for large timelines
10
+ - **Drag interactions** — Move clips, trim boundaries, split at playhead
11
+ - **Keyboard shortcuts** — Play/pause, split, undo/redo via `<daw-keyboard-shortcuts>`
12
+ - **Undo/redo** — Full transaction-based undo with Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z
13
+ - **File drop** — Drag audio files onto the editor to add tracks
14
+ - **Recording** — Live mic recording with waveform preview, pause/resume, cancelable clip creation
15
+ - **Pre-computed peaks** — Instant waveform rendering from `.dat` files before audio decodes
16
+ - **Track controls** — Volume, pan, mute, solo per track via `<daw-track-controls>`
17
+ - **Transport access** — Tempo, metronome, count-in, meter, effects via `@dawcore/transport`
18
+ - **CSS theming** — Dark mode by default, fully customizable via CSS custom properties
19
+ - **Native Web Audio** — Uses `@dawcore/transport` for playback scheduling. No Tone.js dependency.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @dawcore/components
25
+ ```
26
+
27
+ Peer dependencies:
28
+ ```bash
29
+ npm install @waveform-playlist/core @waveform-playlist/engine @dawcore/transport
30
+ ```
31
+
32
+ Optional (for recording):
33
+ ```bash
34
+ npm install @waveform-playlist/recording @waveform-playlist/worklets
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```html
40
+ <script type="module">
41
+ import '@dawcore/components';
42
+ </script>
43
+
44
+ <daw-editor id="editor" samples-per-pixel="1024" wave-height="100" timescale>
45
+ <daw-track src="/audio/drums.opus" name="Drums"></daw-track>
46
+ <daw-track src="/audio/bass.opus" name="Bass"></daw-track>
47
+ <daw-track src="/audio/synth.opus" name="Synth"></daw-track>
48
+ </daw-editor>
49
+
50
+ <daw-transport for="editor">
51
+ <daw-play-button></daw-play-button>
52
+ <daw-pause-button></daw-pause-button>
53
+ <daw-stop-button></daw-stop-button>
54
+ </daw-transport>
55
+ ```
56
+
57
+ That's it. The editor loads audio, generates waveforms, and handles playback.
58
+
59
+ ## Multi-Clip Timeline
60
+
61
+ For multiple clips per track with independent positioning:
62
+
63
+ ```html
64
+ <daw-editor id="editor" samples-per-pixel="1024" wave-height="80"
65
+ timescale clip-headers interactive-clips>
66
+ <daw-keyboard-shortcuts playback splitting undo></daw-keyboard-shortcuts>
67
+
68
+ <daw-track name="Drums">
69
+ <daw-clip src="/audio/drums.opus" start="0" duration="8"></daw-clip>
70
+ <daw-clip src="/audio/drums.opus" start="12" duration="8" offset="8"></daw-clip>
71
+ </daw-track>
72
+
73
+ <daw-track name="Bass">
74
+ <daw-clip src="/audio/bass.opus" start="0" duration="20"></daw-clip>
75
+ </daw-track>
76
+ </daw-editor>
77
+ ```
78
+
79
+ ## Pre-Computed Peaks
80
+
81
+ For instant waveform rendering before audio finishes decoding:
82
+
83
+ ```html
84
+ <daw-track name="Drums">
85
+ <daw-clip src="/audio/drums.opus"
86
+ peaks-src="/audio/drums.dat"
87
+ start="0" duration="8"></daw-clip>
88
+ </daw-track>
89
+ ```
90
+
91
+ The `.dat` file renders the waveform immediately. Audio decodes in the background for playback.
92
+
93
+ ## Transport Access
94
+
95
+ Access the native transport for tempo, metronome, count-in, meter, and effects:
96
+
97
+ ```javascript
98
+ const editor = document.getElementById('editor');
99
+
100
+ // Transport is available after first track loads
101
+ editor.addEventListener('daw-play', () => {
102
+ const transport = editor.engine?.adapter?.transport;
103
+ if (!transport) return;
104
+
105
+ // Tempo & meter
106
+ transport.setTempo(140);
107
+ transport.setMeter(3, 4);
108
+
109
+ // Metronome (default click sounds built in)
110
+ transport.setMetronomeEnabled(true);
111
+
112
+ // Count-in
113
+ transport.setCountIn(true);
114
+ transport.setCountInBars(1);
115
+ transport.setCountInMode('always');
116
+
117
+ transport.on('countIn', ({ beat, totalBeats }) => {
118
+ console.log(beat + '/' + totalBeats);
119
+ });
120
+
121
+ // Effects hook — insert any AudioNode chain
122
+ transport.connectTrackOutput('track-id', reverbNode);
123
+ });
124
+ ```
125
+
126
+ ## Programmatic File Loading
127
+
128
+ ```javascript
129
+ const editor = document.getElementById('editor');
130
+ const result = await editor.loadFiles(fileList);
131
+ // result: { loaded: string[], failed: Array<{ file, error }> }
132
+ ```
133
+
134
+ ## Recording
135
+
136
+ ```html
137
+ <daw-editor id="editor" samples-per-pixel="1024" wave-height="100">
138
+ <daw-track name="Recording"></daw-track>
139
+ </daw-editor>
140
+
141
+ <daw-transport for="editor">
142
+ <daw-play-button></daw-play-button>
143
+ <daw-pause-button></daw-pause-button>
144
+ <daw-stop-button></daw-stop-button>
145
+ <daw-record-button></daw-record-button>
146
+ </daw-transport>
147
+
148
+ <script type="module">
149
+ const editor = document.getElementById('editor');
150
+ // Consumer provides the mic stream
151
+ const stream = await navigator.mediaDevices.getUserMedia({
152
+ audio: { channelCount: { ideal: 2 } } // request stereo when available
153
+ });
154
+ editor.recordingStream = stream;
155
+
156
+ // Cancelable — prevent default to handle the AudioBuffer yourself
157
+ editor.addEventListener('daw-recording-complete', (e) => {
158
+ // e.preventDefault(); // skip automatic clip creation
159
+ console.log('recorded:', e.detail.audioBuffer);
160
+ });
161
+ </script>
162
+ ```
163
+
164
+ ## Keyboard Shortcuts
165
+
166
+ Add `<daw-keyboard-shortcuts>` as a child of `<daw-editor>`:
167
+
168
+ ```html
169
+ <daw-editor id="editor">
170
+ <daw-keyboard-shortcuts playback splitting undo></daw-keyboard-shortcuts>
171
+ <!-- ... tracks ... -->
172
+ </daw-editor>
173
+ ```
174
+
175
+ | Attribute | Shortcuts |
176
+ |-----------|-----------|
177
+ | `playback` | Space (play/pause), Enter (stop) |
178
+ | `splitting` | S (split at playhead) |
179
+ | `undo` | Cmd/Ctrl+Z (undo), Cmd/Ctrl+Shift+Z (redo) |
180
+
181
+ Custom shortcuts via JS properties: `playbackShortcuts`, `splittingShortcuts`, `undoShortcuts`, `customShortcuts`.
182
+
183
+ ## CSS Theming
184
+
185
+ Style with CSS custom properties on `<daw-editor>` or any ancestor:
186
+
187
+ ```css
188
+ daw-editor {
189
+ --daw-wave-color: #c49a6c;
190
+ --daw-playhead-color: #d08070;
191
+ --daw-background: #1a1a2e;
192
+ --daw-track-background: #16213e;
193
+ --daw-ruler-color: #c49a6c;
194
+ --daw-ruler-background: #0f0f1a;
195
+ --daw-selection-color: rgba(99, 199, 95, 0.3);
196
+ --daw-controls-background: #1a1a2e;
197
+ --daw-controls-text: #e0d4c8;
198
+ --daw-clip-header-background: rgba(0, 0, 0, 0.4);
199
+ --daw-clip-header-text: #e0d4c8;
200
+ --daw-controls-width: 180px;
201
+ --daw-min-height: 200px;
202
+ }
203
+ ```
204
+
205
+ ## Elements
206
+
207
+ ### `<daw-editor>`
208
+
209
+ Core orchestrator. Attributes:
210
+
211
+ | Attribute | Type | Default | Description |
212
+ |-----------|------|---------|-------------|
213
+ | `samples-per-pixel` | number | `1024` | Zoom level |
214
+ | `sample-rate` | number | `48000` | AudioContext sample rate hint |
215
+ | `wave-height` | number | `100` | Track waveform height in pixels |
216
+ | `timescale` | boolean | `false` | Show time ruler |
217
+ | `clip-headers` | boolean | `false` | Show clip name headers |
218
+ | `interactive-clips` | boolean | `false` | Enable drag/trim/split |
219
+ | `mono` | boolean | `false` | Merge stereo to mono display |
220
+ | `eager-resume` | boolean | `false` | Resume AudioContext on first user gesture |
221
+
222
+ JS properties: `audioContext`, `recordingStream`, `engine`.
223
+
224
+ Methods: `loadFiles(fileList)`, `splitAtPlayhead()`.
225
+
226
+ ### `<daw-track>`
227
+
228
+ Declarative track data. Attributes: `src`, `name`, `volume`, `pan`, `muted`, `soloed`, `mono`.
229
+
230
+ ### `<daw-clip>`
231
+
232
+ Declarative clip data. Attributes: `src`, `peaks-src`, `start`, `duration`, `offset`, `gain`.
233
+
234
+ ### `<daw-transport for="editor-id">`
235
+
236
+ Container that resolves target editor. Children: `<daw-play-button>`, `<daw-pause-button>`, `<daw-stop-button>`, `<daw-record-button>`.
237
+
238
+ ### `<daw-track-controls>`
239
+
240
+ Per-track UI for volume, pan, mute, solo. Receives state from editor, dispatches `daw-track-control` and `daw-track-remove` events.
241
+
242
+ ### `<daw-keyboard-shortcuts>`
243
+
244
+ Render-less child of `<daw-editor>`. Boolean attributes: `playback`, `splitting`, `undo`.
245
+
246
+ ## Events
247
+
248
+ ```javascript
249
+ const editor = document.getElementById('editor');
250
+
251
+ // Playback
252
+ editor.addEventListener('daw-play', () => {});
253
+ editor.addEventListener('daw-pause', () => {});
254
+ editor.addEventListener('daw-stop', () => {});
255
+ editor.addEventListener('daw-seek', (e) => console.log(e.detail.time));
256
+
257
+ // Selection & tracks
258
+ editor.addEventListener('daw-selection', (e) => console.log(e.detail));
259
+ editor.addEventListener('daw-track-select', (e) => console.log(e.detail.trackId));
260
+
261
+ // Clip interactions
262
+ editor.addEventListener('daw-clip-move', (e) => console.log(e.detail));
263
+ editor.addEventListener('daw-clip-trim', (e) => console.log(e.detail));
264
+ editor.addEventListener('daw-clip-split', (e) => console.log(e.detail));
265
+
266
+ // Recording
267
+ editor.addEventListener('daw-recording-start', (e) => console.log(e.detail));
268
+ editor.addEventListener('daw-recording-complete', (e) => {
269
+ // e.preventDefault() to skip automatic clip creation
270
+ console.log(e.detail.audioBuffer);
271
+ });
272
+
273
+ // Errors
274
+ editor.addEventListener('daw-track-error', (e) => console.error(e.detail));
275
+ editor.addEventListener('daw-error', (e) => console.error(e.detail));
276
+ editor.addEventListener('daw-files-load-error', (e) => console.error(e.detail));
277
+ ```
278
+
279
+ ## Custom AudioContext
280
+
281
+ By default, `<daw-editor>` creates its own `AudioContext` using the `sample-rate` attribute. To provide your own:
282
+
283
+ ```javascript
284
+ const editor = document.getElementById('editor');
285
+ editor.audioContext = new AudioContext({ sampleRate: 48000, latencyHint: 0 });
286
+ ```
287
+
288
+ Set this before tracks load. The provided context is used for decoding, playback, and recording.
289
+
290
+ ## License
291
+
292
+ MIT
package/dist/index.d.mts CHANGED
@@ -278,6 +278,8 @@ declare global {
278
278
  interface RecordingOptions {
279
279
  trackId?: string;
280
280
  bits?: 8 | 16;
281
+ /** Fallback channel count when stream doesn't report one via getSettings(). Must be 1 or 2. */
282
+ channelCount?: 1 | 2;
281
283
  startSample?: number;
282
284
  /** Start playback during recording so user hears existing tracks. */
283
285
  overdub?: boolean;
@@ -300,7 +302,7 @@ interface RecordingSession {
300
302
  readonly channelCount: number;
301
303
  readonly bits: Bits;
302
304
  isFirstMessage: boolean;
303
- /** Latency samples to skip in live preview (outputLatency + lookAhead). */
305
+ /** Latency samples to skip in live preview (outputLatency only). */
304
306
  readonly latencySamples: number;
305
307
  readonly wasOverdub: boolean;
306
308
  /** Stored so it can be removed on stop/cleanup — not just when stream ends. */
@@ -315,6 +317,7 @@ type ReadonlyRecordingSession = Readonly<Omit<RecordingSession, 'chunks' | 'peak
315
317
  };
316
318
  /** Narrow interface for the host editor. */
317
319
  interface RecordingHost extends ReactiveControllerHost {
320
+ readonly audioContext: AudioContext;
318
321
  readonly samplesPerPixel: number;
319
322
  readonly effectiveSampleRate: number;
320
323
  readonly _selectedTrackId: string | null;
@@ -329,7 +332,7 @@ interface RecordingHost extends ReactiveControllerHost {
329
332
  declare class RecordingController implements ReactiveController {
330
333
  private _host;
331
334
  private _sessions;
332
- private _workletLoaded;
335
+ private _workletLoadedCtx;
333
336
  constructor(host: RecordingHost & HTMLElement);
334
337
  hostConnected(): void;
335
338
  hostDisconnected(): void;
@@ -562,6 +565,12 @@ declare class DawEditorElement extends LitElement {
562
565
  _selectionStartTime: number;
563
566
  _selectionEndTime: number;
564
567
  _currentTime: number;
568
+ /** Consumer-provided AudioContext. When set, used for decode, playback, and recording. */
569
+ private _externalAudioContext;
570
+ private _ownedAudioContext;
571
+ /** Set an AudioContext to use for all audio operations. Must be set before tracks load. */
572
+ set audioContext(ctx: AudioContext | null);
573
+ get audioContext(): AudioContext;
565
574
  _engine: PlaylistEngine | null;
566
575
  private _enginePromise;
567
576
  _audioCache: Map<string, Promise<AudioBuffer>>;
@@ -612,13 +621,6 @@ declare class DawEditorElement extends LitElement {
612
621
  private _onTrackRemoveRequest;
613
622
  private _readTrackDescriptor;
614
623
  private _loadTrack;
615
- private _contextConfigurePromise;
616
- /**
617
- * Ensure the global AudioContext is configured with the editor's sample-rate hint
618
- * before the first audio operation. Idempotent — concurrent callers await the
619
- * same promise so no one proceeds to getGlobalAudioContext() before configuration.
620
- */
621
- private _ensureContextConfigured;
622
624
  _fetchAndDecode(src: string): Promise<AudioBuffer>;
623
625
  private _fetchPeaks;
624
626
  _recomputeDuration(): void;
@@ -769,6 +771,10 @@ declare global {
769
771
  }
770
772
  }
771
773
 
774
+ interface AudioResumeHost extends ReactiveControllerHost, HTMLElement {
775
+ /** Returns the AudioContext to resume on user gesture */
776
+ readonly audioContext: AudioContext;
777
+ }
772
778
  declare class AudioResumeController implements ReactiveController {
773
779
  private _host;
774
780
  private _target;
@@ -776,7 +782,7 @@ declare class AudioResumeController implements ReactiveController {
776
782
  private _generation;
777
783
  /** CSS selector, or 'document'. When undefined, controller is inert. */
778
784
  target?: string;
779
- constructor(host: ReactiveControllerHost & HTMLElement);
785
+ constructor(host: AudioResumeHost);
780
786
  hostConnected(): void;
781
787
  hostDisconnected(): void;
782
788
  private _onGesture;
package/dist/index.d.ts CHANGED
@@ -278,6 +278,8 @@ declare global {
278
278
  interface RecordingOptions {
279
279
  trackId?: string;
280
280
  bits?: 8 | 16;
281
+ /** Fallback channel count when stream doesn't report one via getSettings(). Must be 1 or 2. */
282
+ channelCount?: 1 | 2;
281
283
  startSample?: number;
282
284
  /** Start playback during recording so user hears existing tracks. */
283
285
  overdub?: boolean;
@@ -300,7 +302,7 @@ interface RecordingSession {
300
302
  readonly channelCount: number;
301
303
  readonly bits: Bits;
302
304
  isFirstMessage: boolean;
303
- /** Latency samples to skip in live preview (outputLatency + lookAhead). */
305
+ /** Latency samples to skip in live preview (outputLatency only). */
304
306
  readonly latencySamples: number;
305
307
  readonly wasOverdub: boolean;
306
308
  /** Stored so it can be removed on stop/cleanup — not just when stream ends. */
@@ -315,6 +317,7 @@ type ReadonlyRecordingSession = Readonly<Omit<RecordingSession, 'chunks' | 'peak
315
317
  };
316
318
  /** Narrow interface for the host editor. */
317
319
  interface RecordingHost extends ReactiveControllerHost {
320
+ readonly audioContext: AudioContext;
318
321
  readonly samplesPerPixel: number;
319
322
  readonly effectiveSampleRate: number;
320
323
  readonly _selectedTrackId: string | null;
@@ -329,7 +332,7 @@ interface RecordingHost extends ReactiveControllerHost {
329
332
  declare class RecordingController implements ReactiveController {
330
333
  private _host;
331
334
  private _sessions;
332
- private _workletLoaded;
335
+ private _workletLoadedCtx;
333
336
  constructor(host: RecordingHost & HTMLElement);
334
337
  hostConnected(): void;
335
338
  hostDisconnected(): void;
@@ -562,6 +565,12 @@ declare class DawEditorElement extends LitElement {
562
565
  _selectionStartTime: number;
563
566
  _selectionEndTime: number;
564
567
  _currentTime: number;
568
+ /** Consumer-provided AudioContext. When set, used for decode, playback, and recording. */
569
+ private _externalAudioContext;
570
+ private _ownedAudioContext;
571
+ /** Set an AudioContext to use for all audio operations. Must be set before tracks load. */
572
+ set audioContext(ctx: AudioContext | null);
573
+ get audioContext(): AudioContext;
565
574
  _engine: PlaylistEngine | null;
566
575
  private _enginePromise;
567
576
  _audioCache: Map<string, Promise<AudioBuffer>>;
@@ -612,13 +621,6 @@ declare class DawEditorElement extends LitElement {
612
621
  private _onTrackRemoveRequest;
613
622
  private _readTrackDescriptor;
614
623
  private _loadTrack;
615
- private _contextConfigurePromise;
616
- /**
617
- * Ensure the global AudioContext is configured with the editor's sample-rate hint
618
- * before the first audio operation. Idempotent — concurrent callers await the
619
- * same promise so no one proceeds to getGlobalAudioContext() before configuration.
620
- */
621
- private _ensureContextConfigured;
622
624
  _fetchAndDecode(src: string): Promise<AudioBuffer>;
623
625
  private _fetchPeaks;
624
626
  _recomputeDuration(): void;
@@ -769,6 +771,10 @@ declare global {
769
771
  }
770
772
  }
771
773
 
774
+ interface AudioResumeHost extends ReactiveControllerHost, HTMLElement {
775
+ /** Returns the AudioContext to resume on user gesture */
776
+ readonly audioContext: AudioContext;
777
+ }
772
778
  declare class AudioResumeController implements ReactiveController {
773
779
  private _host;
774
780
  private _target;
@@ -776,7 +782,7 @@ declare class AudioResumeController implements ReactiveController {
776
782
  private _generation;
777
783
  /** CSS selector, or 'document'. When undefined, controller is inert. */
778
784
  target?: string;
779
- constructor(host: ReactiveControllerHost & HTMLElement);
785
+ constructor(host: AudioResumeHost);
780
786
  hostConnected(): void;
781
787
  hostDisconnected(): void;
782
788
  private _onGesture;