@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.js
CHANGED
|
@@ -210,7 +210,9 @@ var Timer = class {
|
|
|
210
210
|
} catch (err) {
|
|
211
211
|
console.warn("[waveform-playlist] Timer tick error:", String(err));
|
|
212
212
|
}
|
|
213
|
-
this.
|
|
213
|
+
if (this._running) {
|
|
214
|
+
this._scheduleFrame();
|
|
215
|
+
}
|
|
214
216
|
});
|
|
215
217
|
}
|
|
216
218
|
};
|
|
@@ -887,7 +889,7 @@ var ClipPlayer = class {
|
|
|
887
889
|
if (!clip.audioBuffer) continue;
|
|
888
890
|
const clipStartSample = clip.startSample;
|
|
889
891
|
const clipEndSample = clipStartSample + clip.durationSamples;
|
|
890
|
-
if (clipStartSample
|
|
892
|
+
if (clipStartSample < newSample && clipEndSample > newSample) {
|
|
891
893
|
const offsetIntoClipSamples = newSample - clipStartSample;
|
|
892
894
|
const offsetSamples = clip.offsetSamples + offsetIntoClipSamples;
|
|
893
895
|
let durationSamples = clipEndSample - newSample;
|
|
@@ -1050,8 +1052,145 @@ var MetronomePlayer = class {
|
|
|
1050
1052
|
}
|
|
1051
1053
|
};
|
|
1052
1054
|
|
|
1055
|
+
// src/audio/count-in-player.ts
|
|
1056
|
+
var CountInPlayer = class {
|
|
1057
|
+
constructor(audioContext, tempoMap, destination, toAudioTime) {
|
|
1058
|
+
this._activeSources = /* @__PURE__ */ new Set();
|
|
1059
|
+
this._totalBeats = 0;
|
|
1060
|
+
this._beatsGenerated = 0;
|
|
1061
|
+
this._accentBuffer = null;
|
|
1062
|
+
this._normalBuffer = null;
|
|
1063
|
+
this._meterMap = null;
|
|
1064
|
+
this._onBeat = null;
|
|
1065
|
+
this._audioContext = audioContext;
|
|
1066
|
+
this._tempoMap = tempoMap;
|
|
1067
|
+
this._destination = destination;
|
|
1068
|
+
this._toAudioTime = toAudioTime;
|
|
1069
|
+
}
|
|
1070
|
+
configure(config) {
|
|
1071
|
+
this._totalBeats = config.totalBeats;
|
|
1072
|
+
this._beatsGenerated = 0;
|
|
1073
|
+
this._accentBuffer = config.accentBuffer;
|
|
1074
|
+
this._normalBuffer = config.normalBuffer;
|
|
1075
|
+
this._meterMap = config.meterMap;
|
|
1076
|
+
this._tempoMap = config.tempoMap;
|
|
1077
|
+
this._onBeat = config.onBeat;
|
|
1078
|
+
}
|
|
1079
|
+
generate(fromTick, toTick) {
|
|
1080
|
+
if (!this._accentBuffer || !this._normalBuffer || !this._meterMap) {
|
|
1081
|
+
return [];
|
|
1082
|
+
}
|
|
1083
|
+
const events = [];
|
|
1084
|
+
const meterMap = this._meterMap;
|
|
1085
|
+
let entry = meterMap.getEntryAt(fromTick);
|
|
1086
|
+
let beatSize = meterMap.ticksPerBeat(fromTick);
|
|
1087
|
+
const tickIntoSection = fromTick - entry.tick;
|
|
1088
|
+
let tick = entry.tick + Math.ceil(tickIntoSection / beatSize) * beatSize;
|
|
1089
|
+
while (tick < toTick && this._beatsGenerated < this._totalBeats) {
|
|
1090
|
+
const tickPos = tick;
|
|
1091
|
+
const currentEntry = meterMap.getEntryAt(tickPos);
|
|
1092
|
+
if (currentEntry.tick !== entry.tick) {
|
|
1093
|
+
entry = currentEntry;
|
|
1094
|
+
beatSize = meterMap.ticksPerBeat(tickPos);
|
|
1095
|
+
}
|
|
1096
|
+
this._beatsGenerated++;
|
|
1097
|
+
const isAccent = meterMap.isBarBoundary(tickPos);
|
|
1098
|
+
events.push({
|
|
1099
|
+
tick: tickPos,
|
|
1100
|
+
isAccent,
|
|
1101
|
+
buffer: isAccent ? this._accentBuffer : this._normalBuffer,
|
|
1102
|
+
beat: this._beatsGenerated,
|
|
1103
|
+
totalBeats: this._totalBeats
|
|
1104
|
+
});
|
|
1105
|
+
beatSize = meterMap.ticksPerBeat(tickPos);
|
|
1106
|
+
tick += beatSize;
|
|
1107
|
+
}
|
|
1108
|
+
return events;
|
|
1109
|
+
}
|
|
1110
|
+
consume(event) {
|
|
1111
|
+
try {
|
|
1112
|
+
const source = this._audioContext.createBufferSource();
|
|
1113
|
+
source.buffer = event.buffer;
|
|
1114
|
+
source.connect(this._destination);
|
|
1115
|
+
this._activeSources.add(source);
|
|
1116
|
+
source.addEventListener("ended", () => {
|
|
1117
|
+
this._activeSources.delete(source);
|
|
1118
|
+
try {
|
|
1119
|
+
source.disconnect();
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
console.warn(
|
|
1122
|
+
"[waveform-playlist] CountInPlayer: error disconnecting source:",
|
|
1123
|
+
String(err)
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
const transportTime = this._tempoMap.ticksToSeconds(event.tick);
|
|
1128
|
+
source.start(this._toAudioTime(transportTime));
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
console.warn(
|
|
1131
|
+
"[waveform-playlist] CountInPlayer.consume: failed to schedule beat " + event.beat + "/" + event.totalBeats + ": " + String(err)
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
this._onBeat?.(event.beat, event.totalBeats);
|
|
1135
|
+
}
|
|
1136
|
+
onPositionJump(_newTick) {
|
|
1137
|
+
}
|
|
1138
|
+
silence() {
|
|
1139
|
+
for (const source of this._activeSources) {
|
|
1140
|
+
try {
|
|
1141
|
+
source.stop();
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.warn(
|
|
1144
|
+
"[waveform-playlist] CountInPlayer.silence: error stopping source:",
|
|
1145
|
+
String(err)
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
source.disconnect();
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
console.warn(
|
|
1152
|
+
"[waveform-playlist] CountInPlayer.silence: error disconnecting:",
|
|
1153
|
+
String(err)
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
this._activeSources.clear();
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// src/audio/click-sounds.ts
|
|
1162
|
+
var DEFAULT_ACCENT_FREQUENCY = 1e3;
|
|
1163
|
+
var DEFAULT_NORMAL_FREQUENCY = 800;
|
|
1164
|
+
var ACCENT_DURATION = 0.04;
|
|
1165
|
+
var NORMAL_DURATION = 0.03;
|
|
1166
|
+
function synthesizeClick(audioContext, frequency, duration) {
|
|
1167
|
+
const sampleRate = audioContext.sampleRate;
|
|
1168
|
+
const length = Math.ceil(sampleRate * duration);
|
|
1169
|
+
const buffer = audioContext.createBuffer(1, length, sampleRate);
|
|
1170
|
+
const data = buffer.getChannelData(0);
|
|
1171
|
+
for (let i = 0; i < length; i++) {
|
|
1172
|
+
const t = i / sampleRate;
|
|
1173
|
+
const envelope = Math.exp(-t * 50);
|
|
1174
|
+
data[i] = Math.sin(2 * Math.PI * frequency * t) * envelope;
|
|
1175
|
+
}
|
|
1176
|
+
return buffer;
|
|
1177
|
+
}
|
|
1178
|
+
function createDefaultClickSounds(audioContext, options) {
|
|
1179
|
+
const accentFreq = options?.accentFrequency ?? DEFAULT_ACCENT_FREQUENCY;
|
|
1180
|
+
const normalFreq = options?.normalFrequency ?? DEFAULT_NORMAL_FREQUENCY;
|
|
1181
|
+
if (accentFreq <= 0 || normalFreq <= 0) {
|
|
1182
|
+
console.warn(
|
|
1183
|
+
"[waveform-playlist] createDefaultClickSounds: frequency must be positive, got accent=" + accentFreq + " normal=" + normalFreq
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
accent: synthesizeClick(audioContext, accentFreq, ACCENT_DURATION),
|
|
1188
|
+
normal: synthesizeClick(audioContext, normalFreq, NORMAL_DURATION)
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1053
1192
|
// src/transport.ts
|
|
1054
|
-
var
|
|
1193
|
+
var _Transport = class _Transport {
|
|
1055
1194
|
constructor(audioContext, options = {}) {
|
|
1056
1195
|
this._trackNodes = /* @__PURE__ */ new Map();
|
|
1057
1196
|
this._tracks = [];
|
|
@@ -1061,7 +1200,22 @@ var Transport = class _Transport {
|
|
|
1061
1200
|
this._loopEnabled = false;
|
|
1062
1201
|
this._loopStartTick = 0;
|
|
1063
1202
|
this._loopStartSeconds = 0;
|
|
1203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1064
1204
|
this._listeners = /* @__PURE__ */ new Map();
|
|
1205
|
+
// --- Count-In state ---
|
|
1206
|
+
this._countInEnabled = false;
|
|
1207
|
+
this._countInBars = 1;
|
|
1208
|
+
this._countInMode = "recording-only";
|
|
1209
|
+
this._recording = false;
|
|
1210
|
+
this._countingIn = false;
|
|
1211
|
+
this._countInStartPosition = 0;
|
|
1212
|
+
this._countInDuration = 0;
|
|
1213
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1214
|
+
this._countInScheduler = null;
|
|
1215
|
+
this._accentBuffer = null;
|
|
1216
|
+
this._normalBuffer = null;
|
|
1217
|
+
this._schedulerLookahead = 0.2;
|
|
1218
|
+
this._ppqn = 960;
|
|
1065
1219
|
this._audioContext = audioContext;
|
|
1066
1220
|
const sampleRate = options.sampleRate ?? audioContext.sampleRate;
|
|
1067
1221
|
const ppqn = options.ppqn ?? 960;
|
|
@@ -1069,6 +1223,8 @@ var Transport = class _Transport {
|
|
|
1069
1223
|
const numerator = options.numerator ?? 4;
|
|
1070
1224
|
const denominator = options.denominator ?? 4;
|
|
1071
1225
|
const lookahead = options.schedulerLookahead ?? 0.2;
|
|
1226
|
+
this._ppqn = ppqn;
|
|
1227
|
+
this._schedulerLookahead = lookahead;
|
|
1072
1228
|
_Transport._validateOptions(sampleRate, ppqn, tempo, numerator, denominator, lookahead);
|
|
1073
1229
|
this._clock = new Clock(audioContext);
|
|
1074
1230
|
this._sampleTimeline = new SampleTimeline(sampleRate);
|
|
@@ -1083,8 +1239,16 @@ var Transport = class _Transport {
|
|
|
1083
1239
|
});
|
|
1084
1240
|
this._sampleTimeline.setTempoMap(this._tempoMap);
|
|
1085
1241
|
this._initAudioGraph(audioContext);
|
|
1242
|
+
this._initCountIn(audioContext, options);
|
|
1086
1243
|
this._timer = new Timer(() => {
|
|
1087
1244
|
const time = this._clock.getTime();
|
|
1245
|
+
if (this._countingIn) {
|
|
1246
|
+
this._countInScheduler?.advance(time);
|
|
1247
|
+
if (time >= this._countInDuration) {
|
|
1248
|
+
this._finishCountIn();
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1088
1252
|
if (this._endTime !== void 0 && time >= this._endTime) {
|
|
1089
1253
|
this.stop();
|
|
1090
1254
|
return;
|
|
@@ -1101,6 +1265,10 @@ var Transport = class _Transport {
|
|
|
1101
1265
|
if (startTime !== void 0) {
|
|
1102
1266
|
this._clock.seekTo(startTime);
|
|
1103
1267
|
}
|
|
1268
|
+
if (this._shouldCountIn()) {
|
|
1269
|
+
this._startCountIn(endTime);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1104
1272
|
const currentTime = this._clock.getTime();
|
|
1105
1273
|
this._scheduler.reset(currentTime);
|
|
1106
1274
|
this._endTime = endTime;
|
|
@@ -1113,6 +1281,13 @@ var Transport = class _Transport {
|
|
|
1113
1281
|
}
|
|
1114
1282
|
pause() {
|
|
1115
1283
|
if (!this._playing) return;
|
|
1284
|
+
if (this._countingIn) {
|
|
1285
|
+
this._cancelCountIn();
|
|
1286
|
+
this._clock.stop();
|
|
1287
|
+
this._playing = false;
|
|
1288
|
+
this._emit("pause");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1116
1291
|
this._timer.stop();
|
|
1117
1292
|
this._clock.stop();
|
|
1118
1293
|
this._silenceAll();
|
|
@@ -1121,6 +1296,9 @@ var Transport = class _Transport {
|
|
|
1121
1296
|
}
|
|
1122
1297
|
stop() {
|
|
1123
1298
|
const wasPlaying = this._playing;
|
|
1299
|
+
if (this._countingIn) {
|
|
1300
|
+
this._cancelCountIn();
|
|
1301
|
+
}
|
|
1124
1302
|
this._timer.stop();
|
|
1125
1303
|
this._clock.reset();
|
|
1126
1304
|
this._scheduler.reset(0);
|
|
@@ -1133,14 +1311,19 @@ var Transport = class _Transport {
|
|
|
1133
1311
|
}
|
|
1134
1312
|
seek(time) {
|
|
1135
1313
|
const wasPlaying = this._playing;
|
|
1136
|
-
|
|
1314
|
+
const wasCountingIn = this._countingIn;
|
|
1315
|
+
if (wasCountingIn) {
|
|
1316
|
+
this._cancelCountIn();
|
|
1317
|
+
this._playing = false;
|
|
1318
|
+
}
|
|
1319
|
+
if (wasPlaying && !wasCountingIn) {
|
|
1137
1320
|
this._timer.stop();
|
|
1138
1321
|
}
|
|
1139
1322
|
this._silenceAll();
|
|
1140
1323
|
this._clock.seekTo(time);
|
|
1141
1324
|
this._scheduler.reset(time);
|
|
1142
1325
|
this._endTime = void 0;
|
|
1143
|
-
if (wasPlaying) {
|
|
1326
|
+
if (wasPlaying && !wasCountingIn) {
|
|
1144
1327
|
this._clock.start();
|
|
1145
1328
|
const seekTick = this._tempoMap.secondsToTicks(time);
|
|
1146
1329
|
this._clipPlayer.onPositionJump(seekTick);
|
|
@@ -1148,6 +1331,9 @@ var Transport = class _Transport {
|
|
|
1148
1331
|
}
|
|
1149
1332
|
}
|
|
1150
1333
|
getCurrentTime() {
|
|
1334
|
+
if (this._countingIn) {
|
|
1335
|
+
return this._countInStartPosition;
|
|
1336
|
+
}
|
|
1151
1337
|
const t = this._clock.getTime();
|
|
1152
1338
|
if (this._loopEnabled && t < this._loopStartSeconds) {
|
|
1153
1339
|
return this._loopStartSeconds;
|
|
@@ -1318,7 +1504,7 @@ var Transport = class _Transport {
|
|
|
1318
1504
|
if (this._loopEnabled) {
|
|
1319
1505
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1320
1506
|
}
|
|
1321
|
-
this._emit("tempochange");
|
|
1507
|
+
this._emit("tempochange", { bpm, atTick: atTick ?? 0 });
|
|
1322
1508
|
}
|
|
1323
1509
|
getTempo(atTick) {
|
|
1324
1510
|
return this._tempoMap.getTempo(atTick);
|
|
@@ -1326,25 +1512,35 @@ var Transport = class _Transport {
|
|
|
1326
1512
|
// --- Meter ---
|
|
1327
1513
|
setMeter(numerator, denominator, atTick) {
|
|
1328
1514
|
this._meterMap.setMeter(numerator, denominator, atTick);
|
|
1329
|
-
this._emit("meterchange");
|
|
1515
|
+
this._emit("meterchange", { numerator, denominator, atTick: atTick ?? 0 });
|
|
1330
1516
|
}
|
|
1331
1517
|
getMeter(atTick) {
|
|
1332
1518
|
return this._meterMap.getMeter(atTick);
|
|
1333
1519
|
}
|
|
1334
1520
|
removeMeter(atTick) {
|
|
1335
1521
|
this._meterMap.removeMeter(atTick);
|
|
1336
|
-
this.
|
|
1522
|
+
const meter = this._meterMap.getMeter(atTick);
|
|
1523
|
+
this._emit("meterchange", {
|
|
1524
|
+
numerator: meter.numerator,
|
|
1525
|
+
denominator: meter.denominator,
|
|
1526
|
+
atTick
|
|
1527
|
+
});
|
|
1337
1528
|
}
|
|
1338
1529
|
clearMeters() {
|
|
1339
1530
|
this._meterMap.clearMeters();
|
|
1340
|
-
this.
|
|
1531
|
+
const meter = this._meterMap.getMeter();
|
|
1532
|
+
this._emit("meterchange", {
|
|
1533
|
+
numerator: meter.numerator,
|
|
1534
|
+
denominator: meter.denominator,
|
|
1535
|
+
atTick: 0
|
|
1536
|
+
});
|
|
1341
1537
|
}
|
|
1342
1538
|
clearTempos() {
|
|
1343
1539
|
this._tempoMap.clearTempos();
|
|
1344
1540
|
if (this._loopEnabled) {
|
|
1345
1541
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1346
1542
|
}
|
|
1347
|
-
this._emit("tempochange");
|
|
1543
|
+
this._emit("tempochange", { bpm: this._tempoMap.getTempo(), atTick: 0 });
|
|
1348
1544
|
}
|
|
1349
1545
|
barToTick(bar) {
|
|
1350
1546
|
return this._meterMap.barToTick(bar);
|
|
@@ -1365,8 +1561,40 @@ var Transport = class _Transport {
|
|
|
1365
1561
|
this._metronomePlayer.setEnabled(enabled);
|
|
1366
1562
|
}
|
|
1367
1563
|
setMetronomeClickSounds(accent, normal) {
|
|
1564
|
+
this._accentBuffer = accent;
|
|
1565
|
+
this._normalBuffer = normal;
|
|
1368
1566
|
this._metronomePlayer.setClickSounds(accent, normal);
|
|
1369
1567
|
}
|
|
1568
|
+
setCountIn(enabled) {
|
|
1569
|
+
this._countInEnabled = enabled;
|
|
1570
|
+
}
|
|
1571
|
+
setCountInBars(bars) {
|
|
1572
|
+
const rounded = Math.round(bars);
|
|
1573
|
+
if (rounded < _Transport.MIN_COUNT_IN_BARS) {
|
|
1574
|
+
console.warn(
|
|
1575
|
+
"[waveform-playlist] Transport.setCountInBars: clamping " + bars + " to " + _Transport.MIN_COUNT_IN_BARS
|
|
1576
|
+
);
|
|
1577
|
+
this._countInBars = _Transport.MIN_COUNT_IN_BARS;
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (rounded > _Transport.MAX_COUNT_IN_BARS) {
|
|
1581
|
+
console.warn(
|
|
1582
|
+
"[waveform-playlist] Transport.setCountInBars: clamping " + bars + " to " + _Transport.MAX_COUNT_IN_BARS
|
|
1583
|
+
);
|
|
1584
|
+
this._countInBars = _Transport.MAX_COUNT_IN_BARS;
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
this._countInBars = rounded;
|
|
1588
|
+
}
|
|
1589
|
+
setCountInMode(mode) {
|
|
1590
|
+
this._countInMode = mode;
|
|
1591
|
+
}
|
|
1592
|
+
setRecording(recording) {
|
|
1593
|
+
this._recording = recording;
|
|
1594
|
+
}
|
|
1595
|
+
isCountingIn() {
|
|
1596
|
+
return this._countingIn;
|
|
1597
|
+
}
|
|
1370
1598
|
// --- Effects Hook ---
|
|
1371
1599
|
connectTrackOutput(trackId, node) {
|
|
1372
1600
|
const trackNode = this._trackNodes.get(trackId);
|
|
@@ -1455,9 +1683,94 @@ var Transport = class _Transport {
|
|
|
1455
1683
|
this._scheduler.addListener(this._clipPlayer);
|
|
1456
1684
|
this._scheduler.addListener(this._metronomePlayer);
|
|
1457
1685
|
}
|
|
1686
|
+
_initCountIn(audioContext, options) {
|
|
1687
|
+
const toAudioTime = (transportTime) => this._clock.toAudioTime(transportTime);
|
|
1688
|
+
this._countInPlayer = new CountInPlayer(
|
|
1689
|
+
audioContext,
|
|
1690
|
+
this._tempoMap,
|
|
1691
|
+
this._masterNode.input,
|
|
1692
|
+
toAudioTime
|
|
1693
|
+
);
|
|
1694
|
+
try {
|
|
1695
|
+
const { accent, normal } = createDefaultClickSounds(audioContext, {
|
|
1696
|
+
accentFrequency: options.accentFrequency,
|
|
1697
|
+
normalFrequency: options.normalFrequency
|
|
1698
|
+
});
|
|
1699
|
+
this._accentBuffer = accent;
|
|
1700
|
+
this._normalBuffer = normal;
|
|
1701
|
+
this._metronomePlayer.setClickSounds(accent, normal);
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
console.warn(
|
|
1704
|
+
"[waveform-playlist] Transport: failed to create default click sounds. Metronome and count-in will be silent until setMetronomeClickSounds() is called. Error: " + String(err)
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
_shouldCountIn() {
|
|
1709
|
+
if (!this._countInEnabled) return false;
|
|
1710
|
+
if (!this._accentBuffer || !this._normalBuffer) {
|
|
1711
|
+
console.warn("[waveform-playlist] Transport: count-in skipped \u2014 no click sounds loaded");
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
if (this._countInMode === "recording-only" && !this._recording) return false;
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
_startCountIn(endTime) {
|
|
1718
|
+
const currentTime = this._clock.getTime();
|
|
1719
|
+
this._countInStartPosition = currentTime;
|
|
1720
|
+
this._countingIn = true;
|
|
1721
|
+
this._playing = true;
|
|
1722
|
+
this._endTime = endTime;
|
|
1723
|
+
const playPositionTick = this._tempoMap.secondsToTicks(currentTime);
|
|
1724
|
+
const meter = this._meterMap.getMeter(playPositionTick);
|
|
1725
|
+
const totalBeats = meter.numerator * this._countInBars;
|
|
1726
|
+
const bpmAtPosition = this._tempoMap.getTempo(playPositionTick);
|
|
1727
|
+
const ticksPerBeat = this._ppqn * (4 / meter.denominator);
|
|
1728
|
+
const countInTicks = totalBeats * ticksPerBeat;
|
|
1729
|
+
const countInTempoMap = new TempoMap(this._ppqn, bpmAtPosition);
|
|
1730
|
+
const countInMeterMap = new MeterMap(this._ppqn, meter.numerator, meter.denominator);
|
|
1731
|
+
this._countInDuration = countInTempoMap.ticksToSeconds(countInTicks);
|
|
1732
|
+
this._countInScheduler = new Scheduler(countInTempoMap, {
|
|
1733
|
+
lookahead: this._schedulerLookahead
|
|
1734
|
+
});
|
|
1735
|
+
this._countInPlayer.configure({
|
|
1736
|
+
totalBeats,
|
|
1737
|
+
accentBuffer: this._accentBuffer,
|
|
1738
|
+
normalBuffer: this._normalBuffer,
|
|
1739
|
+
meterMap: countInMeterMap,
|
|
1740
|
+
tempoMap: countInTempoMap,
|
|
1741
|
+
onBeat: (beat, total) => {
|
|
1742
|
+
this._emit("countIn", { beat, totalBeats: total });
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
this._countInScheduler.addListener(this._countInPlayer);
|
|
1746
|
+
this._countInScheduler.reset(0);
|
|
1747
|
+
this._clock.seekTo(0);
|
|
1748
|
+
this._clock.start();
|
|
1749
|
+
this._timer.start();
|
|
1750
|
+
}
|
|
1751
|
+
_finishCountIn() {
|
|
1752
|
+
this._countInScheduler?.removeListener(this._countInPlayer);
|
|
1753
|
+
this._countInScheduler = null;
|
|
1754
|
+
this._countingIn = false;
|
|
1755
|
+
this._emit("countInEnd");
|
|
1756
|
+
this._clock.seekTo(this._countInStartPosition);
|
|
1757
|
+
const currentTime = this._clock.getTime();
|
|
1758
|
+
this._scheduler.reset(currentTime);
|
|
1759
|
+
const currentTick = this._tempoMap.secondsToTicks(currentTime);
|
|
1760
|
+
this._clipPlayer.onPositionJump(currentTick);
|
|
1761
|
+
this._emit("play");
|
|
1762
|
+
}
|
|
1763
|
+
_cancelCountIn() {
|
|
1764
|
+
this._countInPlayer.silence();
|
|
1765
|
+
this._countInScheduler?.removeListener(this._countInPlayer);
|
|
1766
|
+
this._countInScheduler = null;
|
|
1767
|
+
this._countingIn = false;
|
|
1768
|
+
this._timer.stop();
|
|
1769
|
+
}
|
|
1458
1770
|
_silenceAll() {
|
|
1459
1771
|
this._clipPlayer.silence();
|
|
1460
1772
|
this._metronomePlayer.silence();
|
|
1773
|
+
this._countInPlayer.silence();
|
|
1461
1774
|
}
|
|
1462
1775
|
_applyMuteState() {
|
|
1463
1776
|
const hasSolo = this._soloedTrackIds.size > 0;
|
|
@@ -1467,12 +1780,12 @@ var Transport = class _Transport {
|
|
|
1467
1780
|
node.setMute(isExplicitlyMuted || isSoloMuted);
|
|
1468
1781
|
}
|
|
1469
1782
|
}
|
|
1470
|
-
_emit(event) {
|
|
1783
|
+
_emit(event, ...args) {
|
|
1471
1784
|
const listeners = this._listeners.get(event);
|
|
1472
1785
|
if (listeners) {
|
|
1473
1786
|
for (const cb of listeners) {
|
|
1474
1787
|
try {
|
|
1475
|
-
cb();
|
|
1788
|
+
cb(...args);
|
|
1476
1789
|
} catch (err) {
|
|
1477
1790
|
console.warn(
|
|
1478
1791
|
'[waveform-playlist] Transport "' + event + '" listener threw:',
|
|
@@ -1483,6 +1796,10 @@ var Transport = class _Transport {
|
|
|
1483
1796
|
}
|
|
1484
1797
|
}
|
|
1485
1798
|
};
|
|
1799
|
+
// --- Count-In ---
|
|
1800
|
+
_Transport.MIN_COUNT_IN_BARS = 1;
|
|
1801
|
+
_Transport.MAX_COUNT_IN_BARS = 8;
|
|
1802
|
+
var Transport = _Transport;
|
|
1486
1803
|
|
|
1487
1804
|
// src/adapter.ts
|
|
1488
1805
|
var NativePlayoutAdapter = class {
|
|
@@ -1499,6 +1816,27 @@ var NativePlayoutAdapter = class {
|
|
|
1499
1816
|
}
|
|
1500
1817
|
if (this._audioContext.state === "suspended") {
|
|
1501
1818
|
await this._audioContext.resume();
|
|
1819
|
+
const MIN_WARMUP = 0.02;
|
|
1820
|
+
const warmupTarget = Math.max(MIN_WARMUP, this._audioContext.outputLatency ?? MIN_WARMUP);
|
|
1821
|
+
if (this._audioContext.currentTime < warmupTarget) {
|
|
1822
|
+
const MAX_WARMUP_MS = 2e3;
|
|
1823
|
+
await new Promise((resolve) => {
|
|
1824
|
+
const startMs = performance.now();
|
|
1825
|
+
const check = () => {
|
|
1826
|
+
if (this._audioContext.currentTime >= warmupTarget) {
|
|
1827
|
+
resolve();
|
|
1828
|
+
} else if (this._audioContext.state === "closed" || performance.now() - startMs > MAX_WARMUP_MS) {
|
|
1829
|
+
console.warn(
|
|
1830
|
+
"[waveform-playlist] AudioContext warmup timed out (currentTime=" + this._audioContext.currentTime + ", target=" + warmupTarget + ", state=" + this._audioContext.state + "). Proceeding without warmup."
|
|
1831
|
+
);
|
|
1832
|
+
resolve();
|
|
1833
|
+
} else {
|
|
1834
|
+
requestAnimationFrame(check);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
requestAnimationFrame(check);
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1502
1840
|
}
|
|
1503
1841
|
}
|
|
1504
1842
|
setTracks(tracks) {
|
|
@@ -1549,6 +1887,21 @@ var NativePlayoutAdapter = class {
|
|
|
1549
1887
|
setLoop(enabled, start, end) {
|
|
1550
1888
|
this._transport.setLoopSeconds(enabled, start, end);
|
|
1551
1889
|
}
|
|
1890
|
+
setCountIn(enabled) {
|
|
1891
|
+
this._transport.setCountIn(enabled);
|
|
1892
|
+
}
|
|
1893
|
+
setCountInBars(bars) {
|
|
1894
|
+
this._transport.setCountInBars(bars);
|
|
1895
|
+
}
|
|
1896
|
+
setCountInMode(mode) {
|
|
1897
|
+
this._transport.setCountInMode(mode);
|
|
1898
|
+
}
|
|
1899
|
+
setRecording(recording) {
|
|
1900
|
+
this._transport.setRecording(recording);
|
|
1901
|
+
}
|
|
1902
|
+
isCountingIn() {
|
|
1903
|
+
return this._transport.isCountingIn();
|
|
1904
|
+
}
|
|
1552
1905
|
dispose() {
|
|
1553
1906
|
this._transport.dispose();
|
|
1554
1907
|
}
|