@dawcore/transport 0.0.4 → 0.0.6
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 +43 -4
- package/dist/index.d.mts +80 -3
- package/dist/index.d.ts +80 -3
- package/dist/index.js +365 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +365 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -173,7 +173,9 @@ var Timer = class {
|
|
|
173
173
|
} catch (err) {
|
|
174
174
|
console.warn("[waveform-playlist] Timer tick error:", String(err));
|
|
175
175
|
}
|
|
176
|
-
this.
|
|
176
|
+
if (this._running) {
|
|
177
|
+
this._scheduleFrame();
|
|
178
|
+
}
|
|
177
179
|
});
|
|
178
180
|
}
|
|
179
181
|
};
|
|
@@ -850,7 +852,7 @@ var ClipPlayer = class {
|
|
|
850
852
|
if (!clip.audioBuffer) continue;
|
|
851
853
|
const clipStartSample = clip.startSample;
|
|
852
854
|
const clipEndSample = clipStartSample + clip.durationSamples;
|
|
853
|
-
if (clipStartSample
|
|
855
|
+
if (clipStartSample < newSample && clipEndSample > newSample) {
|
|
854
856
|
const offsetIntoClipSamples = newSample - clipStartSample;
|
|
855
857
|
const offsetSamples = clip.offsetSamples + offsetIntoClipSamples;
|
|
856
858
|
let durationSamples = clipEndSample - newSample;
|
|
@@ -1013,8 +1015,145 @@ var MetronomePlayer = class {
|
|
|
1013
1015
|
}
|
|
1014
1016
|
};
|
|
1015
1017
|
|
|
1018
|
+
// src/audio/count-in-player.ts
|
|
1019
|
+
var CountInPlayer = class {
|
|
1020
|
+
constructor(audioContext, tempoMap, destination, toAudioTime) {
|
|
1021
|
+
this._activeSources = /* @__PURE__ */ new Set();
|
|
1022
|
+
this._totalBeats = 0;
|
|
1023
|
+
this._beatsGenerated = 0;
|
|
1024
|
+
this._accentBuffer = null;
|
|
1025
|
+
this._normalBuffer = null;
|
|
1026
|
+
this._meterMap = null;
|
|
1027
|
+
this._onBeat = null;
|
|
1028
|
+
this._audioContext = audioContext;
|
|
1029
|
+
this._tempoMap = tempoMap;
|
|
1030
|
+
this._destination = destination;
|
|
1031
|
+
this._toAudioTime = toAudioTime;
|
|
1032
|
+
}
|
|
1033
|
+
configure(config) {
|
|
1034
|
+
this._totalBeats = config.totalBeats;
|
|
1035
|
+
this._beatsGenerated = 0;
|
|
1036
|
+
this._accentBuffer = config.accentBuffer;
|
|
1037
|
+
this._normalBuffer = config.normalBuffer;
|
|
1038
|
+
this._meterMap = config.meterMap;
|
|
1039
|
+
this._tempoMap = config.tempoMap;
|
|
1040
|
+
this._onBeat = config.onBeat;
|
|
1041
|
+
}
|
|
1042
|
+
generate(fromTick, toTick) {
|
|
1043
|
+
if (!this._accentBuffer || !this._normalBuffer || !this._meterMap) {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
const events = [];
|
|
1047
|
+
const meterMap = this._meterMap;
|
|
1048
|
+
let entry = meterMap.getEntryAt(fromTick);
|
|
1049
|
+
let beatSize = meterMap.ticksPerBeat(fromTick);
|
|
1050
|
+
const tickIntoSection = fromTick - entry.tick;
|
|
1051
|
+
let tick = entry.tick + Math.ceil(tickIntoSection / beatSize) * beatSize;
|
|
1052
|
+
while (tick < toTick && this._beatsGenerated < this._totalBeats) {
|
|
1053
|
+
const tickPos = tick;
|
|
1054
|
+
const currentEntry = meterMap.getEntryAt(tickPos);
|
|
1055
|
+
if (currentEntry.tick !== entry.tick) {
|
|
1056
|
+
entry = currentEntry;
|
|
1057
|
+
beatSize = meterMap.ticksPerBeat(tickPos);
|
|
1058
|
+
}
|
|
1059
|
+
this._beatsGenerated++;
|
|
1060
|
+
const isAccent = meterMap.isBarBoundary(tickPos);
|
|
1061
|
+
events.push({
|
|
1062
|
+
tick: tickPos,
|
|
1063
|
+
isAccent,
|
|
1064
|
+
buffer: isAccent ? this._accentBuffer : this._normalBuffer,
|
|
1065
|
+
beat: this._beatsGenerated,
|
|
1066
|
+
totalBeats: this._totalBeats
|
|
1067
|
+
});
|
|
1068
|
+
beatSize = meterMap.ticksPerBeat(tickPos);
|
|
1069
|
+
tick += beatSize;
|
|
1070
|
+
}
|
|
1071
|
+
return events;
|
|
1072
|
+
}
|
|
1073
|
+
consume(event) {
|
|
1074
|
+
try {
|
|
1075
|
+
const source = this._audioContext.createBufferSource();
|
|
1076
|
+
source.buffer = event.buffer;
|
|
1077
|
+
source.connect(this._destination);
|
|
1078
|
+
this._activeSources.add(source);
|
|
1079
|
+
source.addEventListener("ended", () => {
|
|
1080
|
+
this._activeSources.delete(source);
|
|
1081
|
+
try {
|
|
1082
|
+
source.disconnect();
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
console.warn(
|
|
1085
|
+
"[waveform-playlist] CountInPlayer: error disconnecting source:",
|
|
1086
|
+
String(err)
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
const transportTime = this._tempoMap.ticksToSeconds(event.tick);
|
|
1091
|
+
source.start(this._toAudioTime(transportTime));
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
console.warn(
|
|
1094
|
+
"[waveform-playlist] CountInPlayer.consume: failed to schedule beat " + event.beat + "/" + event.totalBeats + ": " + String(err)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
this._onBeat?.(event.beat, event.totalBeats);
|
|
1098
|
+
}
|
|
1099
|
+
onPositionJump(_newTick) {
|
|
1100
|
+
}
|
|
1101
|
+
silence() {
|
|
1102
|
+
for (const source of this._activeSources) {
|
|
1103
|
+
try {
|
|
1104
|
+
source.stop();
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
console.warn(
|
|
1107
|
+
"[waveform-playlist] CountInPlayer.silence: error stopping source:",
|
|
1108
|
+
String(err)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
source.disconnect();
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
console.warn(
|
|
1115
|
+
"[waveform-playlist] CountInPlayer.silence: error disconnecting:",
|
|
1116
|
+
String(err)
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
this._activeSources.clear();
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// src/audio/click-sounds.ts
|
|
1125
|
+
var DEFAULT_ACCENT_FREQUENCY = 1e3;
|
|
1126
|
+
var DEFAULT_NORMAL_FREQUENCY = 800;
|
|
1127
|
+
var ACCENT_DURATION = 0.04;
|
|
1128
|
+
var NORMAL_DURATION = 0.03;
|
|
1129
|
+
function synthesizeClick(audioContext, frequency, duration) {
|
|
1130
|
+
const sampleRate = audioContext.sampleRate;
|
|
1131
|
+
const length = Math.ceil(sampleRate * duration);
|
|
1132
|
+
const buffer = audioContext.createBuffer(1, length, sampleRate);
|
|
1133
|
+
const data = buffer.getChannelData(0);
|
|
1134
|
+
for (let i = 0; i < length; i++) {
|
|
1135
|
+
const t = i / sampleRate;
|
|
1136
|
+
const envelope = Math.exp(-t * 50);
|
|
1137
|
+
data[i] = Math.sin(2 * Math.PI * frequency * t) * envelope;
|
|
1138
|
+
}
|
|
1139
|
+
return buffer;
|
|
1140
|
+
}
|
|
1141
|
+
function createDefaultClickSounds(audioContext, options) {
|
|
1142
|
+
const accentFreq = options?.accentFrequency ?? DEFAULT_ACCENT_FREQUENCY;
|
|
1143
|
+
const normalFreq = options?.normalFrequency ?? DEFAULT_NORMAL_FREQUENCY;
|
|
1144
|
+
if (accentFreq <= 0 || normalFreq <= 0) {
|
|
1145
|
+
console.warn(
|
|
1146
|
+
"[waveform-playlist] createDefaultClickSounds: frequency must be positive, got accent=" + accentFreq + " normal=" + normalFreq
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
accent: synthesizeClick(audioContext, accentFreq, ACCENT_DURATION),
|
|
1151
|
+
normal: synthesizeClick(audioContext, normalFreq, NORMAL_DURATION)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1016
1155
|
// src/transport.ts
|
|
1017
|
-
var
|
|
1156
|
+
var _Transport = class _Transport {
|
|
1018
1157
|
constructor(audioContext, options = {}) {
|
|
1019
1158
|
this._trackNodes = /* @__PURE__ */ new Map();
|
|
1020
1159
|
this._tracks = [];
|
|
@@ -1024,7 +1163,22 @@ var Transport = class _Transport {
|
|
|
1024
1163
|
this._loopEnabled = false;
|
|
1025
1164
|
this._loopStartTick = 0;
|
|
1026
1165
|
this._loopStartSeconds = 0;
|
|
1166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1027
1167
|
this._listeners = /* @__PURE__ */ new Map();
|
|
1168
|
+
// --- Count-In state ---
|
|
1169
|
+
this._countInEnabled = false;
|
|
1170
|
+
this._countInBars = 1;
|
|
1171
|
+
this._countInMode = "recording-only";
|
|
1172
|
+
this._recording = false;
|
|
1173
|
+
this._countingIn = false;
|
|
1174
|
+
this._countInStartPosition = 0;
|
|
1175
|
+
this._countInDuration = 0;
|
|
1176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1177
|
+
this._countInScheduler = null;
|
|
1178
|
+
this._accentBuffer = null;
|
|
1179
|
+
this._normalBuffer = null;
|
|
1180
|
+
this._schedulerLookahead = 0.2;
|
|
1181
|
+
this._ppqn = 960;
|
|
1028
1182
|
this._audioContext = audioContext;
|
|
1029
1183
|
const sampleRate = options.sampleRate ?? audioContext.sampleRate;
|
|
1030
1184
|
const ppqn = options.ppqn ?? 960;
|
|
@@ -1032,6 +1186,8 @@ var Transport = class _Transport {
|
|
|
1032
1186
|
const numerator = options.numerator ?? 4;
|
|
1033
1187
|
const denominator = options.denominator ?? 4;
|
|
1034
1188
|
const lookahead = options.schedulerLookahead ?? 0.2;
|
|
1189
|
+
this._ppqn = ppqn;
|
|
1190
|
+
this._schedulerLookahead = lookahead;
|
|
1035
1191
|
_Transport._validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead);
|
|
1036
1192
|
this._clock = new Clock(audioContext);
|
|
1037
1193
|
this._sampleTimeline = new SampleTimeline(sampleRate);
|
|
@@ -1046,8 +1202,16 @@ var Transport = class _Transport {
|
|
|
1046
1202
|
});
|
|
1047
1203
|
this._sampleTimeline.setTempoMap(this._tempoMap);
|
|
1048
1204
|
this._initAudioGraph(audioContext);
|
|
1205
|
+
this._initCountIn(audioContext, options);
|
|
1049
1206
|
this._timer = new Timer(() => {
|
|
1050
1207
|
const time = this._clock.getTime();
|
|
1208
|
+
if (this._countingIn) {
|
|
1209
|
+
this._countInScheduler?.advance(time);
|
|
1210
|
+
if (time >= this._countInDuration) {
|
|
1211
|
+
this._finishCountIn();
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1051
1215
|
if (this._endTime !== void 0 && time >= this._endTime) {
|
|
1052
1216
|
this.stop();
|
|
1053
1217
|
return;
|
|
@@ -1064,6 +1228,10 @@ var Transport = class _Transport {
|
|
|
1064
1228
|
if (startTime !== void 0) {
|
|
1065
1229
|
this._clock.seekTo(startTime);
|
|
1066
1230
|
}
|
|
1231
|
+
if (this._shouldCountIn()) {
|
|
1232
|
+
this._startCountIn(endTime);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1067
1235
|
const currentTime = this._clock.getTime();
|
|
1068
1236
|
this._scheduler.reset(currentTime);
|
|
1069
1237
|
this._endTime = endTime;
|
|
@@ -1076,6 +1244,13 @@ var Transport = class _Transport {
|
|
|
1076
1244
|
}
|
|
1077
1245
|
pause() {
|
|
1078
1246
|
if (!this._playing) return;
|
|
1247
|
+
if (this._countingIn) {
|
|
1248
|
+
this._cancelCountIn();
|
|
1249
|
+
this._clock.stop();
|
|
1250
|
+
this._playing = false;
|
|
1251
|
+
this._emit("pause");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1079
1254
|
this._timer.stop();
|
|
1080
1255
|
this._clock.stop();
|
|
1081
1256
|
this._silenceAll();
|
|
@@ -1084,6 +1259,9 @@ var Transport = class _Transport {
|
|
|
1084
1259
|
}
|
|
1085
1260
|
stop() {
|
|
1086
1261
|
const wasPlaying = this._playing;
|
|
1262
|
+
if (this._countingIn) {
|
|
1263
|
+
this._cancelCountIn();
|
|
1264
|
+
}
|
|
1087
1265
|
this._timer.stop();
|
|
1088
1266
|
this._clock.reset();
|
|
1089
1267
|
this._scheduler.reset(0);
|
|
@@ -1096,14 +1274,19 @@ var Transport = class _Transport {
|
|
|
1096
1274
|
}
|
|
1097
1275
|
seek(time) {
|
|
1098
1276
|
const wasPlaying = this._playing;
|
|
1099
|
-
|
|
1277
|
+
const wasCountingIn = this._countingIn;
|
|
1278
|
+
if (wasCountingIn) {
|
|
1279
|
+
this._cancelCountIn();
|
|
1280
|
+
this._playing = false;
|
|
1281
|
+
}
|
|
1282
|
+
if (wasPlaying && !wasCountingIn) {
|
|
1100
1283
|
this._timer.stop();
|
|
1101
1284
|
}
|
|
1102
1285
|
this._silenceAll();
|
|
1103
1286
|
this._clock.seekTo(time);
|
|
1104
1287
|
this._scheduler.reset(time);
|
|
1105
1288
|
this._endTime = void 0;
|
|
1106
|
-
if (wasPlaying) {
|
|
1289
|
+
if (wasPlaying && !wasCountingIn) {
|
|
1107
1290
|
this._clock.start();
|
|
1108
1291
|
const seekTick = this._tempoMap.secondsToTicks(time);
|
|
1109
1292
|
this._clipPlayer.onPositionJump(seekTick);
|
|
@@ -1111,6 +1294,9 @@ var Transport = class _Transport {
|
|
|
1111
1294
|
}
|
|
1112
1295
|
}
|
|
1113
1296
|
getCurrentTime() {
|
|
1297
|
+
if (this._countingIn) {
|
|
1298
|
+
return this._countInStartPosition;
|
|
1299
|
+
}
|
|
1114
1300
|
const t = this._clock.getTime();
|
|
1115
1301
|
if (this._loopEnabled && t < this._loopStartSeconds) {
|
|
1116
1302
|
return this._loopStartSeconds;
|
|
@@ -1281,7 +1467,7 @@ var Transport = class _Transport {
|
|
|
1281
1467
|
if (this._loopEnabled) {
|
|
1282
1468
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1283
1469
|
}
|
|
1284
|
-
this._emit("tempochange");
|
|
1470
|
+
this._emit("tempochange", { bpm, atTick: atTick ?? 0 });
|
|
1285
1471
|
}
|
|
1286
1472
|
getTempo(atTick) {
|
|
1287
1473
|
return this._tempoMap.getTempo(atTick);
|
|
@@ -1289,25 +1475,35 @@ var Transport = class _Transport {
|
|
|
1289
1475
|
// --- Meter ---
|
|
1290
1476
|
setMeter(numerator, denominator, atTick) {
|
|
1291
1477
|
this._meterMap.setMeter(numerator, denominator, atTick);
|
|
1292
|
-
this._emit("meterchange");
|
|
1478
|
+
this._emit("meterchange", { numerator, denominator, atTick: atTick ?? 0 });
|
|
1293
1479
|
}
|
|
1294
1480
|
getMeter(atTick) {
|
|
1295
1481
|
return this._meterMap.getMeter(atTick);
|
|
1296
1482
|
}
|
|
1297
1483
|
removeMeter(atTick) {
|
|
1298
1484
|
this._meterMap.removeMeter(atTick);
|
|
1299
|
-
this.
|
|
1485
|
+
const meter = this._meterMap.getMeter(atTick);
|
|
1486
|
+
this._emit("meterchange", {
|
|
1487
|
+
numerator: meter.numerator,
|
|
1488
|
+
denominator: meter.denominator,
|
|
1489
|
+
atTick
|
|
1490
|
+
});
|
|
1300
1491
|
}
|
|
1301
1492
|
clearMeters() {
|
|
1302
1493
|
this._meterMap.clearMeters();
|
|
1303
|
-
this.
|
|
1494
|
+
const meter = this._meterMap.getMeter();
|
|
1495
|
+
this._emit("meterchange", {
|
|
1496
|
+
numerator: meter.numerator,
|
|
1497
|
+
denominator: meter.denominator,
|
|
1498
|
+
atTick: 0
|
|
1499
|
+
});
|
|
1304
1500
|
}
|
|
1305
1501
|
clearTempos() {
|
|
1306
1502
|
this._tempoMap.clearTempos();
|
|
1307
1503
|
if (this._loopEnabled) {
|
|
1308
1504
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1309
1505
|
}
|
|
1310
|
-
this._emit("tempochange");
|
|
1506
|
+
this._emit("tempochange", { bpm: this._tempoMap.getTempo(), atTick: 0 });
|
|
1311
1507
|
}
|
|
1312
1508
|
barToTick(bar) {
|
|
1313
1509
|
return this._meterMap.barToTick(bar);
|
|
@@ -1328,8 +1524,40 @@ var Transport = class _Transport {
|
|
|
1328
1524
|
this._metronomePlayer.setEnabled(enabled);
|
|
1329
1525
|
}
|
|
1330
1526
|
setMetronomeClickSounds(accent, normal) {
|
|
1527
|
+
this._accentBuffer = accent;
|
|
1528
|
+
this._normalBuffer = normal;
|
|
1331
1529
|
this._metronomePlayer.setClickSounds(accent, normal);
|
|
1332
1530
|
}
|
|
1531
|
+
setCountIn(enabled) {
|
|
1532
|
+
this._countInEnabled = enabled;
|
|
1533
|
+
}
|
|
1534
|
+
setCountInBars(bars) {
|
|
1535
|
+
const rounded = Math.round(bars);
|
|
1536
|
+
if (rounded < _Transport.MIN_COUNT_IN_BARS) {
|
|
1537
|
+
console.warn(
|
|
1538
|
+
"[waveform-playlist] Transport.setCountInBars: clamping " + bars + " to " + _Transport.MIN_COUNT_IN_BARS
|
|
1539
|
+
);
|
|
1540
|
+
this._countInBars = _Transport.MIN_COUNT_IN_BARS;
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (rounded > _Transport.MAX_COUNT_IN_BARS) {
|
|
1544
|
+
console.warn(
|
|
1545
|
+
"[waveform-playlist] Transport.setCountInBars: clamping " + bars + " to " + _Transport.MAX_COUNT_IN_BARS
|
|
1546
|
+
);
|
|
1547
|
+
this._countInBars = _Transport.MAX_COUNT_IN_BARS;
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
this._countInBars = rounded;
|
|
1551
|
+
}
|
|
1552
|
+
setCountInMode(mode) {
|
|
1553
|
+
this._countInMode = mode;
|
|
1554
|
+
}
|
|
1555
|
+
setRecording(recording) {
|
|
1556
|
+
this._recording = recording;
|
|
1557
|
+
}
|
|
1558
|
+
isCountingIn() {
|
|
1559
|
+
return this._countingIn;
|
|
1560
|
+
}
|
|
1333
1561
|
// --- Effects Hook ---
|
|
1334
1562
|
connectTrackOutput(trackId, node) {
|
|
1335
1563
|
const trackNode = this._trackNodes.get(trackId);
|
|
@@ -1418,9 +1646,94 @@ var Transport = class _Transport {
|
|
|
1418
1646
|
this._scheduler.addListener(this._clipPlayer);
|
|
1419
1647
|
this._scheduler.addListener(this._metronomePlayer);
|
|
1420
1648
|
}
|
|
1649
|
+
_initCountIn(audioContext, options) {
|
|
1650
|
+
const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
|
|
1651
|
+
this._countInPlayer = new CountInPlayer(
|
|
1652
|
+
audioContext,
|
|
1653
|
+
this._tempoMap,
|
|
1654
|
+
this._masterNode.input,
|
|
1655
|
+
toAudioTime
|
|
1656
|
+
);
|
|
1657
|
+
try {
|
|
1658
|
+
const { accent, normal } = createDefaultClickSounds(audioContext, {
|
|
1659
|
+
accentFrequency: options.accentFrequency,
|
|
1660
|
+
normalFrequency: options.normalFrequency
|
|
1661
|
+
});
|
|
1662
|
+
this._accentBuffer = accent;
|
|
1663
|
+
this._normalBuffer = normal;
|
|
1664
|
+
this._metronomePlayer.setClickSounds(accent, normal);
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
console.warn(
|
|
1667
|
+
"[waveform-playlist] Transport: failed to create default click sounds. Metronome and count-in will be silent until setMetronomeClickSounds() is called. Error: " + String(err)
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
_shouldCountIn() {
|
|
1672
|
+
if (!this._countInEnabled) return false;
|
|
1673
|
+
if (!this._accentBuffer || !this._normalBuffer) {
|
|
1674
|
+
console.warn("[waveform-playlist] Transport: count-in skipped \u2014 no click sounds loaded");
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
if (this._countInMode === "recording-only" && !this._recording) return false;
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
_startCountIn(endTime) {
|
|
1681
|
+
const currentTime = this._clock.getTime();
|
|
1682
|
+
this._countInStartPosition = currentTime;
|
|
1683
|
+
this._countingIn = true;
|
|
1684
|
+
this._playing = true;
|
|
1685
|
+
this._endTime = endTime;
|
|
1686
|
+
const playPositionTick = this._tempoMap.secondsToTicks(currentTime);
|
|
1687
|
+
const meter = this._meterMap.getMeter(playPositionTick);
|
|
1688
|
+
const totalBeats = meter.numerator * this._countInBars;
|
|
1689
|
+
const bpmAtPosition = this._tempoMap.getTempo(playPositionTick);
|
|
1690
|
+
const ticksPerBeat = this._ppqn * (4 / meter.denominator);
|
|
1691
|
+
const countInTicks = totalBeats * ticksPerBeat;
|
|
1692
|
+
const countInTempoMap = new TempoMap(this._ppqn, bpmAtPosition);
|
|
1693
|
+
const countInMeterMap = new MeterMap(this._ppqn, meter.numerator, meter.denominator);
|
|
1694
|
+
this._countInDuration = countInTempoMap.ticksToSeconds(countInTicks);
|
|
1695
|
+
this._countInScheduler = new Scheduler(countInTempoMap, {
|
|
1696
|
+
lookahead: this._schedulerLookahead
|
|
1697
|
+
});
|
|
1698
|
+
this._countInPlayer.configure({
|
|
1699
|
+
totalBeats,
|
|
1700
|
+
accentBuffer: this._accentBuffer,
|
|
1701
|
+
normalBuffer: this._normalBuffer,
|
|
1702
|
+
meterMap: countInMeterMap,
|
|
1703
|
+
tempoMap: countInTempoMap,
|
|
1704
|
+
onBeat: (beat, total) => {
|
|
1705
|
+
this._emit("countIn", { beat, totalBeats: total });
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
this._countInScheduler.addListener(this._countInPlayer);
|
|
1709
|
+
this._countInScheduler.reset(0);
|
|
1710
|
+
this._clock.seekTo(0);
|
|
1711
|
+
this._clock.start();
|
|
1712
|
+
this._timer.start();
|
|
1713
|
+
}
|
|
1714
|
+
_finishCountIn() {
|
|
1715
|
+
this._countInScheduler?.removeListener(this._countInPlayer);
|
|
1716
|
+
this._countInScheduler = null;
|
|
1717
|
+
this._countingIn = false;
|
|
1718
|
+
this._emit("countInEnd");
|
|
1719
|
+
this._clock.seekTo(this._countInStartPosition);
|
|
1720
|
+
const currentTime = this._clock.getTime();
|
|
1721
|
+
this._scheduler.reset(currentTime);
|
|
1722
|
+
const currentTick = this._tempoMap.secondsToTicks(currentTime);
|
|
1723
|
+
this._clipPlayer.onPositionJump(currentTick);
|
|
1724
|
+
this._emit("play");
|
|
1725
|
+
}
|
|
1726
|
+
_cancelCountIn() {
|
|
1727
|
+
this._countInPlayer.silence();
|
|
1728
|
+
this._countInScheduler?.removeListener(this._countInPlayer);
|
|
1729
|
+
this._countInScheduler = null;
|
|
1730
|
+
this._countingIn = false;
|
|
1731
|
+
this._timer.stop();
|
|
1732
|
+
}
|
|
1421
1733
|
_silenceAll() {
|
|
1422
1734
|
this._clipPlayer.silence();
|
|
1423
1735
|
this._metronomePlayer.silence();
|
|
1736
|
+
this._countInPlayer.silence();
|
|
1424
1737
|
}
|
|
1425
1738
|
_applyMuteState() {
|
|
1426
1739
|
const hasSolo = this._soloedTrackIds.size > 0;
|
|
@@ -1430,12 +1743,12 @@ var Transport = class _Transport {
|
|
|
1430
1743
|
node.setMute(isExplicitlyMuted || isSoloMuted);
|
|
1431
1744
|
}
|
|
1432
1745
|
}
|
|
1433
|
-
_emit(event) {
|
|
1746
|
+
_emit(event, ...args) {
|
|
1434
1747
|
const listeners = this._listeners.get(event);
|
|
1435
1748
|
if (listeners) {
|
|
1436
1749
|
for (const cb of listeners) {
|
|
1437
1750
|
try {
|
|
1438
|
-
cb();
|
|
1751
|
+
cb(...args);
|
|
1439
1752
|
} catch (err) {
|
|
1440
1753
|
console.warn(
|
|
1441
1754
|
'[waveform-playlist] Transport "' + event + '" listener threw:',
|
|
@@ -1446,6 +1759,10 @@ var Transport = class _Transport {
|
|
|
1446
1759
|
}
|
|
1447
1760
|
}
|
|
1448
1761
|
};
|
|
1762
|
+
// --- Count-In ---
|
|
1763
|
+
_Transport.MIN_COUNT_IN_BARS = 1;
|
|
1764
|
+
_Transport.MAX_COUNT_IN_BARS = 8;
|
|
1765
|
+
var Transport = _Transport;
|
|
1449
1766
|
|
|
1450
1767
|
// src/adapter.ts
|
|
1451
1768
|
var NativePlayoutAdapter = class {
|
|
@@ -1462,6 +1779,27 @@ var NativePlayoutAdapter = class {
|
|
|
1462
1779
|
}
|
|
1463
1780
|
if (this._audioContext.state === "suspended") {
|
|
1464
1781
|
await this._audioContext.resume();
|
|
1782
|
+
const MIN_WARMUP = 0.02;
|
|
1783
|
+
const warmupTarget = Math.max(MIN_WARMUP, this._audioContext.outputLatency ?? MIN_WARMUP);
|
|
1784
|
+
if (this._audioContext.currentTime < warmupTarget) {
|
|
1785
|
+
const MAX_WARMUP_MS = 2e3;
|
|
1786
|
+
await new Promise((resolve) => {
|
|
1787
|
+
const startMs = performance.now();
|
|
1788
|
+
const check = () => {
|
|
1789
|
+
if (this._audioContext.currentTime >= warmupTarget) {
|
|
1790
|
+
resolve();
|
|
1791
|
+
} else if (this._audioContext.state === "closed" || performance.now() - startMs > MAX_WARMUP_MS) {
|
|
1792
|
+
console.warn(
|
|
1793
|
+
"[waveform-playlist] AudioContext warmup timed out (currentTime=" + this._audioContext.currentTime + ", target=" + warmupTarget + ", state=" + this._audioContext.state + "). Proceeding without warmup."
|
|
1794
|
+
);
|
|
1795
|
+
resolve();
|
|
1796
|
+
} else {
|
|
1797
|
+
requestAnimationFrame(check);
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
requestAnimationFrame(check);
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1465
1803
|
}
|
|
1466
1804
|
}
|
|
1467
1805
|
setTracks(tracks) {
|
|
@@ -1512,6 +1850,21 @@ var NativePlayoutAdapter = class {
|
|
|
1512
1850
|
setLoop(enabled, start, end) {
|
|
1513
1851
|
this._transport.setLoopSeconds(enabled, start, end);
|
|
1514
1852
|
}
|
|
1853
|
+
setCountIn(enabled) {
|
|
1854
|
+
this._transport.setCountIn(enabled);
|
|
1855
|
+
}
|
|
1856
|
+
setCountInBars(bars) {
|
|
1857
|
+
this._transport.setCountInBars(bars);
|
|
1858
|
+
}
|
|
1859
|
+
setCountInMode(mode) {
|
|
1860
|
+
this._transport.setCountInMode(mode);
|
|
1861
|
+
}
|
|
1862
|
+
setRecording(recording) {
|
|
1863
|
+
this._transport.setRecording(recording);
|
|
1864
|
+
}
|
|
1865
|
+
isCountingIn() {
|
|
1866
|
+
return this._transport.isCountingIn();
|
|
1867
|
+
}
|
|
1515
1868
|
dispose() {
|
|
1516
1869
|
this._transport.dispose();
|
|
1517
1870
|
}
|