@dawcore/transport 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1107 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ClipPlayer: () => ClipPlayer,
24
+ Clock: () => Clock,
25
+ MasterNode: () => MasterNode,
26
+ MetronomePlayer: () => MetronomePlayer,
27
+ NativePlayoutAdapter: () => NativePlayoutAdapter,
28
+ SampleTimeline: () => SampleTimeline,
29
+ Scheduler: () => Scheduler,
30
+ TempoMap: () => TempoMap,
31
+ TickTimeline: () => TickTimeline,
32
+ Timer: () => Timer,
33
+ TrackNode: () => TrackNode,
34
+ Transport: () => Transport
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/core/clock.ts
39
+ var Clock = class {
40
+ constructor(audioContext) {
41
+ this._running = false;
42
+ this._audioTimeAtStart = 0;
43
+ this._clockTimeAtStart = 0;
44
+ this._audioContext = audioContext;
45
+ }
46
+ start() {
47
+ if (this._running) return;
48
+ this._audioTimeAtStart = this._audioContext.currentTime;
49
+ this._running = true;
50
+ }
51
+ stop() {
52
+ if (!this._running) return;
53
+ this._clockTimeAtStart = this.getTime();
54
+ this._running = false;
55
+ }
56
+ reset() {
57
+ this._running = false;
58
+ this._clockTimeAtStart = 0;
59
+ this._audioTimeAtStart = 0;
60
+ }
61
+ getTime() {
62
+ if (this._running) {
63
+ return this._clockTimeAtStart + (this._audioContext.currentTime - this._audioTimeAtStart);
64
+ }
65
+ return this._clockTimeAtStart;
66
+ }
67
+ seekTo(time) {
68
+ if (this._running) {
69
+ this._clockTimeAtStart = time;
70
+ this._audioTimeAtStart = this._audioContext.currentTime;
71
+ } else {
72
+ this._clockTimeAtStart = time;
73
+ }
74
+ }
75
+ /**
76
+ * Convert transport time to AudioContext.currentTime space.
77
+ * Used by players to schedule AudioBufferSourceNode.start(when).
78
+ */
79
+ toAudioTime(transportTime) {
80
+ return this._audioContext.currentTime + (transportTime - this.getTime());
81
+ }
82
+ isRunning() {
83
+ return this._running;
84
+ }
85
+ };
86
+
87
+ // src/core/scheduler.ts
88
+ var Scheduler = class {
89
+ constructor(options = {}) {
90
+ this._rightEdge = 0;
91
+ this._listeners = /* @__PURE__ */ new Set();
92
+ this._loopEnabled = false;
93
+ this._loopStart = 0;
94
+ this._loopEnd = 0;
95
+ this._lookahead = options.lookahead ?? 0.2;
96
+ this._onLoop = options.onLoop;
97
+ }
98
+ addListener(listener) {
99
+ this._listeners.add(listener);
100
+ }
101
+ removeListener(listener) {
102
+ this._listeners.delete(listener);
103
+ }
104
+ setLoop(enabled, start, end) {
105
+ if (enabled && start >= end) {
106
+ console.warn(
107
+ "[waveform-playlist] Scheduler.setLoop: start (" + start + ") must be less than end (" + end + ")"
108
+ );
109
+ return;
110
+ }
111
+ this._loopEnabled = enabled;
112
+ this._loopStart = start;
113
+ this._loopEnd = end;
114
+ }
115
+ reset(time) {
116
+ this._rightEdge = time;
117
+ }
118
+ advance(currentTime) {
119
+ const targetEdge = currentTime + this._lookahead;
120
+ if (this._loopEnabled && this._loopEnd > this._loopStart) {
121
+ const loopDuration = this._loopEnd - this._loopStart;
122
+ let remaining = targetEdge - this._rightEdge;
123
+ while (remaining > 0) {
124
+ const distToEnd = this._loopEnd - this._rightEdge;
125
+ if (distToEnd <= 0 || distToEnd > remaining) {
126
+ this._generateAndConsume(this._rightEdge, this._rightEdge + remaining);
127
+ this._rightEdge += remaining;
128
+ break;
129
+ }
130
+ this._generateAndConsume(this._rightEdge, this._loopEnd);
131
+ remaining -= distToEnd;
132
+ for (const listener of this._listeners) {
133
+ listener.onPositionJump(this._loopStart);
134
+ }
135
+ this._onLoop?.(this._loopStart);
136
+ this._rightEdge = this._loopStart;
137
+ if (loopDuration <= 0) break;
138
+ }
139
+ return;
140
+ }
141
+ if (targetEdge > this._rightEdge) {
142
+ this._generateAndConsume(this._rightEdge, targetEdge);
143
+ this._rightEdge = targetEdge;
144
+ }
145
+ }
146
+ _generateAndConsume(from, to) {
147
+ for (const listener of this._listeners) {
148
+ try {
149
+ const events = listener.generate(from, to);
150
+ for (const event of events) {
151
+ try {
152
+ listener.consume(event);
153
+ } catch (err) {
154
+ console.warn("[waveform-playlist] Scheduler: error consuming event:", String(err));
155
+ }
156
+ }
157
+ } catch (err) {
158
+ console.warn("[waveform-playlist] Scheduler: error generating events:", String(err));
159
+ }
160
+ }
161
+ }
162
+ };
163
+
164
+ // src/core/timer.ts
165
+ var Timer = class {
166
+ constructor(onTick) {
167
+ this._rafId = null;
168
+ this._running = false;
169
+ this._onTick = onTick;
170
+ }
171
+ start() {
172
+ if (this._running) return;
173
+ this._running = true;
174
+ this._scheduleFrame();
175
+ }
176
+ stop() {
177
+ this._running = false;
178
+ if (this._rafId !== null) {
179
+ cancelAnimationFrame(this._rafId);
180
+ this._rafId = null;
181
+ }
182
+ }
183
+ _scheduleFrame() {
184
+ this._rafId = requestAnimationFrame(() => {
185
+ if (!this._running) return;
186
+ try {
187
+ this._onTick();
188
+ } catch (err) {
189
+ console.warn("[waveform-playlist] Timer tick error:", String(err));
190
+ }
191
+ this._scheduleFrame();
192
+ });
193
+ }
194
+ };
195
+
196
+ // src/timeline/sample-timeline.ts
197
+ var SampleTimeline = class {
198
+ constructor(sampleRate) {
199
+ this._sampleRate = sampleRate;
200
+ }
201
+ get sampleRate() {
202
+ return this._sampleRate;
203
+ }
204
+ samplesToSeconds(samples) {
205
+ return samples / this._sampleRate;
206
+ }
207
+ secondsToSamples(seconds) {
208
+ return Math.round(seconds * this._sampleRate);
209
+ }
210
+ };
211
+
212
+ // src/timeline/tick-timeline.ts
213
+ var TickTimeline = class {
214
+ constructor(ppqn = 960) {
215
+ this._ppqn = ppqn;
216
+ }
217
+ get ppqn() {
218
+ return this._ppqn;
219
+ }
220
+ ticksPerBeat() {
221
+ return this._ppqn;
222
+ }
223
+ ticksPerBar(beatsPerBar) {
224
+ return this._ppqn * beatsPerBar;
225
+ }
226
+ toPosition(ticks, beatsPerBar) {
227
+ const ticksPerBar = this.ticksPerBar(beatsPerBar);
228
+ const bar = Math.floor(ticks / ticksPerBar) + 1;
229
+ const remaining = ticks % ticksPerBar;
230
+ const beat = Math.floor(remaining / this._ppqn) + 1;
231
+ const tick = remaining % this._ppqn;
232
+ return { bar, beat, tick };
233
+ }
234
+ fromPosition(bar, beat, tick, beatsPerBar) {
235
+ const ticksPerBar = this.ticksPerBar(beatsPerBar);
236
+ return (bar - 1) * ticksPerBar + (beat - 1) * this._ppqn + tick;
237
+ }
238
+ };
239
+
240
+ // src/timeline/tempo-map.ts
241
+ var TempoMap = class {
242
+ constructor(ppqn = 960, initialBpm = 120) {
243
+ this._ppqn = ppqn;
244
+ this._entries = [{ tick: 0, bpm: initialBpm, secondsAtTick: 0 }];
245
+ }
246
+ getTempo(atTick = 0) {
247
+ const entry = this._entryAt(atTick);
248
+ return entry.bpm;
249
+ }
250
+ setTempo(bpm, atTick = 0) {
251
+ if (atTick === 0) {
252
+ this._entries[0] = { ...this._entries[0], bpm };
253
+ this._recomputeCache(0);
254
+ return;
255
+ }
256
+ let i = this._entries.length - 1;
257
+ while (i > 0 && this._entries[i].tick > atTick) i--;
258
+ if (this._entries[i].tick === atTick) {
259
+ this._entries[i] = { ...this._entries[i], bpm };
260
+ } else {
261
+ const secondsAtTick = this._ticksToSecondsInternal(atTick);
262
+ this._entries.splice(i + 1, 0, { tick: atTick, bpm, secondsAtTick });
263
+ i = i + 1;
264
+ }
265
+ this._recomputeCache(i);
266
+ }
267
+ ticksToSeconds(ticks) {
268
+ return this._ticksToSecondsInternal(ticks);
269
+ }
270
+ secondsToTicks(seconds) {
271
+ let lo = 0;
272
+ let hi = this._entries.length - 1;
273
+ while (lo < hi) {
274
+ const mid = lo + hi + 1 >> 1;
275
+ if (this._entries[mid].secondsAtTick <= seconds) {
276
+ lo = mid;
277
+ } else {
278
+ hi = mid - 1;
279
+ }
280
+ }
281
+ const entry = this._entries[lo];
282
+ const secondsIntoSegment = seconds - entry.secondsAtTick;
283
+ const ticksPerSecond = entry.bpm / 60 * this._ppqn;
284
+ return entry.tick + secondsIntoSegment * ticksPerSecond;
285
+ }
286
+ beatsToSeconds(beats) {
287
+ return this.ticksToSeconds(beats * this._ppqn);
288
+ }
289
+ secondsToBeats(seconds) {
290
+ return this.secondsToTicks(seconds) / this._ppqn;
291
+ }
292
+ _ticksToSecondsInternal(ticks) {
293
+ const entry = this._entryAt(ticks);
294
+ const ticksIntoSegment = ticks - entry.tick;
295
+ const secondsPerTick = 60 / (entry.bpm * this._ppqn);
296
+ return entry.secondsAtTick + ticksIntoSegment * secondsPerTick;
297
+ }
298
+ _entryAt(tick) {
299
+ let lo = 0;
300
+ let hi = this._entries.length - 1;
301
+ while (lo < hi) {
302
+ const mid = lo + hi + 1 >> 1;
303
+ if (this._entries[mid].tick <= tick) {
304
+ lo = mid;
305
+ } else {
306
+ hi = mid - 1;
307
+ }
308
+ }
309
+ return this._entries[lo];
310
+ }
311
+ _recomputeCache(fromIndex) {
312
+ for (let i = Math.max(1, fromIndex); i < this._entries.length; i++) {
313
+ const prev = this._entries[i - 1];
314
+ const tickDelta = this._entries[i].tick - prev.tick;
315
+ const secondsPerTick = 60 / (prev.bpm * this._ppqn);
316
+ this._entries[i] = {
317
+ ...this._entries[i],
318
+ secondsAtTick: prev.secondsAtTick + tickDelta * secondsPerTick
319
+ };
320
+ }
321
+ }
322
+ };
323
+
324
+ // src/audio/master-node.ts
325
+ var MasterNode = class {
326
+ constructor(audioContext) {
327
+ this._gainNode = audioContext.createGain();
328
+ }
329
+ get input() {
330
+ return this._gainNode;
331
+ }
332
+ get output() {
333
+ return this._gainNode;
334
+ }
335
+ setVolume(value) {
336
+ this._gainNode.gain.value = value;
337
+ }
338
+ dispose() {
339
+ try {
340
+ this._gainNode.disconnect();
341
+ } catch (err) {
342
+ console.warn("[waveform-playlist] MasterNode.dispose: error disconnecting:", String(err));
343
+ }
344
+ }
345
+ };
346
+
347
+ // src/audio/track-node.ts
348
+ var TrackNode = class {
349
+ constructor(id, audioContext) {
350
+ this._destination = null;
351
+ this._effectsInput = null;
352
+ this.id = id;
353
+ this._volumeNode = audioContext.createGain();
354
+ this._panNode = audioContext.createStereoPanner();
355
+ this._panNode.channelCount = 2;
356
+ this._muteNode = audioContext.createGain();
357
+ this._volumeNode.connect(this._panNode);
358
+ this._panNode.connect(this._muteNode);
359
+ }
360
+ /** Where clip sources connect */
361
+ get input() {
362
+ return this._volumeNode;
363
+ }
364
+ /** Connect this track's output to a destination (master node) */
365
+ connectOutput(destination) {
366
+ this._destination = destination;
367
+ this._muteNode.connect(destination);
368
+ }
369
+ setVolume(value) {
370
+ this._volumeNode.gain.value = value;
371
+ }
372
+ setPan(value) {
373
+ this._panNode.pan.value = value;
374
+ }
375
+ setMute(muted) {
376
+ this._muteNode.gain.value = muted ? 0 : 1;
377
+ }
378
+ connectEffects(effectsInput) {
379
+ if (this._effectsInput) {
380
+ this.disconnectEffects();
381
+ }
382
+ this._muteNode.disconnect();
383
+ this._muteNode.connect(effectsInput);
384
+ this._effectsInput = effectsInput;
385
+ }
386
+ disconnectEffects() {
387
+ if (this._effectsInput && this._destination) {
388
+ this._muteNode.disconnect();
389
+ this._muteNode.connect(this._destination);
390
+ this._effectsInput = null;
391
+ }
392
+ }
393
+ dispose() {
394
+ for (const node of [this._volumeNode, this._panNode, this._muteNode]) {
395
+ try {
396
+ node.disconnect();
397
+ } catch (err) {
398
+ console.warn(
399
+ "[waveform-playlist] TrackNode.dispose: error disconnecting node:",
400
+ String(err)
401
+ );
402
+ }
403
+ }
404
+ }
405
+ };
406
+
407
+ // src/audio/clip-player.ts
408
+ var ClipPlayer = class {
409
+ constructor(audioContext, sampleTimeline, toAudioTime) {
410
+ this._tracks = /* @__PURE__ */ new Map();
411
+ this._trackNodes = /* @__PURE__ */ new Map();
412
+ this._activeSources = /* @__PURE__ */ new Map();
413
+ this._loopEnabled = false;
414
+ this._loopEnd = 0;
415
+ this._audioContext = audioContext;
416
+ this._sampleTimeline = sampleTimeline;
417
+ this._toAudioTime = toAudioTime;
418
+ }
419
+ setTracks(tracks, trackNodes) {
420
+ this._tracks.clear();
421
+ this._trackNodes = trackNodes;
422
+ for (const track of tracks) {
423
+ this._tracks.set(track.id, { track, clips: track.clips });
424
+ }
425
+ }
426
+ setLoop(enabled, _start, end) {
427
+ this._loopEnabled = enabled;
428
+ this._loopEnd = end;
429
+ }
430
+ updateTrack(trackId, track) {
431
+ this._tracks.set(trackId, { track, clips: track.clips });
432
+ this._silenceTrack(trackId);
433
+ }
434
+ generate(fromTime, toTime) {
435
+ const events = [];
436
+ for (const [trackId, state] of this._tracks) {
437
+ for (const clip of state.clips) {
438
+ if (clip.durationSamples === 0) continue;
439
+ if (!clip.audioBuffer) continue;
440
+ const clipStartTime = this._sampleTimeline.samplesToSeconds(clip.startSample);
441
+ const clipDuration = this._sampleTimeline.samplesToSeconds(clip.durationSamples);
442
+ const clipOffsetTime = this._sampleTimeline.samplesToSeconds(clip.offsetSamples);
443
+ if (clipStartTime < fromTime) continue;
444
+ if (clipStartTime >= toTime) continue;
445
+ const fadeInDuration = clip.fadeIn ? this._sampleTimeline.samplesToSeconds(clip.fadeIn.duration ?? 0) : 0;
446
+ const fadeOutDuration = clip.fadeOut ? this._sampleTimeline.samplesToSeconds(clip.fadeOut.duration ?? 0) : 0;
447
+ let duration = clipDuration;
448
+ if (this._loopEnabled && clipStartTime + duration > this._loopEnd) {
449
+ duration = this._loopEnd - clipStartTime;
450
+ }
451
+ events.push({
452
+ trackId,
453
+ clipId: clip.id,
454
+ audioBuffer: clip.audioBuffer,
455
+ transportTime: clipStartTime,
456
+ offset: clipOffsetTime,
457
+ duration,
458
+ gain: clip.gain,
459
+ fadeInDuration,
460
+ fadeOutDuration
461
+ });
462
+ }
463
+ }
464
+ return events;
465
+ }
466
+ consume(event) {
467
+ const trackNode = this._trackNodes.get(event.trackId);
468
+ if (!trackNode) {
469
+ console.warn(
470
+ '[waveform-playlist] ClipPlayer.consume: no TrackNode for trackId "' + event.trackId + '", clipId "' + event.clipId + '" \u2014 clip will not play'
471
+ );
472
+ return;
473
+ }
474
+ if (event.offset >= event.audioBuffer.duration) {
475
+ return;
476
+ }
477
+ const source = this._audioContext.createBufferSource();
478
+ source.buffer = event.audioBuffer;
479
+ const when = this._toAudioTime(event.transportTime);
480
+ const gainNode = this._audioContext.createGain();
481
+ gainNode.gain.value = event.gain;
482
+ let fadeIn = event.fadeInDuration;
483
+ let fadeOut = event.fadeOutDuration;
484
+ if (fadeIn + fadeOut > event.duration) {
485
+ const ratio = event.duration / (fadeIn + fadeOut);
486
+ fadeIn *= ratio;
487
+ fadeOut *= ratio;
488
+ }
489
+ if (fadeIn > 0) {
490
+ gainNode.gain.setValueAtTime(0, when);
491
+ gainNode.gain.linearRampToValueAtTime(event.gain, when + fadeIn);
492
+ }
493
+ if (fadeOut > 0) {
494
+ const fadeOutStart = when + event.duration - fadeOut;
495
+ gainNode.gain.setValueAtTime(event.gain, fadeOutStart);
496
+ gainNode.gain.linearRampToValueAtTime(0, when + event.duration);
497
+ }
498
+ source.connect(gainNode);
499
+ gainNode.connect(trackNode.input);
500
+ this._activeSources.set(source, {
501
+ trackId: event.trackId,
502
+ gainNode
503
+ });
504
+ source.addEventListener("ended", () => {
505
+ this._activeSources.delete(source);
506
+ try {
507
+ gainNode.disconnect();
508
+ } catch (err) {
509
+ console.warn("[waveform-playlist] ClipPlayer: error disconnecting gain node:", String(err));
510
+ }
511
+ });
512
+ source.start(when, event.offset, event.duration);
513
+ }
514
+ onPositionJump(newTime) {
515
+ this.silence();
516
+ for (const [trackId, state] of this._tracks) {
517
+ for (const clip of state.clips) {
518
+ if (clip.durationSamples === 0) continue;
519
+ if (!clip.audioBuffer) continue;
520
+ const clipStartTime = this._sampleTimeline.samplesToSeconds(clip.startSample);
521
+ const clipDuration = this._sampleTimeline.samplesToSeconds(clip.durationSamples);
522
+ const clipEndTime = clipStartTime + clipDuration;
523
+ const clipOffsetTime = this._sampleTimeline.samplesToSeconds(clip.offsetSamples);
524
+ if (clipStartTime <= newTime && clipEndTime > newTime) {
525
+ const offsetIntoClip = newTime - clipStartTime;
526
+ const offset = clipOffsetTime + offsetIntoClip;
527
+ const duration = clipEndTime - newTime;
528
+ const fadeOutDuration = clip.fadeOut ? this._sampleTimeline.samplesToSeconds(clip.fadeOut.duration ?? 0) : 0;
529
+ this.consume({
530
+ trackId,
531
+ clipId: clip.id,
532
+ audioBuffer: clip.audioBuffer,
533
+ transportTime: newTime,
534
+ offset,
535
+ duration,
536
+ gain: clip.gain,
537
+ fadeInDuration: 0,
538
+ fadeOutDuration
539
+ });
540
+ }
541
+ }
542
+ }
543
+ }
544
+ silence() {
545
+ for (const [source, { gainNode }] of this._activeSources) {
546
+ try {
547
+ source.stop();
548
+ } catch (err) {
549
+ console.warn("[waveform-playlist] ClipPlayer.silence: error stopping source:", String(err));
550
+ }
551
+ try {
552
+ gainNode.disconnect();
553
+ } catch (err) {
554
+ console.warn("[waveform-playlist] ClipPlayer.silence: error disconnecting:", String(err));
555
+ }
556
+ }
557
+ this._activeSources.clear();
558
+ }
559
+ _silenceTrack(trackId) {
560
+ const toDelete = [];
561
+ for (const [source, info] of this._activeSources) {
562
+ if (info.trackId === trackId) {
563
+ try {
564
+ source.stop();
565
+ } catch (err) {
566
+ console.warn(
567
+ "[waveform-playlist] ClipPlayer._silenceTrack: error stopping source:",
568
+ String(err)
569
+ );
570
+ }
571
+ try {
572
+ info.gainNode.disconnect();
573
+ } catch (err) {
574
+ console.warn(
575
+ "[waveform-playlist] ClipPlayer._silenceTrack: error disconnecting:",
576
+ String(err)
577
+ );
578
+ }
579
+ toDelete.push(source);
580
+ }
581
+ }
582
+ for (const source of toDelete) {
583
+ this._activeSources.delete(source);
584
+ }
585
+ }
586
+ };
587
+
588
+ // src/audio/metronome-player.ts
589
+ var MetronomePlayer = class {
590
+ constructor(audioContext, tempoMap, tickTimeline, destination, toAudioTime) {
591
+ this._enabled = false;
592
+ this._beatsPerBar = 4;
593
+ this._accentBuffer = null;
594
+ this._normalBuffer = null;
595
+ this._activeSources = /* @__PURE__ */ new Set();
596
+ this._audioContext = audioContext;
597
+ this._tempoMap = tempoMap;
598
+ this._tickTimeline = tickTimeline;
599
+ this._destination = destination;
600
+ this._toAudioTime = toAudioTime;
601
+ }
602
+ setEnabled(enabled) {
603
+ this._enabled = enabled;
604
+ if (!enabled) {
605
+ this.silence();
606
+ }
607
+ }
608
+ setBeatsPerBar(beats) {
609
+ this._beatsPerBar = beats;
610
+ }
611
+ setClickSounds(accent, normal) {
612
+ this._accentBuffer = accent;
613
+ this._normalBuffer = normal;
614
+ }
615
+ generate(fromTime, toTime) {
616
+ if (!this._enabled || !this._accentBuffer || !this._normalBuffer) {
617
+ return [];
618
+ }
619
+ const events = [];
620
+ const ppqn = this._tickTimeline.ppqn;
621
+ const fromTicks = this._tempoMap.secondsToTicks(fromTime);
622
+ const toTicks = this._tempoMap.secondsToTicks(toTime);
623
+ const firstBeatTick = Math.ceil(fromTicks / ppqn) * ppqn;
624
+ for (let tick = firstBeatTick; tick < toTicks; tick += ppqn) {
625
+ const transportTime = this._tempoMap.ticksToSeconds(tick);
626
+ const ticksPerBar = this._tickTimeline.ticksPerBar(this._beatsPerBar);
627
+ const isAccent = tick % ticksPerBar === 0;
628
+ events.push({
629
+ transportTime,
630
+ isAccent,
631
+ buffer: isAccent ? this._accentBuffer : this._normalBuffer
632
+ });
633
+ }
634
+ return events;
635
+ }
636
+ consume(event) {
637
+ const source = this._audioContext.createBufferSource();
638
+ source.buffer = event.buffer;
639
+ source.connect(this._destination);
640
+ this._activeSources.add(source);
641
+ source.addEventListener("ended", () => {
642
+ this._activeSources.delete(source);
643
+ try {
644
+ source.disconnect();
645
+ } catch (err) {
646
+ console.warn(
647
+ "[waveform-playlist] MetronomePlayer: error disconnecting source:",
648
+ String(err)
649
+ );
650
+ }
651
+ });
652
+ source.start(this._toAudioTime(event.transportTime));
653
+ }
654
+ onPositionJump(_newTime) {
655
+ this.silence();
656
+ }
657
+ silence() {
658
+ for (const source of this._activeSources) {
659
+ try {
660
+ source.stop();
661
+ } catch (err) {
662
+ console.warn(
663
+ "[waveform-playlist] MetronomePlayer.silence: error stopping source:",
664
+ String(err)
665
+ );
666
+ }
667
+ try {
668
+ source.disconnect();
669
+ } catch (err) {
670
+ console.warn(
671
+ "[waveform-playlist] MetronomePlayer.silence: error disconnecting:",
672
+ String(err)
673
+ );
674
+ }
675
+ }
676
+ this._activeSources.clear();
677
+ }
678
+ };
679
+
680
+ // src/transport.ts
681
+ var Transport = class _Transport {
682
+ constructor(audioContext, options = {}) {
683
+ this._trackNodes = /* @__PURE__ */ new Map();
684
+ this._tracks = [];
685
+ this._soloedTrackIds = /* @__PURE__ */ new Set();
686
+ this._mutedTrackIds = /* @__PURE__ */ new Set();
687
+ this._playing = false;
688
+ this._listeners = /* @__PURE__ */ new Map();
689
+ this._audioContext = audioContext;
690
+ const sampleRate = options.sampleRate ?? audioContext.sampleRate;
691
+ const ppqn = options.ppqn ?? 960;
692
+ const tempo = options.tempo ?? 120;
693
+ const beatsPerBar = options.beatsPerBar ?? 4;
694
+ const lookahead = options.schedulerLookahead ?? 0.2;
695
+ _Transport._validateOptions(sampleRate, ppqn, tempo, beatsPerBar, lookahead);
696
+ this._clock = new Clock(audioContext);
697
+ this._scheduler = new Scheduler({
698
+ lookahead,
699
+ onLoop: (loopStartTime) => {
700
+ this._clock.seekTo(loopStartTime);
701
+ }
702
+ });
703
+ this._sampleTimeline = new SampleTimeline(sampleRate);
704
+ this._tickTimeline = new TickTimeline(ppqn);
705
+ this._tempoMap = new TempoMap(ppqn, tempo);
706
+ this._initAudioGraph(audioContext, beatsPerBar);
707
+ this._timer = new Timer(() => {
708
+ const time = this._clock.getTime();
709
+ if (this._endTime !== void 0 && time >= this._endTime) {
710
+ this.stop();
711
+ return;
712
+ }
713
+ this._scheduler.advance(time);
714
+ });
715
+ }
716
+ get audioContext() {
717
+ return this._audioContext;
718
+ }
719
+ // --- Playback ---
720
+ play(startTime, endTime) {
721
+ if (this._playing) return;
722
+ if (startTime !== void 0) {
723
+ this._clock.seekTo(startTime);
724
+ }
725
+ const currentTime = this._clock.getTime();
726
+ this._scheduler.reset(currentTime);
727
+ this._endTime = endTime;
728
+ this._clock.start();
729
+ this._clipPlayer.onPositionJump(currentTime);
730
+ this._timer.start();
731
+ this._playing = true;
732
+ this._emit("play");
733
+ }
734
+ pause() {
735
+ if (!this._playing) return;
736
+ this._timer.stop();
737
+ this._clock.stop();
738
+ this._silenceAll();
739
+ this._playing = false;
740
+ this._emit("pause");
741
+ }
742
+ stop() {
743
+ const wasPlaying = this._playing;
744
+ this._timer.stop();
745
+ this._clock.reset();
746
+ this._scheduler.reset(0);
747
+ this._silenceAll();
748
+ this._playing = false;
749
+ this._endTime = void 0;
750
+ if (wasPlaying) {
751
+ this._emit("stop");
752
+ }
753
+ }
754
+ seek(time) {
755
+ const wasPlaying = this._playing;
756
+ if (wasPlaying) {
757
+ this._timer.stop();
758
+ }
759
+ this._silenceAll();
760
+ this._clock.seekTo(time);
761
+ this._scheduler.reset(time);
762
+ this._endTime = void 0;
763
+ if (wasPlaying) {
764
+ this._clock.start();
765
+ this._clipPlayer.onPositionJump(time);
766
+ this._timer.start();
767
+ }
768
+ }
769
+ getCurrentTime() {
770
+ return this._clock.getTime();
771
+ }
772
+ isPlaying() {
773
+ return this._playing;
774
+ }
775
+ // --- Tracks ---
776
+ setTracks(tracks) {
777
+ for (const node of this._trackNodes.values()) {
778
+ node.dispose();
779
+ }
780
+ this._trackNodes.clear();
781
+ this._soloedTrackIds.clear();
782
+ this._mutedTrackIds.clear();
783
+ this._tracks = tracks;
784
+ for (const track of tracks) {
785
+ const trackNode = new TrackNode(track.id, this._audioContext);
786
+ trackNode.setVolume(track.volume);
787
+ trackNode.setPan(track.pan);
788
+ trackNode.connectOutput(this._masterNode.input);
789
+ this._trackNodes.set(track.id, trackNode);
790
+ if (track.muted) {
791
+ this._mutedTrackIds.add(track.id);
792
+ }
793
+ if (track.soloed) {
794
+ this._soloedTrackIds.add(track.id);
795
+ }
796
+ }
797
+ this._applyMuteState();
798
+ this._clipPlayer.setTracks(tracks, this._trackNodes);
799
+ }
800
+ addTrack(track) {
801
+ const trackNode = new TrackNode(track.id, this._audioContext);
802
+ trackNode.setVolume(track.volume);
803
+ trackNode.setPan(track.pan);
804
+ trackNode.connectOutput(this._masterNode.input);
805
+ this._trackNodes.set(track.id, trackNode);
806
+ if (track.muted) {
807
+ this._mutedTrackIds.add(track.id);
808
+ }
809
+ if (track.soloed) {
810
+ this._soloedTrackIds.add(track.id);
811
+ }
812
+ this._tracks = [...this._tracks, track];
813
+ this._applyMuteState();
814
+ this._clipPlayer.setTracks(this._tracks, this._trackNodes);
815
+ }
816
+ removeTrack(trackId) {
817
+ const node = this._trackNodes.get(trackId);
818
+ if (node) {
819
+ node.dispose();
820
+ this._trackNodes.delete(trackId);
821
+ }
822
+ this._soloedTrackIds.delete(trackId);
823
+ this._mutedTrackIds.delete(trackId);
824
+ this._tracks = this._tracks.filter((t) => t.id !== trackId);
825
+ this._applyMuteState();
826
+ this._clipPlayer.setTracks(this._tracks, this._trackNodes);
827
+ }
828
+ updateTrack(trackId, track) {
829
+ this._tracks = this._tracks.map((t) => t.id === trackId ? track : t);
830
+ const node = this._trackNodes.get(trackId);
831
+ if (node) {
832
+ node.setVolume(track.volume);
833
+ node.setPan(track.pan);
834
+ }
835
+ if (track.muted) {
836
+ this._mutedTrackIds.add(trackId);
837
+ } else {
838
+ this._mutedTrackIds.delete(trackId);
839
+ }
840
+ if (track.soloed) {
841
+ this._soloedTrackIds.add(trackId);
842
+ } else {
843
+ this._soloedTrackIds.delete(trackId);
844
+ }
845
+ this._applyMuteState();
846
+ this._clipPlayer.updateTrack(trackId, track);
847
+ }
848
+ // --- Track Controls ---
849
+ setTrackVolume(trackId, volume) {
850
+ const node = this._trackNodes.get(trackId);
851
+ if (!node) {
852
+ console.warn('[waveform-playlist] setTrackVolume: unknown trackId "' + trackId + '"');
853
+ return;
854
+ }
855
+ node.setVolume(volume);
856
+ }
857
+ setTrackPan(trackId, pan) {
858
+ const node = this._trackNodes.get(trackId);
859
+ if (!node) {
860
+ console.warn('[waveform-playlist] setTrackPan: unknown trackId "' + trackId + '"');
861
+ return;
862
+ }
863
+ node.setPan(pan);
864
+ }
865
+ setTrackMute(trackId, muted) {
866
+ if (muted) {
867
+ this._mutedTrackIds.add(trackId);
868
+ } else {
869
+ this._mutedTrackIds.delete(trackId);
870
+ }
871
+ this._applyMuteState();
872
+ }
873
+ setTrackSolo(trackId, soloed) {
874
+ if (soloed) {
875
+ this._soloedTrackIds.add(trackId);
876
+ } else {
877
+ this._soloedTrackIds.delete(trackId);
878
+ }
879
+ this._applyMuteState();
880
+ }
881
+ // --- Master ---
882
+ setMasterVolume(volume) {
883
+ this._masterNode.setVolume(volume);
884
+ }
885
+ // --- Loop ---
886
+ setLoop(enabled, start, end) {
887
+ if (enabled && start >= end) {
888
+ console.warn(
889
+ "[waveform-playlist] Transport.setLoop: start (" + start + ") must be less than end (" + end + ")"
890
+ );
891
+ return;
892
+ }
893
+ this._scheduler.setLoop(enabled, start, end);
894
+ this._clipPlayer.setLoop(enabled, start, end);
895
+ this._emit("loop");
896
+ }
897
+ // --- Tempo ---
898
+ setTempo(bpm) {
899
+ this._tempoMap.setTempo(bpm);
900
+ this._emit("tempochange");
901
+ }
902
+ getTempo() {
903
+ return this._tempoMap.getTempo();
904
+ }
905
+ setBeatsPerBar(beats) {
906
+ this._metronomePlayer.setBeatsPerBar(beats);
907
+ }
908
+ // --- Metronome ---
909
+ setMetronomeEnabled(enabled) {
910
+ this._metronomePlayer.setEnabled(enabled);
911
+ }
912
+ setMetronomeClickSounds(accent, normal) {
913
+ this._metronomePlayer.setClickSounds(accent, normal);
914
+ }
915
+ // --- Effects Hook ---
916
+ connectTrackOutput(trackId, node) {
917
+ const trackNode = this._trackNodes.get(trackId);
918
+ if (!trackNode) {
919
+ console.warn('[waveform-playlist] connectTrackOutput: unknown trackId "' + trackId + '"');
920
+ return;
921
+ }
922
+ trackNode.connectEffects(node);
923
+ }
924
+ disconnectTrackOutput(trackId) {
925
+ const trackNode = this._trackNodes.get(trackId);
926
+ if (!trackNode) {
927
+ console.warn('[waveform-playlist] disconnectTrackOutput: unknown trackId "' + trackId + '"');
928
+ return;
929
+ }
930
+ trackNode.disconnectEffects();
931
+ }
932
+ // --- Events ---
933
+ on(event, cb) {
934
+ if (!this._listeners.has(event)) {
935
+ this._listeners.set(event, /* @__PURE__ */ new Set());
936
+ }
937
+ this._listeners.get(event).add(cb);
938
+ }
939
+ off(event, cb) {
940
+ this._listeners.get(event)?.delete(cb);
941
+ }
942
+ // --- Dispose ---
943
+ dispose() {
944
+ this.stop();
945
+ for (const node of this._trackNodes.values()) {
946
+ node.dispose();
947
+ }
948
+ this._trackNodes.clear();
949
+ this._masterNode.dispose();
950
+ this._listeners.clear();
951
+ }
952
+ // --- Private ---
953
+ static _validateOptions(sampleRate, ppqn, tempo, beatsPerBar, lookahead) {
954
+ if (sampleRate <= 0) {
955
+ throw new Error(
956
+ "[waveform-playlist] Transport: sampleRate must be positive, got " + sampleRate
957
+ );
958
+ }
959
+ if (ppqn <= 0 || !Number.isInteger(ppqn)) {
960
+ throw new Error(
961
+ "[waveform-playlist] Transport: ppqn must be a positive integer, got " + ppqn
962
+ );
963
+ }
964
+ if (tempo <= 0) {
965
+ throw new Error("[waveform-playlist] Transport: tempo must be positive, got " + tempo);
966
+ }
967
+ if (beatsPerBar <= 0 || !Number.isInteger(beatsPerBar)) {
968
+ throw new Error(
969
+ "[waveform-playlist] Transport: beatsPerBar must be a positive integer, got " + beatsPerBar
970
+ );
971
+ }
972
+ if (lookahead <= 0) {
973
+ throw new Error(
974
+ "[waveform-playlist] Transport: schedulerLookahead must be positive, got " + lookahead
975
+ );
976
+ }
977
+ }
978
+ _initAudioGraph(audioContext, beatsPerBar) {
979
+ this._masterNode = new MasterNode(audioContext);
980
+ this._masterNode.output.connect(audioContext.destination);
981
+ const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
982
+ this._clipPlayer = new ClipPlayer(audioContext, this._sampleTimeline, toAudioTime);
983
+ this._metronomePlayer = new MetronomePlayer(
984
+ audioContext,
985
+ this._tempoMap,
986
+ this._tickTimeline,
987
+ this._masterNode.input,
988
+ toAudioTime
989
+ );
990
+ this._metronomePlayer.setBeatsPerBar(beatsPerBar);
991
+ this._scheduler.addListener(this._clipPlayer);
992
+ this._scheduler.addListener(this._metronomePlayer);
993
+ }
994
+ _silenceAll() {
995
+ this._clipPlayer.silence();
996
+ this._metronomePlayer.silence();
997
+ }
998
+ _applyMuteState() {
999
+ const hasSolo = this._soloedTrackIds.size > 0;
1000
+ for (const [trackId, node] of this._trackNodes) {
1001
+ const isExplicitlyMuted = this._mutedTrackIds.has(trackId);
1002
+ const isSoloMuted = hasSolo && !this._soloedTrackIds.has(trackId);
1003
+ node.setMute(isExplicitlyMuted || isSoloMuted);
1004
+ }
1005
+ }
1006
+ _emit(event) {
1007
+ const listeners = this._listeners.get(event);
1008
+ if (listeners) {
1009
+ for (const cb of listeners) {
1010
+ try {
1011
+ cb();
1012
+ } catch (err) {
1013
+ console.warn(
1014
+ '[waveform-playlist] Transport "' + event + '" listener threw:',
1015
+ String(err)
1016
+ );
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ };
1022
+
1023
+ // src/adapter.ts
1024
+ var NativePlayoutAdapter = class {
1025
+ constructor(audioContext, options) {
1026
+ this._audioContext = audioContext;
1027
+ this._transport = new Transport(audioContext, options);
1028
+ }
1029
+ get transport() {
1030
+ return this._transport;
1031
+ }
1032
+ async init() {
1033
+ if (this._audioContext.state === "closed") {
1034
+ throw new Error("[waveform-playlist] Cannot init: AudioContext is closed");
1035
+ }
1036
+ if (this._audioContext.state === "suspended") {
1037
+ await this._audioContext.resume();
1038
+ }
1039
+ }
1040
+ setTracks(tracks) {
1041
+ this._transport.setTracks(tracks);
1042
+ }
1043
+ addTrack(track) {
1044
+ this._transport.addTrack(track);
1045
+ }
1046
+ removeTrack(trackId) {
1047
+ this._transport.removeTrack(trackId);
1048
+ }
1049
+ updateTrack(trackId, track) {
1050
+ this._transport.updateTrack(trackId, track);
1051
+ }
1052
+ play(startTime, endTime) {
1053
+ this._transport.play(startTime, endTime);
1054
+ }
1055
+ pause() {
1056
+ this._transport.pause();
1057
+ }
1058
+ stop() {
1059
+ this._transport.stop();
1060
+ }
1061
+ seek(time) {
1062
+ this._transport.seek(time);
1063
+ }
1064
+ getCurrentTime() {
1065
+ return this._transport.getCurrentTime();
1066
+ }
1067
+ isPlaying() {
1068
+ return this._transport.isPlaying();
1069
+ }
1070
+ setMasterVolume(volume) {
1071
+ this._transport.setMasterVolume(volume);
1072
+ }
1073
+ setTrackVolume(trackId, volume) {
1074
+ this._transport.setTrackVolume(trackId, volume);
1075
+ }
1076
+ setTrackMute(trackId, muted) {
1077
+ this._transport.setTrackMute(trackId, muted);
1078
+ }
1079
+ setTrackSolo(trackId, soloed) {
1080
+ this._transport.setTrackSolo(trackId, soloed);
1081
+ }
1082
+ setTrackPan(trackId, pan) {
1083
+ this._transport.setTrackPan(trackId, pan);
1084
+ }
1085
+ setLoop(enabled, start, end) {
1086
+ this._transport.setLoop(enabled, start, end);
1087
+ }
1088
+ dispose() {
1089
+ this._transport.dispose();
1090
+ }
1091
+ };
1092
+ // Annotate the CommonJS export names for ESM import in node:
1093
+ 0 && (module.exports = {
1094
+ ClipPlayer,
1095
+ Clock,
1096
+ MasterNode,
1097
+ MetronomePlayer,
1098
+ NativePlayoutAdapter,
1099
+ SampleTimeline,
1100
+ Scheduler,
1101
+ TempoMap,
1102
+ TickTimeline,
1103
+ Timer,
1104
+ TrackNode,
1105
+ Transport
1106
+ });
1107
+ //# sourceMappingURL=index.js.map