@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.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._scheduleFrame();
213
+ if (this._running) {
214
+ this._scheduleFrame();
215
+ }
214
216
  });
215
217
  }
216
218
  };
@@ -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 Transport = class _Transport {
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
- if (wasPlaying) {
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._emit("meterchange");
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._emit("meterchange");
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 {
@@ -1549,6 +1866,21 @@ var NativePlayoutAdapter = class {
1549
1866
  setLoop(enabled, start, end) {
1550
1867
  this._transport.setLoopSeconds(enabled, start, end);
1551
1868
  }
1869
+ setCountIn(enabled) {
1870
+ this._transport.setCountIn(enabled);
1871
+ }
1872
+ setCountInBars(bars) {
1873
+ this._transport.setCountInBars(bars);
1874
+ }
1875
+ setCountInMode(mode) {
1876
+ this._transport.setCountInMode(mode);
1877
+ }
1878
+ setRecording(recording) {
1879
+ this._transport.setRecording(recording);
1880
+ }
1881
+ isCountingIn() {
1882
+ return this._transport.isCountingIn();
1883
+ }
1552
1884
  dispose() {
1553
1885
  this._transport.dispose();
1554
1886
  }