@dawcore/transport 0.0.2 → 0.0.3
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 +8 -4
- package/dist/index.d.mts +110 -69
- package/dist/index.d.ts +110 -69
- package/dist/index.js +203 -96
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +203 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -49,12 +49,15 @@ var Clock = class {
|
|
|
49
49
|
|
|
50
50
|
// src/core/scheduler.ts
|
|
51
51
|
var Scheduler = class {
|
|
52
|
-
constructor(options = {}) {
|
|
52
|
+
constructor(tempoMap, options = {}) {
|
|
53
53
|
this._rightEdge = 0;
|
|
54
|
+
// integer ticks
|
|
54
55
|
this._listeners = /* @__PURE__ */ new Set();
|
|
55
56
|
this._loopEnabled = false;
|
|
56
57
|
this._loopStart = 0;
|
|
58
|
+
// integer ticks
|
|
57
59
|
this._loopEnd = 0;
|
|
60
|
+
this._tempoMap = tempoMap;
|
|
58
61
|
this._lookahead = options.lookahead ?? 0.2;
|
|
59
62
|
this._onLoop = options.onLoop;
|
|
60
63
|
}
|
|
@@ -64,25 +67,40 @@ var Scheduler = class {
|
|
|
64
67
|
removeListener(listener) {
|
|
65
68
|
this._listeners.delete(listener);
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
/** Primary API — ticks as source of truth */
|
|
71
|
+
setLoop(enabled, startTick, endTick) {
|
|
72
|
+
if (enabled && (!Number.isFinite(startTick) || !Number.isFinite(endTick))) {
|
|
69
73
|
console.warn(
|
|
70
|
-
"[waveform-playlist] Scheduler.setLoop:
|
|
74
|
+
"[waveform-playlist] Scheduler.setLoop: non-finite tick values (" + startTick + ", " + endTick + ")"
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (enabled && startTick >= endTick) {
|
|
79
|
+
console.warn(
|
|
80
|
+
"[waveform-playlist] Scheduler.setLoop: startTick (" + startTick + ") must be less than endTick (" + endTick + ")"
|
|
71
81
|
);
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
74
84
|
this._loopEnabled = enabled;
|
|
75
|
-
this._loopStart =
|
|
76
|
-
this._loopEnd =
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
this._loopStart = Math.round(startTick);
|
|
86
|
+
this._loopEnd = Math.round(endTick);
|
|
87
|
+
}
|
|
88
|
+
/** Convenience — converts seconds to ticks via TempoMap */
|
|
89
|
+
setLoopSeconds(enabled, startSec, endSec) {
|
|
90
|
+
const startTick = this._tempoMap.secondsToTicks(startSec);
|
|
91
|
+
const endTick = this._tempoMap.secondsToTicks(endSec);
|
|
92
|
+
this.setLoop(enabled, startTick, endTick);
|
|
93
|
+
}
|
|
94
|
+
/** Reset scheduling cursor. Takes seconds (from Clock), converts to ticks. */
|
|
95
|
+
reset(timeSeconds) {
|
|
96
|
+
this._rightEdge = this._tempoMap.secondsToTicks(timeSeconds);
|
|
97
|
+
}
|
|
98
|
+
/** Advance the scheduling window. Takes seconds (from Clock), converts to ticks. */
|
|
99
|
+
advance(currentTimeSeconds) {
|
|
100
|
+
const targetTick = this._tempoMap.secondsToTicks(currentTimeSeconds + this._lookahead);
|
|
83
101
|
if (this._loopEnabled && this._loopEnd > this._loopStart) {
|
|
84
102
|
const loopDuration = this._loopEnd - this._loopStart;
|
|
85
|
-
let remaining =
|
|
103
|
+
let remaining = targetTick - this._rightEdge;
|
|
86
104
|
while (remaining > 0) {
|
|
87
105
|
const distToEnd = this._loopEnd - this._rightEdge;
|
|
88
106
|
if (distToEnd <= 0 || distToEnd > remaining) {
|
|
@@ -95,21 +113,25 @@ var Scheduler = class {
|
|
|
95
113
|
for (const listener of this._listeners) {
|
|
96
114
|
listener.onPositionJump(this._loopStart);
|
|
97
115
|
}
|
|
98
|
-
this._onLoop?.(
|
|
116
|
+
this._onLoop?.(
|
|
117
|
+
this._tempoMap.ticksToSeconds(this._loopStart),
|
|
118
|
+
this._tempoMap.ticksToSeconds(this._loopEnd),
|
|
119
|
+
currentTimeSeconds
|
|
120
|
+
);
|
|
99
121
|
this._rightEdge = this._loopStart;
|
|
100
122
|
if (loopDuration <= 0) break;
|
|
101
123
|
}
|
|
102
124
|
return;
|
|
103
125
|
}
|
|
104
|
-
if (
|
|
105
|
-
this._generateAndConsume(this._rightEdge,
|
|
106
|
-
this._rightEdge =
|
|
126
|
+
if (targetTick > this._rightEdge) {
|
|
127
|
+
this._generateAndConsume(this._rightEdge, targetTick);
|
|
128
|
+
this._rightEdge = targetTick;
|
|
107
129
|
}
|
|
108
130
|
}
|
|
109
|
-
_generateAndConsume(
|
|
131
|
+
_generateAndConsume(fromTick, toTick) {
|
|
110
132
|
for (const listener of this._listeners) {
|
|
111
133
|
try {
|
|
112
|
-
const events = listener.generate(
|
|
134
|
+
const events = listener.generate(fromTick, toTick);
|
|
113
135
|
for (const event of events) {
|
|
114
136
|
try {
|
|
115
137
|
listener.consume(event);
|
|
@@ -159,17 +181,37 @@ var Timer = class {
|
|
|
159
181
|
// src/timeline/sample-timeline.ts
|
|
160
182
|
var SampleTimeline = class {
|
|
161
183
|
constructor(sampleRate) {
|
|
184
|
+
this._tempoMap = null;
|
|
162
185
|
this._sampleRate = sampleRate;
|
|
163
186
|
}
|
|
164
187
|
get sampleRate() {
|
|
165
188
|
return this._sampleRate;
|
|
166
189
|
}
|
|
190
|
+
setTempoMap(tempoMap) {
|
|
191
|
+
this._tempoMap = tempoMap;
|
|
192
|
+
}
|
|
167
193
|
samplesToSeconds(samples) {
|
|
168
194
|
return samples / this._sampleRate;
|
|
169
195
|
}
|
|
170
196
|
secondsToSamples(seconds) {
|
|
171
197
|
return Math.round(seconds * this._sampleRate);
|
|
172
198
|
}
|
|
199
|
+
ticksToSamples(ticks) {
|
|
200
|
+
if (!this._tempoMap) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"[waveform-playlist] SampleTimeline: tempoMap not set \u2014 call setTempoMap() first"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return Math.round(this._tempoMap.ticksToSeconds(ticks) * this._sampleRate);
|
|
206
|
+
}
|
|
207
|
+
samplesToTicks(samples) {
|
|
208
|
+
if (!this._tempoMap) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"[waveform-playlist] SampleTimeline: tempoMap not set \u2014 call setTempoMap() first"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return this._tempoMap.secondsToTicks(samples / this._sampleRate);
|
|
214
|
+
}
|
|
173
215
|
};
|
|
174
216
|
|
|
175
217
|
// src/timeline/tempo-map.ts
|
|
@@ -216,7 +258,7 @@ var TempoMap = class {
|
|
|
216
258
|
const entry = this._entries[lo];
|
|
217
259
|
const secondsIntoSegment = seconds - entry.secondsAtTick;
|
|
218
260
|
const ticksPerSecond = entry.bpm / 60 * this._ppqn;
|
|
219
|
-
return entry.tick + secondsIntoSegment * ticksPerSecond;
|
|
261
|
+
return Math.round(entry.tick + secondsIntoSegment * ticksPerSecond);
|
|
220
262
|
}
|
|
221
263
|
beatsToSeconds(beats) {
|
|
222
264
|
return this.ticksToSeconds(beats * this._ppqn);
|
|
@@ -518,14 +560,15 @@ var TrackNode = class {
|
|
|
518
560
|
|
|
519
561
|
// src/audio/clip-player.ts
|
|
520
562
|
var ClipPlayer = class {
|
|
521
|
-
constructor(audioContext, sampleTimeline, toAudioTime) {
|
|
563
|
+
constructor(audioContext, sampleTimeline, tempoMap, toAudioTime) {
|
|
522
564
|
this._tracks = /* @__PURE__ */ new Map();
|
|
523
565
|
this._trackNodes = /* @__PURE__ */ new Map();
|
|
524
566
|
this._activeSources = /* @__PURE__ */ new Map();
|
|
525
567
|
this._loopEnabled = false;
|
|
526
|
-
this.
|
|
568
|
+
this._loopEndSamples = 0;
|
|
527
569
|
this._audioContext = audioContext;
|
|
528
570
|
this._sampleTimeline = sampleTimeline;
|
|
571
|
+
this._tempoMap = tempoMap;
|
|
529
572
|
this._toAudioTime = toAudioTime;
|
|
530
573
|
}
|
|
531
574
|
setTracks(tracks, trackNodes) {
|
|
@@ -535,41 +578,50 @@ var ClipPlayer = class {
|
|
|
535
578
|
this._tracks.set(track.id, { track, clips: track.clips });
|
|
536
579
|
}
|
|
537
580
|
}
|
|
538
|
-
|
|
581
|
+
/** Set loop region using ticks. startTick is unused — loop clamping only needs
|
|
582
|
+
* the end boundary; mid-clip restart at loopStart is handled by onPositionJump. */
|
|
583
|
+
setLoop(enabled, _startTick, endTick) {
|
|
539
584
|
this._loopEnabled = enabled;
|
|
540
|
-
this.
|
|
585
|
+
this._loopEndSamples = this._sampleTimeline.ticksToSamples(endTick);
|
|
586
|
+
}
|
|
587
|
+
/** Set loop region using samples directly */
|
|
588
|
+
setLoopSamples(enabled, _startSample, endSample) {
|
|
589
|
+
this._loopEnabled = enabled;
|
|
590
|
+
this._loopEndSamples = endSample;
|
|
541
591
|
}
|
|
542
592
|
updateTrack(trackId, track) {
|
|
543
593
|
this._tracks.set(trackId, { track, clips: track.clips });
|
|
544
594
|
this._silenceTrack(trackId);
|
|
545
595
|
}
|
|
546
|
-
generate(
|
|
596
|
+
generate(fromTick, toTick) {
|
|
547
597
|
const events = [];
|
|
598
|
+
const fromSample = this._sampleTimeline.ticksToSamples(fromTick);
|
|
599
|
+
const toSample = this._sampleTimeline.ticksToSamples(toTick);
|
|
548
600
|
for (const [trackId, state] of this._tracks) {
|
|
549
601
|
for (const clip of state.clips) {
|
|
550
602
|
if (clip.durationSamples === 0) continue;
|
|
551
603
|
if (!clip.audioBuffer) continue;
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (this._loopEnabled && clipStartTime + duration > this._loopEnd) {
|
|
561
|
-
duration = this._loopEnd - clipStartTime;
|
|
604
|
+
const clipStartSample = clip.startSample;
|
|
605
|
+
if (clipStartSample < fromSample) continue;
|
|
606
|
+
if (clipStartSample >= toSample) continue;
|
|
607
|
+
const fadeInDurationSamples = clip.fadeIn ? clip.fadeIn.duration ?? 0 : 0;
|
|
608
|
+
const fadeOutDurationSamples = clip.fadeOut ? clip.fadeOut.duration ?? 0 : 0;
|
|
609
|
+
let durationSamples = clip.durationSamples;
|
|
610
|
+
if (this._loopEnabled && clipStartSample + durationSamples > this._loopEndSamples) {
|
|
611
|
+
durationSamples = this._loopEndSamples - clipStartSample;
|
|
562
612
|
}
|
|
613
|
+
const clipTick = this._sampleTimeline.samplesToTicks(clipStartSample);
|
|
563
614
|
events.push({
|
|
564
615
|
trackId,
|
|
565
616
|
clipId: clip.id,
|
|
566
617
|
audioBuffer: clip.audioBuffer,
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
618
|
+
tick: clipTick,
|
|
619
|
+
startSample: clipStartSample,
|
|
620
|
+
offsetSamples: clip.offsetSamples,
|
|
621
|
+
durationSamples,
|
|
570
622
|
gain: clip.gain,
|
|
571
|
-
|
|
572
|
-
|
|
623
|
+
fadeInDurationSamples,
|
|
624
|
+
fadeOutDurationSamples
|
|
573
625
|
});
|
|
574
626
|
}
|
|
575
627
|
}
|
|
@@ -583,18 +635,25 @@ var ClipPlayer = class {
|
|
|
583
635
|
);
|
|
584
636
|
return;
|
|
585
637
|
}
|
|
586
|
-
|
|
638
|
+
const sampleRate = this._sampleTimeline.sampleRate;
|
|
639
|
+
const offsetSeconds = event.offsetSamples / sampleRate;
|
|
640
|
+
const durationSeconds = event.durationSamples / sampleRate;
|
|
641
|
+
if (offsetSeconds >= event.audioBuffer.duration) {
|
|
642
|
+
console.warn(
|
|
643
|
+
"[waveform-playlist] ClipPlayer.consume: offset (" + offsetSeconds + "s) exceeds audioBuffer.duration (" + event.audioBuffer.duration + 's) for clipId "' + event.clipId + '" \u2014 clip will not play'
|
|
644
|
+
);
|
|
587
645
|
return;
|
|
588
646
|
}
|
|
589
647
|
const source = this._audioContext.createBufferSource();
|
|
590
648
|
source.buffer = event.audioBuffer;
|
|
591
|
-
const
|
|
649
|
+
const transportSeconds = this._tempoMap.ticksToSeconds(event.tick);
|
|
650
|
+
const when = this._toAudioTime(transportSeconds);
|
|
592
651
|
const gainNode = this._audioContext.createGain();
|
|
593
652
|
gainNode.gain.value = event.gain;
|
|
594
|
-
let fadeIn = event.
|
|
595
|
-
let fadeOut = event.
|
|
596
|
-
if (fadeIn + fadeOut >
|
|
597
|
-
const ratio =
|
|
653
|
+
let fadeIn = event.fadeInDurationSamples / sampleRate;
|
|
654
|
+
let fadeOut = event.fadeOutDurationSamples / sampleRate;
|
|
655
|
+
if (fadeIn + fadeOut > durationSeconds) {
|
|
656
|
+
const ratio = durationSeconds / (fadeIn + fadeOut);
|
|
598
657
|
fadeIn *= ratio;
|
|
599
658
|
fadeOut *= ratio;
|
|
600
659
|
}
|
|
@@ -603,9 +662,9 @@ var ClipPlayer = class {
|
|
|
603
662
|
gainNode.gain.linearRampToValueAtTime(event.gain, when + fadeIn);
|
|
604
663
|
}
|
|
605
664
|
if (fadeOut > 0) {
|
|
606
|
-
const fadeOutStart = when +
|
|
665
|
+
const fadeOutStart = when + durationSeconds - fadeOut;
|
|
607
666
|
gainNode.gain.setValueAtTime(event.gain, fadeOutStart);
|
|
608
|
-
gainNode.gain.linearRampToValueAtTime(0, when +
|
|
667
|
+
gainNode.gain.linearRampToValueAtTime(0, when + durationSeconds);
|
|
609
668
|
}
|
|
610
669
|
source.connect(gainNode);
|
|
611
670
|
gainNode.connect(trackNode.input);
|
|
@@ -621,33 +680,37 @@ var ClipPlayer = class {
|
|
|
621
680
|
console.warn("[waveform-playlist] ClipPlayer: error disconnecting gain node:", String(err));
|
|
622
681
|
}
|
|
623
682
|
});
|
|
624
|
-
source.start(when,
|
|
683
|
+
source.start(when, offsetSeconds, durationSeconds);
|
|
625
684
|
}
|
|
626
|
-
onPositionJump(
|
|
685
|
+
onPositionJump(newTick) {
|
|
627
686
|
this.silence();
|
|
687
|
+
const newSample = this._sampleTimeline.ticksToSamples(newTick);
|
|
628
688
|
for (const [trackId, state] of this._tracks) {
|
|
629
689
|
for (const clip of state.clips) {
|
|
630
690
|
if (clip.durationSamples === 0) continue;
|
|
631
691
|
if (!clip.audioBuffer) continue;
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
692
|
+
const clipStartSample = clip.startSample;
|
|
693
|
+
const clipEndSample = clipStartSample + clip.durationSamples;
|
|
694
|
+
if (clipStartSample <= newSample && clipEndSample > newSample) {
|
|
695
|
+
const offsetIntoClipSamples = newSample - clipStartSample;
|
|
696
|
+
const offsetSamples = clip.offsetSamples + offsetIntoClipSamples;
|
|
697
|
+
let durationSamples = clipEndSample - newSample;
|
|
698
|
+
if (this._loopEnabled && newSample + durationSamples > this._loopEndSamples) {
|
|
699
|
+
durationSamples = this._loopEndSamples - newSample;
|
|
700
|
+
}
|
|
701
|
+
if (durationSamples <= 0) continue;
|
|
702
|
+
const fadeOutDurationSamples = clip.fadeOut ? clip.fadeOut.duration ?? 0 : 0;
|
|
641
703
|
this.consume({
|
|
642
704
|
trackId,
|
|
643
705
|
clipId: clip.id,
|
|
644
706
|
audioBuffer: clip.audioBuffer,
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
707
|
+
tick: newTick,
|
|
708
|
+
startSample: newSample,
|
|
709
|
+
offsetSamples,
|
|
710
|
+
durationSamples,
|
|
648
711
|
gain: clip.gain,
|
|
649
|
-
|
|
650
|
-
|
|
712
|
+
fadeInDurationSamples: 0,
|
|
713
|
+
fadeOutDurationSamples
|
|
651
714
|
});
|
|
652
715
|
}
|
|
653
716
|
}
|
|
@@ -720,31 +783,29 @@ var MetronomePlayer = class {
|
|
|
720
783
|
this._accentBuffer = accent;
|
|
721
784
|
this._normalBuffer = normal;
|
|
722
785
|
}
|
|
723
|
-
generate(
|
|
786
|
+
generate(fromTick, toTick) {
|
|
724
787
|
if (!this._enabled || !this._accentBuffer || !this._normalBuffer) {
|
|
725
788
|
return [];
|
|
726
789
|
}
|
|
727
790
|
const events = [];
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
let beatSize = this._meterMap.ticksPerBeat(fromTicks);
|
|
732
|
-
const tickIntoSection = fromTicks - entry.tick;
|
|
791
|
+
let entry = this._meterMap.getEntryAt(fromTick);
|
|
792
|
+
let beatSize = this._meterMap.ticksPerBeat(fromTick);
|
|
793
|
+
const tickIntoSection = fromTick - entry.tick;
|
|
733
794
|
let tick = entry.tick + Math.ceil(tickIntoSection / beatSize) * beatSize;
|
|
734
|
-
while (tick <
|
|
735
|
-
const
|
|
795
|
+
while (tick < toTick) {
|
|
796
|
+
const tickPos = tick;
|
|
797
|
+
const currentEntry = this._meterMap.getEntryAt(tickPos);
|
|
736
798
|
if (currentEntry.tick !== entry.tick) {
|
|
737
799
|
entry = currentEntry;
|
|
738
|
-
beatSize = this._meterMap.ticksPerBeat(
|
|
800
|
+
beatSize = this._meterMap.ticksPerBeat(tickPos);
|
|
739
801
|
}
|
|
740
|
-
const isAccent = this._meterMap.isBarBoundary(
|
|
741
|
-
const transportTime = this._tempoMap.ticksToSeconds(tick);
|
|
802
|
+
const isAccent = this._meterMap.isBarBoundary(tickPos);
|
|
742
803
|
events.push({
|
|
743
|
-
|
|
804
|
+
tick: tickPos,
|
|
744
805
|
isAccent,
|
|
745
806
|
buffer: isAccent ? this._accentBuffer : this._normalBuffer
|
|
746
807
|
});
|
|
747
|
-
beatSize = this._meterMap.ticksPerBeat(
|
|
808
|
+
beatSize = this._meterMap.ticksPerBeat(tickPos);
|
|
748
809
|
tick += beatSize;
|
|
749
810
|
}
|
|
750
811
|
return events;
|
|
@@ -765,10 +826,10 @@ var MetronomePlayer = class {
|
|
|
765
826
|
);
|
|
766
827
|
}
|
|
767
828
|
});
|
|
768
|
-
|
|
829
|
+
const transportTime = this._tempoMap.ticksToSeconds(event.tick);
|
|
830
|
+
source.start(this._toAudioTime(transportTime));
|
|
769
831
|
}
|
|
770
|
-
onPositionJump(
|
|
771
|
-
this.silence();
|
|
832
|
+
onPositionJump(_newTick) {
|
|
772
833
|
}
|
|
773
834
|
silence() {
|
|
774
835
|
for (const source of this._activeSources) {
|
|
@@ -801,6 +862,8 @@ var Transport = class _Transport {
|
|
|
801
862
|
this._soloedTrackIds = /* @__PURE__ */ new Set();
|
|
802
863
|
this._mutedTrackIds = /* @__PURE__ */ new Set();
|
|
803
864
|
this._playing = false;
|
|
865
|
+
this._loopEnabled = false;
|
|
866
|
+
this._loopStartSeconds = 0;
|
|
804
867
|
this._listeners = /* @__PURE__ */ new Map();
|
|
805
868
|
this._audioContext = audioContext;
|
|
806
869
|
const sampleRate = options.sampleRate ?? audioContext.sampleRate;
|
|
@@ -811,15 +874,17 @@ var Transport = class _Transport {
|
|
|
811
874
|
const lookahead = options.schedulerLookahead ?? 0.2;
|
|
812
875
|
_Transport._validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead);
|
|
813
876
|
this._clock = new Clock(audioContext);
|
|
814
|
-
this._scheduler = new Scheduler({
|
|
815
|
-
lookahead,
|
|
816
|
-
onLoop: (loopStartTime) => {
|
|
817
|
-
this._clock.seekTo(loopStartTime);
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
877
|
this._sampleTimeline = new SampleTimeline(sampleRate);
|
|
821
878
|
this._meterMap = new MeterMap(ppqn, numerator, denominator);
|
|
822
879
|
this._tempoMap = new TempoMap(ppqn, tempo);
|
|
880
|
+
this._scheduler = new Scheduler(this._tempoMap, {
|
|
881
|
+
lookahead,
|
|
882
|
+
onLoop: (loopStartSeconds, loopEndSeconds, currentTimeSeconds) => {
|
|
883
|
+
const timeToBoundary = loopEndSeconds - currentTimeSeconds;
|
|
884
|
+
this._clock.seekTo(loopStartSeconds - timeToBoundary);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
this._sampleTimeline.setTempoMap(this._tempoMap);
|
|
823
888
|
this._initAudioGraph(audioContext);
|
|
824
889
|
this._timer = new Timer(() => {
|
|
825
890
|
const time = this._clock.getTime();
|
|
@@ -843,7 +908,8 @@ var Transport = class _Transport {
|
|
|
843
908
|
this._scheduler.reset(currentTime);
|
|
844
909
|
this._endTime = endTime;
|
|
845
910
|
this._clock.start();
|
|
846
|
-
this.
|
|
911
|
+
const currentTick = this._tempoMap.secondsToTicks(currentTime);
|
|
912
|
+
this._clipPlayer.onPositionJump(currentTick);
|
|
847
913
|
this._timer.start();
|
|
848
914
|
this._playing = true;
|
|
849
915
|
this._emit("play");
|
|
@@ -879,12 +945,17 @@ var Transport = class _Transport {
|
|
|
879
945
|
this._endTime = void 0;
|
|
880
946
|
if (wasPlaying) {
|
|
881
947
|
this._clock.start();
|
|
882
|
-
this.
|
|
948
|
+
const seekTick = this._tempoMap.secondsToTicks(time);
|
|
949
|
+
this._clipPlayer.onPositionJump(seekTick);
|
|
883
950
|
this._timer.start();
|
|
884
951
|
}
|
|
885
952
|
}
|
|
886
953
|
getCurrentTime() {
|
|
887
|
-
|
|
954
|
+
const t = this._clock.getTime();
|
|
955
|
+
if (this._loopEnabled && t < this._loopStartSeconds) {
|
|
956
|
+
return this._loopStartSeconds;
|
|
957
|
+
}
|
|
958
|
+
return t;
|
|
888
959
|
}
|
|
889
960
|
isPlaying() {
|
|
890
961
|
return this._playing;
|
|
@@ -1000,15 +1071,46 @@ var Transport = class _Transport {
|
|
|
1000
1071
|
this._masterNode.setVolume(volume);
|
|
1001
1072
|
}
|
|
1002
1073
|
// --- Loop ---
|
|
1003
|
-
|
|
1004
|
-
|
|
1074
|
+
/** Primary loop API — ticks as source of truth */
|
|
1075
|
+
setLoop(enabled, startTick, endTick) {
|
|
1076
|
+
if (enabled && startTick >= endTick) {
|
|
1077
|
+
console.warn(
|
|
1078
|
+
"[waveform-playlist] Transport.setLoop: startTick (" + startTick + ") must be less than endTick (" + endTick + ")"
|
|
1079
|
+
);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
this._loopEnabled = enabled;
|
|
1083
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(startTick);
|
|
1084
|
+
this._scheduler.setLoop(enabled, startTick, endTick);
|
|
1085
|
+
this._clipPlayer.setLoop(enabled, startTick, endTick);
|
|
1086
|
+
this._emit("loop");
|
|
1087
|
+
}
|
|
1088
|
+
/** Convenience — converts seconds to ticks */
|
|
1089
|
+
setLoopSeconds(enabled, startSec, endSec) {
|
|
1090
|
+
const startTick = this._tempoMap.secondsToTicks(startSec);
|
|
1091
|
+
const endTick = this._tempoMap.secondsToTicks(endSec);
|
|
1092
|
+
this.setLoop(enabled, startTick, endTick);
|
|
1093
|
+
}
|
|
1094
|
+
/** Convenience — sets loop in samples */
|
|
1095
|
+
setLoopSamples(enabled, startSample, endSample) {
|
|
1096
|
+
if (enabled && (!Number.isFinite(startSample) || !Number.isFinite(endSample))) {
|
|
1005
1097
|
console.warn(
|
|
1006
|
-
"[waveform-playlist] Transport.
|
|
1098
|
+
"[waveform-playlist] Transport.setLoopSamples: non-finite sample values (" + startSample + ", " + endSample + ")"
|
|
1007
1099
|
);
|
|
1008
1100
|
return;
|
|
1009
1101
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1102
|
+
if (enabled && startSample >= endSample) {
|
|
1103
|
+
console.warn(
|
|
1104
|
+
"[waveform-playlist] Transport.setLoopSamples: startSample (" + startSample + ") must be less than endSample (" + endSample + ")"
|
|
1105
|
+
);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const startTick = this._sampleTimeline.samplesToTicks(startSample);
|
|
1109
|
+
const endTick = this._sampleTimeline.samplesToTicks(endSample);
|
|
1110
|
+
this._loopEnabled = enabled;
|
|
1111
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(startTick);
|
|
1112
|
+
this._clipPlayer.setLoopSamples(enabled, startSample, endSample);
|
|
1113
|
+
this._scheduler.setLoop(enabled, startTick, endTick);
|
|
1012
1114
|
this._emit("loop");
|
|
1013
1115
|
}
|
|
1014
1116
|
// --- Tempo ---
|
|
@@ -1132,7 +1234,12 @@ var Transport = class _Transport {
|
|
|
1132
1234
|
this._masterNode = new MasterNode(audioContext);
|
|
1133
1235
|
this._masterNode.output.connect(audioContext.destination);
|
|
1134
1236
|
const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
|
|
1135
|
-
this._clipPlayer = new ClipPlayer(
|
|
1237
|
+
this._clipPlayer = new ClipPlayer(
|
|
1238
|
+
audioContext,
|
|
1239
|
+
this._sampleTimeline,
|
|
1240
|
+
this._tempoMap,
|
|
1241
|
+
toAudioTime
|
|
1242
|
+
);
|
|
1136
1243
|
this._metronomePlayer = new MetronomePlayer(
|
|
1137
1244
|
audioContext,
|
|
1138
1245
|
this._tempoMap,
|
|
@@ -1235,7 +1342,7 @@ var NativePlayoutAdapter = class {
|
|
|
1235
1342
|
this._transport.setTrackPan(trackId, pan);
|
|
1236
1343
|
}
|
|
1237
1344
|
setLoop(enabled, start, end) {
|
|
1238
|
-
this._transport.
|
|
1345
|
+
this._transport.setLoopSeconds(enabled, start, end);
|
|
1239
1346
|
}
|
|
1240
1347
|
dispose() {
|
|
1241
1348
|
this._transport.dispose();
|