@dawcore/transport 0.0.4 → 0.0.5

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.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._scheduleFrame();
176
+ if (this._running) {
177
+ this._scheduleFrame();
178
+ }
177
179
  });
178
180
  }
179
181
  };
@@ -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 Transport = class _Transport {
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
- if (wasPlaying) {
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._emit("meterchange");
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._emit("meterchange");
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 {
@@ -1512,6 +1829,21 @@ var NativePlayoutAdapter = class {
1512
1829
  setLoop(enabled, start, end) {
1513
1830
  this._transport.setLoopSeconds(enabled, start, end);
1514
1831
  }
1832
+ setCountIn(enabled) {
1833
+ this._transport.setCountIn(enabled);
1834
+ }
1835
+ setCountInBars(bars) {
1836
+ this._transport.setCountInBars(bars);
1837
+ }
1838
+ setCountInMode(mode) {
1839
+ this._transport.setCountInMode(mode);
1840
+ }
1841
+ setRecording(recording) {
1842
+ this._transport.setRecording(recording);
1843
+ }
1844
+ isCountingIn() {
1845
+ return this._transport.isCountingIn();
1846
+ }
1515
1847
  dispose() {
1516
1848
  this._transport.dispose();
1517
1849
  }