@dawcore/components 0.0.1 → 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/dist/index.js CHANGED
@@ -39,8 +39,10 @@ var __decorateClass = (decorators, target, key, kind) => {
39
39
  var index_exports = {};
40
40
  __export(index_exports, {
41
41
  AudioResumeController: () => AudioResumeController,
42
+ ClipPointerHandler: () => ClipPointerHandler,
42
43
  DawClipElement: () => DawClipElement,
43
44
  DawEditorElement: () => DawEditorElement,
45
+ DawKeyboardShortcutsElement: () => DawKeyboardShortcutsElement,
44
46
  DawPauseButtonElement: () => DawPauseButtonElement,
45
47
  DawPlayButtonElement: () => DawPlayButtonElement,
46
48
  DawPlayheadElement: () => DawPlayheadElement,
@@ -53,7 +55,8 @@ __export(index_exports, {
53
55
  DawTransportButton: () => DawTransportButton,
54
56
  DawTransportElement: () => DawTransportElement,
55
57
  DawWaveformElement: () => DawWaveformElement,
56
- RecordingController: () => RecordingController
58
+ RecordingController: () => RecordingController,
59
+ splitAtPlayhead: () => splitAtPlayhead
57
60
  });
58
61
  module.exports = __toCommonJS(index_exports);
59
62
 
@@ -629,9 +632,40 @@ DawTransportButton.styles = import_lit6.css`
629
632
 
630
633
  // src/elements/daw-play-button.ts
631
634
  var DawPlayButtonElement = class extends DawTransportButton {
635
+ constructor() {
636
+ super(...arguments);
637
+ this._isRecording = false;
638
+ this._targetRef = null;
639
+ this._onRecStart = () => {
640
+ this._isRecording = true;
641
+ };
642
+ this._onRecEnd = () => {
643
+ this._isRecording = false;
644
+ };
645
+ }
646
+ connectedCallback() {
647
+ super.connectedCallback();
648
+ requestAnimationFrame(() => {
649
+ const target = this.target;
650
+ if (!target) return;
651
+ this._targetRef = target;
652
+ target.addEventListener("daw-recording-start", this._onRecStart);
653
+ target.addEventListener("daw-recording-complete", this._onRecEnd);
654
+ target.addEventListener("daw-recording-error", this._onRecEnd);
655
+ });
656
+ }
657
+ disconnectedCallback() {
658
+ super.disconnectedCallback();
659
+ if (this._targetRef) {
660
+ this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
661
+ this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
662
+ this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
663
+ this._targetRef = null;
664
+ }
665
+ }
632
666
  render() {
633
667
  return import_lit7.html`
634
- <button part="button" @click=${this._onClick}>
668
+ <button part="button" ?disabled=${this._isRecording} @click=${this._onClick}>
635
669
  <slot>Play</slot>
636
670
  </button>
637
671
  `;
@@ -647,6 +681,9 @@ var DawPlayButtonElement = class extends DawTransportButton {
647
681
  target.play();
648
682
  }
649
683
  };
684
+ __decorateClass([
685
+ (0, import_decorators6.state)()
686
+ ], DawPlayButtonElement.prototype, "_isRecording", 2);
650
687
  DawPlayButtonElement = __decorateClass([
651
688
  (0, import_decorators6.customElement)("daw-play-button")
652
689
  ], DawPlayButtonElement);
@@ -655,9 +692,42 @@ DawPlayButtonElement = __decorateClass([
655
692
  var import_lit8 = require("lit");
656
693
  var import_decorators7 = require("lit/decorators.js");
657
694
  var DawPauseButtonElement = class extends DawTransportButton {
695
+ constructor() {
696
+ super(...arguments);
697
+ this._isPaused = false;
698
+ this._isRecording = false;
699
+ this._targetRef = null;
700
+ this._onRecStart = () => {
701
+ this._isRecording = true;
702
+ };
703
+ this._onRecEnd = () => {
704
+ this._isRecording = false;
705
+ this._isPaused = false;
706
+ };
707
+ }
708
+ connectedCallback() {
709
+ super.connectedCallback();
710
+ requestAnimationFrame(() => {
711
+ const target = this.target;
712
+ if (!target) return;
713
+ this._targetRef = target;
714
+ target.addEventListener("daw-recording-start", this._onRecStart);
715
+ target.addEventListener("daw-recording-complete", this._onRecEnd);
716
+ target.addEventListener("daw-recording-error", this._onRecEnd);
717
+ });
718
+ }
719
+ disconnectedCallback() {
720
+ super.disconnectedCallback();
721
+ if (this._targetRef) {
722
+ this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
723
+ this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
724
+ this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
725
+ this._targetRef = null;
726
+ }
727
+ }
658
728
  render() {
659
729
  return import_lit8.html`
660
- <button part="button" @click=${this._onClick}>
730
+ <button part="button" ?data-paused=${this._isPaused} @click=${this._onClick}>
661
731
  <slot>Pause</slot>
662
732
  </button>
663
733
  `;
@@ -670,9 +740,36 @@ var DawPauseButtonElement = class extends DawTransportButton {
670
740
  );
671
741
  return;
672
742
  }
673
- target.pause();
743
+ if (this._isRecording) {
744
+ if (this._isPaused) {
745
+ target.resumeRecording();
746
+ target.play(target.currentTime);
747
+ this._isPaused = false;
748
+ } else {
749
+ target.pauseRecording();
750
+ target.pause();
751
+ this._isPaused = true;
752
+ }
753
+ } else {
754
+ target.pause();
755
+ }
674
756
  }
675
757
  };
758
+ DawPauseButtonElement.styles = [
759
+ DawTransportButton.styles,
760
+ import_lit8.css`
761
+ button[data-paused] {
762
+ background: rgba(255, 255, 255, 0.1);
763
+ border-color: var(--daw-controls-text, #e0d4c8);
764
+ }
765
+ `
766
+ ];
767
+ __decorateClass([
768
+ (0, import_decorators7.state)()
769
+ ], DawPauseButtonElement.prototype, "_isPaused", 2);
770
+ __decorateClass([
771
+ (0, import_decorators7.state)()
772
+ ], DawPauseButtonElement.prototype, "_isRecording", 2);
676
773
  DawPauseButtonElement = __decorateClass([
677
774
  (0, import_decorators7.customElement)("daw-pause-button")
678
775
  ], DawPauseButtonElement);
@@ -696,6 +793,9 @@ var DawStopButtonElement = class extends DawTransportButton {
696
793
  );
697
794
  return;
698
795
  }
796
+ if (target.isRecording) {
797
+ target.stopRecording();
798
+ }
699
799
  target.stop();
700
800
  }
701
801
  };
@@ -1027,20 +1127,22 @@ function extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, dura
1027
1127
 
1028
1128
  // src/workers/peakPipeline.ts
1029
1129
  var PeakPipeline = class {
1030
- constructor() {
1130
+ constructor(baseScale = 128, bits = 16) {
1031
1131
  this._worker = null;
1032
1132
  this._cache = /* @__PURE__ */ new WeakMap();
1033
1133
  this._inflight = /* @__PURE__ */ new WeakMap();
1134
+ this._baseScale = baseScale;
1135
+ this._bits = bits;
1034
1136
  }
1035
1137
  /**
1036
1138
  * Generate PeakData for a clip from its AudioBuffer.
1037
1139
  * Uses cached WaveformData when available; otherwise generates via worker.
1038
- * The worker generates at `scale` (= samplesPerPixel) for exact rendering.
1140
+ * Worker generates at baseScale (default 128); extractPeaks resamples to the requested zoom.
1039
1141
  */
1040
- async generatePeaks(audioBuffer, samplesPerPixel, isMono) {
1041
- const waveformData = await this._getWaveformData(audioBuffer, samplesPerPixel);
1142
+ async generatePeaks(audioBuffer, samplesPerPixel, isMono, offsetSamples, durationSamples) {
1143
+ const waveformData = await this._getWaveformData(audioBuffer);
1042
1144
  try {
1043
- return extractPeaks(waveformData, samplesPerPixel, isMono);
1145
+ return extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, durationSamples);
1044
1146
  } catch (err) {
1045
1147
  console.warn("[dawcore] extractPeaks failed: " + String(err));
1046
1148
  throw err;
@@ -1052,14 +1154,24 @@ var PeakPipeline = class {
1052
1154
  * Returns a new Map of clipId → PeakData. Clips without cached data or where
1053
1155
  * the target scale is finer than the cached base are skipped.
1054
1156
  */
1055
- reextractPeaks(clipBuffers, samplesPerPixel, isMono) {
1157
+ reextractPeaks(clipBuffers, samplesPerPixel, isMono, clipOffsets) {
1056
1158
  const result = /* @__PURE__ */ new Map();
1057
1159
  for (const [clipId, audioBuffer] of clipBuffers) {
1058
1160
  const cached = this._cache.get(audioBuffer);
1059
1161
  if (cached) {
1060
1162
  if (samplesPerPixel < cached.scale) continue;
1061
1163
  try {
1062
- result.set(clipId, extractPeaks(cached, samplesPerPixel, isMono));
1164
+ const offsets = clipOffsets?.get(clipId);
1165
+ result.set(
1166
+ clipId,
1167
+ extractPeaks(
1168
+ cached,
1169
+ samplesPerPixel,
1170
+ isMono,
1171
+ offsets?.offsetSamples,
1172
+ offsets?.durationSamples
1173
+ )
1174
+ );
1063
1175
  } catch (err) {
1064
1176
  console.warn("[dawcore] reextractPeaks failed for clip " + clipId + ": " + String(err));
1065
1177
  }
@@ -1071,9 +1183,9 @@ var PeakPipeline = class {
1071
1183
  this._worker?.terminate();
1072
1184
  this._worker = null;
1073
1185
  }
1074
- async _getWaveformData(audioBuffer, samplesPerPixel) {
1186
+ async _getWaveformData(audioBuffer) {
1075
1187
  const cached = this._cache.get(audioBuffer);
1076
- if (cached && cached.scale <= samplesPerPixel) return cached;
1188
+ if (cached) return cached;
1077
1189
  const inflight = this._inflight.get(audioBuffer);
1078
1190
  if (inflight) return inflight;
1079
1191
  if (!this._worker) {
@@ -1087,8 +1199,8 @@ var PeakPipeline = class {
1087
1199
  channels,
1088
1200
  length: audioBuffer.length,
1089
1201
  sampleRate: audioBuffer.sampleRate,
1090
- scale: samplesPerPixel,
1091
- bits: 16,
1202
+ scale: this._baseScale,
1203
+ bits: this._bits,
1092
1204
  splitChannels: true
1093
1205
  }).then((waveformData) => {
1094
1206
  this._cache.set(audioBuffer, waveformData);
@@ -1209,7 +1321,7 @@ DawTrackControlsElement.styles = import_lit10.css`
1209
1321
  :host {
1210
1322
  display: flex;
1211
1323
  flex-direction: column;
1212
- justify-content: center;
1324
+ justify-content: flex-start;
1213
1325
  box-sizing: border-box;
1214
1326
  padding: 6px 8px;
1215
1327
  background: var(--daw-controls-background, #0f0f1a);
@@ -1217,13 +1329,14 @@ DawTrackControlsElement.styles = import_lit10.css`
1217
1329
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1218
1330
  font-family: system-ui, sans-serif;
1219
1331
  font-size: 11px;
1332
+ overflow: hidden;
1220
1333
  }
1221
1334
  .header {
1222
1335
  display: flex;
1223
1336
  align-items: center;
1224
1337
  justify-content: space-between;
1225
1338
  gap: 4px;
1226
- margin-bottom: 6px;
1339
+ margin-bottom: 3px;
1227
1340
  }
1228
1341
  .name {
1229
1342
  flex: 1;
@@ -1250,7 +1363,7 @@ DawTrackControlsElement.styles = import_lit10.css`
1250
1363
  .buttons {
1251
1364
  display: flex;
1252
1365
  gap: 3px;
1253
- margin-bottom: 6px;
1366
+ margin-bottom: 3px;
1254
1367
  }
1255
1368
  .btn {
1256
1369
  background: rgba(255, 255, 255, 0.06);
@@ -1280,7 +1393,7 @@ DawTrackControlsElement.styles = import_lit10.css`
1280
1393
  display: flex;
1281
1394
  align-items: center;
1282
1395
  gap: 4px;
1283
- height: 20px;
1396
+ height: 16px;
1284
1397
  }
1285
1398
  .slider-label {
1286
1399
  width: 50px;
@@ -1377,6 +1490,77 @@ var hostStyles = import_lit11.css`
1377
1490
  --daw-clip-header-text: #e0d4c8;
1378
1491
  }
1379
1492
  `;
1493
+ var clipStyles = import_lit11.css`
1494
+ .clip-container {
1495
+ position: absolute;
1496
+ overflow: hidden;
1497
+ }
1498
+ .clip-header {
1499
+ position: relative;
1500
+ z-index: 1;
1501
+ height: 20px;
1502
+ background: var(--daw-clip-header-background, rgba(0, 0, 0, 0.4));
1503
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
1504
+ display: flex;
1505
+ align-items: center;
1506
+ padding: 0 6px;
1507
+ user-select: none;
1508
+ -webkit-user-drag: none;
1509
+ }
1510
+ .clip-header span {
1511
+ font-size: 10px;
1512
+ font-weight: 500;
1513
+ letter-spacing: 0.02em;
1514
+ font-family: system-ui, sans-serif;
1515
+ color: var(--daw-clip-header-text, #e0d4c8);
1516
+ white-space: nowrap;
1517
+ overflow: hidden;
1518
+ text-overflow: ellipsis;
1519
+ opacity: 0.8;
1520
+ }
1521
+ .clip-boundary {
1522
+ position: absolute;
1523
+ top: 0;
1524
+ width: 8px;
1525
+ height: 100%;
1526
+ z-index: 2;
1527
+ cursor: col-resize;
1528
+ background: transparent;
1529
+ border: none;
1530
+ touch-action: none;
1531
+ user-select: none;
1532
+ -webkit-user-drag: none;
1533
+ transition: background 0.1s, border-color 0.1s;
1534
+ }
1535
+ .clip-boundary[data-boundary-edge='left'] {
1536
+ left: 0;
1537
+ }
1538
+ .clip-boundary[data-boundary-edge='right'] {
1539
+ right: 0;
1540
+ }
1541
+ .clip-boundary[data-boundary-edge='left']:hover {
1542
+ background: rgba(255, 255, 255, 0.2);
1543
+ border-left: 2px solid rgba(255, 255, 255, 0.5);
1544
+ }
1545
+ .clip-boundary[data-boundary-edge='right']:hover {
1546
+ background: rgba(255, 255, 255, 0.2);
1547
+ border-right: 2px solid rgba(255, 255, 255, 0.5);
1548
+ }
1549
+ .clip-boundary[data-boundary-edge='left'].dragging {
1550
+ background: rgba(255, 255, 255, 0.4);
1551
+ border-left: 2px solid rgba(255, 255, 255, 0.8);
1552
+ }
1553
+ .clip-boundary[data-boundary-edge='right'].dragging {
1554
+ background: rgba(255, 255, 255, 0.4);
1555
+ border-right: 2px solid rgba(255, 255, 255, 0.8);
1556
+ }
1557
+ .clip-header[data-interactive] {
1558
+ cursor: grab;
1559
+ }
1560
+ .clip-header[data-interactive]:active {
1561
+ cursor: grabbing;
1562
+ }
1563
+ `;
1380
1564
 
1381
1565
  // src/controllers/viewport-controller.ts
1382
1566
  var OVERSCAN_MULTIPLIER = 1.5;
@@ -1559,6 +1743,9 @@ var RecordingController = class {
1559
1743
  }
1560
1744
  const channelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount ?? 1;
1561
1745
  const startSample = options.startSample ?? Math.floor(this._host._currentTime * this._host.effectiveSampleRate);
1746
+ const outputLatency = rawCtx.outputLatency ?? 0;
1747
+ const lookAhead = context.lookAhead ?? 0;
1748
+ const latencySamples = Math.floor((outputLatency + lookAhead) * rawCtx.sampleRate);
1562
1749
  const source = context.createMediaStreamSource(stream);
1563
1750
  const workletNode = context.createAudioWorkletNode("recording-processor", {
1564
1751
  channelCount,
@@ -1585,6 +1772,8 @@ var RecordingController = class {
1585
1772
  channelCount,
1586
1773
  bits,
1587
1774
  isFirstMessage: true,
1775
+ latencySamples,
1776
+ wasOverdub: options.overdub ?? false,
1588
1777
  _onTrackEnded: onTrackEnded,
1589
1778
  _audioTrack: audioTrack
1590
1779
  };
@@ -1605,6 +1794,9 @@ var RecordingController = class {
1605
1794
  })
1606
1795
  );
1607
1796
  this._host.requestUpdate();
1797
+ if (options.overdub && typeof this._host.play === "function") {
1798
+ await this._host.play(this._host._currentTime);
1799
+ }
1608
1800
  } catch (err) {
1609
1801
  this._cleanupSession(trackId);
1610
1802
  console.warn("[dawcore] RecordingController: Failed to start recording: " + String(err));
@@ -1617,11 +1809,28 @@ var RecordingController = class {
1617
1809
  );
1618
1810
  }
1619
1811
  }
1812
+ pauseRecording(trackId) {
1813
+ const id = trackId ?? [...this._sessions.keys()][0];
1814
+ if (!id) return;
1815
+ const session = this._sessions.get(id);
1816
+ if (!session) return;
1817
+ session.workletNode.port.postMessage({ command: "pause" });
1818
+ }
1819
+ resumeRecording(trackId) {
1820
+ const id = trackId ?? [...this._sessions.keys()][0];
1821
+ if (!id) return;
1822
+ const session = this._sessions.get(id);
1823
+ if (!session) return;
1824
+ session.workletNode.port.postMessage({ command: "resume" });
1825
+ }
1620
1826
  stopRecording(trackId) {
1621
1827
  const id = trackId ?? [...this._sessions.keys()][0];
1622
1828
  if (!id) return;
1623
1829
  const session = this._sessions.get(id);
1624
1830
  if (!session) return;
1831
+ if (session.wasOverdub && typeof this._host.stop === "function") {
1832
+ this._host.stop();
1833
+ }
1625
1834
  session.workletNode.port.postMessage({ command: "stop" });
1626
1835
  session.source.disconnect();
1627
1836
  session.workletNode.disconnect();
@@ -1639,7 +1848,8 @@ var RecordingController = class {
1639
1848
  );
1640
1849
  return;
1641
1850
  }
1642
- const stopCtx = (0, import_playout2.getGlobalContext)().rawContext;
1851
+ const context = (0, import_playout2.getGlobalContext)();
1852
+ const stopCtx = context.rawContext;
1643
1853
  const channelData = session.chunks.map((chunkArr) => (0, import_recording.concatenateAudioData)(chunkArr));
1644
1854
  const audioBuffer = (0, import_recording.createAudioBuffer)(
1645
1855
  stopCtx,
@@ -1647,7 +1857,21 @@ var RecordingController = class {
1647
1857
  this._host.effectiveSampleRate,
1648
1858
  session.channelCount
1649
1859
  );
1650
- const durationSamples = audioBuffer.length;
1860
+ const latencyOffsetSamples = session.latencySamples;
1861
+ const effectiveDuration = Math.max(0, audioBuffer.length - latencyOffsetSamples);
1862
+ if (effectiveDuration === 0) {
1863
+ console.warn("[dawcore] RecordingController: Recording too short for latency compensation");
1864
+ this._sessions.delete(id);
1865
+ this._host.requestUpdate();
1866
+ this._host.dispatchEvent(
1867
+ new CustomEvent("daw-recording-error", {
1868
+ bubbles: true,
1869
+ composed: true,
1870
+ detail: { trackId: id, error: new Error("Recording too short to save") }
1871
+ })
1872
+ );
1873
+ return;
1874
+ }
1651
1875
  const event = new CustomEvent("daw-recording-complete", {
1652
1876
  bubbles: true,
1653
1877
  composed: true,
@@ -1656,14 +1880,21 @@ var RecordingController = class {
1656
1880
  trackId: id,
1657
1881
  audioBuffer,
1658
1882
  startSample: session.startSample,
1659
- durationSamples
1883
+ durationSamples: effectiveDuration,
1884
+ offsetSamples: latencyOffsetSamples
1660
1885
  }
1661
1886
  });
1662
1887
  const notPrevented = this._host.dispatchEvent(event);
1663
1888
  this._sessions.delete(id);
1664
1889
  this._host.requestUpdate();
1665
1890
  if (notPrevented) {
1666
- this._createClipFromRecording(id, audioBuffer, session.startSample, durationSamples);
1891
+ this._createClipFromRecording(
1892
+ id,
1893
+ audioBuffer,
1894
+ session.startSample,
1895
+ effectiveDuration,
1896
+ latencyOffsetSamples
1897
+ );
1667
1898
  }
1668
1899
  }
1669
1900
  // Session fields are mutated in place on the hot path (~60fps worklet messages).
@@ -1694,7 +1925,9 @@ var RecordingController = class {
1694
1925
  );
1695
1926
  const newPeakCount = Math.floor(session.peaks[ch].length / 2);
1696
1927
  const waveformSelector = `daw-waveform[data-recording-track="${trackId}"][data-recording-channel="${ch}"]`;
1697
- const waveformEl = this._host.shadowRoot?.querySelector(waveformSelector);
1928
+ const waveformEl = this._host.shadowRoot?.querySelector(
1929
+ waveformSelector
1930
+ );
1698
1931
  if (waveformEl) {
1699
1932
  if (session.isFirstMessage) {
1700
1933
  waveformEl.peaks = session.peaks[ch];
@@ -1713,9 +1946,15 @@ var RecordingController = class {
1713
1946
  this._host.requestUpdate();
1714
1947
  }
1715
1948
  }
1716
- _createClipFromRecording(trackId, audioBuffer, startSample, durationSamples) {
1949
+ _createClipFromRecording(trackId, audioBuffer, startSample, durationSamples, offsetSamples = 0) {
1717
1950
  if (typeof this._host._addRecordedClip === "function") {
1718
- this._host._addRecordedClip(trackId, audioBuffer, startSample, durationSamples);
1951
+ this._host._addRecordedClip(
1952
+ trackId,
1953
+ audioBuffer,
1954
+ startSample,
1955
+ durationSamples,
1956
+ offsetSamples
1957
+ );
1719
1958
  } else {
1720
1959
  console.warn(
1721
1960
  '[dawcore] RecordingController: host does not implement _addRecordedClip \u2014 clip not created for track "' + trackId + '"'
@@ -1746,6 +1985,11 @@ var RecordingController = class {
1746
1985
 
1747
1986
  // src/interactions/pointer-handler.ts
1748
1987
  var import_core = require("@waveform-playlist/core");
1988
+
1989
+ // src/interactions/constants.ts
1990
+ var DRAG_THRESHOLD = 3;
1991
+
1992
+ // src/interactions/pointer-handler.ts
1749
1993
  var PointerHandler = class {
1750
1994
  constructor(host) {
1751
1995
  this._isDragging = false;
@@ -1754,6 +1998,34 @@ var PointerHandler = class {
1754
1998
  // Cached from onPointerDown to avoid forced layout reflows at 60fps during drag
1755
1999
  this._timelineRect = null;
1756
2000
  this.onPointerDown = (e) => {
2001
+ const clipHandler = this._host._clipHandler;
2002
+ if (clipHandler) {
2003
+ const target = e.composedPath()[0];
2004
+ if (target && clipHandler.tryHandle(target, e)) {
2005
+ e.preventDefault();
2006
+ this._timeline = this._host.shadowRoot?.querySelector(".timeline");
2007
+ if (this._timeline) {
2008
+ this._timeline.setPointerCapture(e.pointerId);
2009
+ const onMove = (me) => clipHandler.onPointerMove(me);
2010
+ const onUp = (ue) => {
2011
+ clipHandler.onPointerUp(ue);
2012
+ this._timeline?.removeEventListener("pointermove", onMove);
2013
+ this._timeline?.removeEventListener("pointerup", onUp);
2014
+ try {
2015
+ this._timeline?.releasePointerCapture(ue.pointerId);
2016
+ } catch (err) {
2017
+ console.warn(
2018
+ "[dawcore] releasePointerCapture failed (may already be released): " + String(err)
2019
+ );
2020
+ }
2021
+ this._timeline = null;
2022
+ };
2023
+ this._timeline.addEventListener("pointermove", onMove);
2024
+ this._timeline.addEventListener("pointerup", onUp);
2025
+ }
2026
+ return;
2027
+ }
2028
+ }
1757
2029
  this._timeline = this._host.shadowRoot?.querySelector(".timeline");
1758
2030
  if (!this._timeline) return;
1759
2031
  this._timelineRect = this._timeline.getBoundingClientRect();
@@ -1766,7 +2038,7 @@ var PointerHandler = class {
1766
2038
  this._onPointerMove = (e) => {
1767
2039
  if (!this._timeline) return;
1768
2040
  const currentPx = this._pxFromPointer(e);
1769
- if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > 3) {
2041
+ if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > DRAG_THRESHOLD) {
1770
2042
  this._isDragging = true;
1771
2043
  }
1772
2044
  if (this._isDragging) {
@@ -1901,6 +2173,261 @@ var PointerHandler = class {
1901
2173
  }
1902
2174
  };
1903
2175
 
2176
+ // src/interactions/clip-pointer-handler.ts
2177
+ var ClipPointerHandler = class {
2178
+ constructor(host) {
2179
+ this._mode = null;
2180
+ this._clipId = "";
2181
+ this._trackId = "";
2182
+ this._startPx = 0;
2183
+ this._isDragging = false;
2184
+ this._lastDeltaPx = 0;
2185
+ this._cumulativeDeltaSamples = 0;
2186
+ // Trim visual feedback: snapshot of original clip state
2187
+ this._clipContainer = null;
2188
+ this._boundaryEl = null;
2189
+ this._originalLeft = 0;
2190
+ this._originalWidth = 0;
2191
+ this._originalOffsetSamples = 0;
2192
+ this._originalDurationSamples = 0;
2193
+ this._host = host;
2194
+ }
2195
+ /** Returns true if a drag interaction is currently in progress. */
2196
+ get isActive() {
2197
+ return this._mode !== null;
2198
+ }
2199
+ /**
2200
+ * Attempts to handle a pointerdown event on the given target element.
2201
+ * Returns true if the target is a recognized clip interaction element.
2202
+ */
2203
+ tryHandle(target, e) {
2204
+ if (!this._host.interactiveClips) return false;
2205
+ const boundary = target.closest?.(".clip-boundary");
2206
+ const header = target.closest?.(".clip-header");
2207
+ if (boundary && boundary.dataset.boundaryEdge !== void 0) {
2208
+ const clipId = boundary.dataset.clipId;
2209
+ const trackId = boundary.dataset.trackId;
2210
+ const edge = boundary.dataset.boundaryEdge;
2211
+ if (!clipId || !trackId || edge !== "left" && edge !== "right") return false;
2212
+ this._beginDrag(edge === "left" ? "trim-left" : "trim-right", clipId, trackId, e);
2213
+ this._boundaryEl = boundary;
2214
+ return true;
2215
+ }
2216
+ if (header && header.dataset.interactive !== void 0) {
2217
+ const clipId = header.dataset.clipId;
2218
+ const trackId = header.dataset.trackId;
2219
+ if (!clipId || !trackId) return false;
2220
+ this._beginDrag("move", clipId, trackId, e);
2221
+ return true;
2222
+ }
2223
+ return false;
2224
+ }
2225
+ _beginDrag(mode, clipId, trackId, e) {
2226
+ this._mode = mode;
2227
+ this._clipId = clipId;
2228
+ this._trackId = trackId;
2229
+ this._startPx = e.clientX;
2230
+ this._isDragging = false;
2231
+ this._lastDeltaPx = 0;
2232
+ this._cumulativeDeltaSamples = 0;
2233
+ if (this._host.engine) {
2234
+ this._host.engine.beginTransaction();
2235
+ } else {
2236
+ console.warn(
2237
+ "[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
2238
+ );
2239
+ }
2240
+ if (mode === "trim-left" || mode === "trim-right") {
2241
+ const container = this._host.shadowRoot?.querySelector(
2242
+ `.clip-container[data-clip-id="${clipId}"]`
2243
+ );
2244
+ if (container) {
2245
+ this._clipContainer = container;
2246
+ this._originalLeft = parseFloat(container.style.left) || 0;
2247
+ this._originalWidth = parseFloat(container.style.width) || 0;
2248
+ } else {
2249
+ console.warn("[dawcore] clip container not found for trim visual feedback: " + clipId);
2250
+ }
2251
+ const engine = this._host.engine;
2252
+ if (engine) {
2253
+ const bounds = engine.getClipBounds(trackId, clipId);
2254
+ if (bounds) {
2255
+ this._originalOffsetSamples = bounds.offsetSamples;
2256
+ this._originalDurationSamples = bounds.durationSamples;
2257
+ }
2258
+ }
2259
+ }
2260
+ }
2261
+ /** Processes pointermove events during an active drag. */
2262
+ onPointerMove(e) {
2263
+ if (this._mode === null) return;
2264
+ const totalDeltaPx = e.clientX - this._startPx;
2265
+ if (!this._isDragging && Math.abs(totalDeltaPx) > DRAG_THRESHOLD) {
2266
+ this._isDragging = true;
2267
+ if (this._boundaryEl) {
2268
+ this._boundaryEl.classList.add("dragging");
2269
+ }
2270
+ }
2271
+ if (!this._isDragging) return;
2272
+ const engine = this._host.engine;
2273
+ if (!engine) return;
2274
+ if (this._mode === "move") {
2275
+ const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
2276
+ this._lastDeltaPx = totalDeltaPx;
2277
+ const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
2278
+ const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2279
+ this._cumulativeDeltaSamples += applied;
2280
+ } else {
2281
+ const boundary = this._mode === "trim-left" ? "left" : "right";
2282
+ const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
2283
+ const deltaSamples = engine.constrainTrimDelta(
2284
+ this._trackId,
2285
+ this._clipId,
2286
+ boundary,
2287
+ rawDeltaSamples
2288
+ );
2289
+ const deltaPx = Math.round(deltaSamples / this._host.samplesPerPixel);
2290
+ this._cumulativeDeltaSamples = deltaSamples;
2291
+ if (this._clipContainer) {
2292
+ if (this._mode === "trim-left") {
2293
+ const newLeft = this._originalLeft + deltaPx;
2294
+ const newWidth = this._originalWidth - deltaPx;
2295
+ if (newWidth > 0) {
2296
+ this._clipContainer.style.left = newLeft + "px";
2297
+ this._clipContainer.style.width = newWidth + "px";
2298
+ const newOffset = this._originalOffsetSamples + deltaSamples;
2299
+ const newDuration = this._originalDurationSamples - deltaSamples;
2300
+ if (this._updateWaveformPeaks(newOffset, newDuration)) {
2301
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2302
+ for (const wf of waveforms) {
2303
+ wf.style.left = "0px";
2304
+ }
2305
+ } else {
2306
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2307
+ for (const wf of waveforms) {
2308
+ wf.style.left = -deltaPx + "px";
2309
+ }
2310
+ }
2311
+ }
2312
+ } else {
2313
+ const newWidth = this._originalWidth + deltaPx;
2314
+ if (newWidth > 0) {
2315
+ this._clipContainer.style.width = newWidth + "px";
2316
+ const newDuration = this._originalDurationSamples + deltaSamples;
2317
+ this._updateWaveformPeaks(this._originalOffsetSamples, newDuration);
2318
+ }
2319
+ }
2320
+ }
2321
+ }
2322
+ }
2323
+ /** Processes pointerup events to finalize and dispatch result events. */
2324
+ onPointerUp(_e) {
2325
+ if (this._mode === null) return;
2326
+ try {
2327
+ if (!this._isDragging || this._cumulativeDeltaSamples === 0) {
2328
+ this._restoreTrimVisual();
2329
+ return;
2330
+ }
2331
+ const engine = this._host.engine;
2332
+ if (this._mode === "move") {
2333
+ if (engine) {
2334
+ engine.updateTrack(this._trackId);
2335
+ this._host.dispatchEvent(
2336
+ new CustomEvent("daw-clip-move", {
2337
+ bubbles: true,
2338
+ composed: true,
2339
+ detail: {
2340
+ trackId: this._trackId,
2341
+ clipId: this._clipId,
2342
+ deltaSamples: this._cumulativeDeltaSamples
2343
+ }
2344
+ })
2345
+ );
2346
+ } else {
2347
+ console.warn(
2348
+ "[dawcore] engine unavailable at move drop \u2014 audio may be out of sync for track " + this._trackId
2349
+ );
2350
+ }
2351
+ } else {
2352
+ this._restoreTrimVisual();
2353
+ const boundary = this._mode === "trim-left" ? "left" : "right";
2354
+ if (engine) {
2355
+ engine.trimClip(this._trackId, this._clipId, boundary, this._cumulativeDeltaSamples);
2356
+ this._host.dispatchEvent(
2357
+ new CustomEvent("daw-clip-trim", {
2358
+ bubbles: true,
2359
+ composed: true,
2360
+ detail: {
2361
+ trackId: this._trackId,
2362
+ clipId: this._clipId,
2363
+ boundary,
2364
+ deltaSamples: this._cumulativeDeltaSamples
2365
+ }
2366
+ })
2367
+ );
2368
+ } else {
2369
+ console.warn(
2370
+ "[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
2371
+ );
2372
+ }
2373
+ }
2374
+ } finally {
2375
+ if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
2376
+ this._host.engine?.commitTransaction();
2377
+ } else {
2378
+ this._host.engine?.abortTransaction();
2379
+ }
2380
+ this._reset();
2381
+ }
2382
+ }
2383
+ /** Re-extract peaks from cache and set on waveform elements during trim drag.
2384
+ * Returns true if peaks were successfully updated. */
2385
+ _updateWaveformPeaks(offsetSamples, durationSamples) {
2386
+ if (!this._clipContainer || durationSamples <= 0) return false;
2387
+ const peakSlice = this._host.reextractClipPeaks(this._clipId, offsetSamples, durationSamples);
2388
+ if (!peakSlice) return false;
2389
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2390
+ for (let i = 0; i < waveforms.length; i++) {
2391
+ const wf = waveforms[i];
2392
+ const channelPeaks = peakSlice.data[i];
2393
+ if (channelPeaks) {
2394
+ wf.peaks = channelPeaks;
2395
+ wf.length = peakSlice.length;
2396
+ }
2397
+ }
2398
+ return true;
2399
+ }
2400
+ /** Restore clip container CSS to original values after trim visual preview. */
2401
+ _restoreTrimVisual() {
2402
+ if (this._clipContainer) {
2403
+ this._clipContainer.style.left = this._originalLeft + "px";
2404
+ this._clipContainer.style.width = this._originalWidth + "px";
2405
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2406
+ for (const wf of waveforms) {
2407
+ wf.style.left = "0px";
2408
+ }
2409
+ }
2410
+ }
2411
+ _reset() {
2412
+ if (this._boundaryEl) {
2413
+ this._boundaryEl.classList.remove("dragging");
2414
+ this._boundaryEl = null;
2415
+ }
2416
+ this._mode = null;
2417
+ this._clipId = "";
2418
+ this._trackId = "";
2419
+ this._startPx = 0;
2420
+ this._isDragging = false;
2421
+ this._lastDeltaPx = 0;
2422
+ this._cumulativeDeltaSamples = 0;
2423
+ this._clipContainer = null;
2424
+ this._originalLeft = 0;
2425
+ this._originalWidth = 0;
2426
+ this._originalOffsetSamples = 0;
2427
+ this._originalDurationSamples = 0;
2428
+ }
2429
+ };
2430
+
1904
2431
  // src/interactions/file-loader.ts
1905
2432
  var import_core2 = require("@waveform-playlist/core");
1906
2433
  async function loadFiles(host, files) {
@@ -1935,10 +2462,16 @@ async function loadFiles(host, files) {
1935
2462
  sourceDuration: audioBuffer.duration
1936
2463
  });
1937
2464
  host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
2465
+ host._clipOffsets.set(clip.id, {
2466
+ offsetSamples: clip.offsetSamples,
2467
+ durationSamples: clip.durationSamples
2468
+ });
1938
2469
  const peakData = await host._peakPipeline.generatePeaks(
1939
2470
  audioBuffer,
1940
2471
  host.samplesPerPixel,
1941
- host.mono
2472
+ host.mono,
2473
+ clip.offsetSamples,
2474
+ clip.durationSamples
1942
2475
  );
1943
2476
  host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
1944
2477
  const trackId = crypto.randomUUID();
@@ -1997,17 +2530,31 @@ async function loadFiles(host, files) {
1997
2530
 
1998
2531
  // src/interactions/recording-clip.ts
1999
2532
  var import_core3 = require("@waveform-playlist/core");
2000
- function addRecordedClip(host, trackId, buf, startSample, durSamples) {
2533
+ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
2534
+ let trimmedBuf = buf;
2535
+ if (offsetSamples > 0 && offsetSamples < buf.length) {
2536
+ const trimmed = new AudioBuffer({
2537
+ numberOfChannels: buf.numberOfChannels,
2538
+ length: durSamples,
2539
+ sampleRate: buf.sampleRate
2540
+ });
2541
+ for (let ch = 0; ch < buf.numberOfChannels; ch++) {
2542
+ const source = buf.getChannelData(ch);
2543
+ trimmed.copyToChannel(source.subarray(offsetSamples, offsetSamples + durSamples), ch);
2544
+ }
2545
+ trimmedBuf = trimmed;
2546
+ }
2001
2547
  const clip = (0, import_core3.createClip)({
2002
- audioBuffer: buf,
2548
+ audioBuffer: trimmedBuf,
2003
2549
  startSample,
2004
2550
  durationSamples: durSamples,
2005
2551
  offsetSamples: 0,
2552
+ // offset already applied by slicing
2006
2553
  gain: 1,
2007
2554
  name: "Recording"
2008
2555
  });
2009
- host._clipBuffers = new Map(host._clipBuffers).set(clip.id, buf);
2010
- host._peakPipeline.generatePeaks(buf, host.samplesPerPixel, host.mono).then((pd) => {
2556
+ host._clipBuffers = new Map(host._clipBuffers).set(clip.id, trimmedBuf);
2557
+ host._peakPipeline.generatePeaks(trimmedBuf, host.samplesPerPixel, host.mono).then((pd) => {
2011
2558
  host._peaksData = new Map(host._peaksData).set(clip.id, pd);
2012
2559
  const t = host._engineTracks.get(trackId);
2013
2560
  if (!t) {
@@ -2040,7 +2587,12 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
2040
2587
  });
2041
2588
  }
2042
2589
  host._recomputeDuration();
2043
- host._engine?.setTracks([...host._engineTracks.values()]);
2590
+ const updatedTrack = host._engineTracks.get(trackId);
2591
+ if (host._engine?.updateTrack && updatedTrack) {
2592
+ host._engine.updateTrack(trackId, updatedTrack);
2593
+ } else {
2594
+ host._engine?.setTracks([...host._engineTracks.values()]);
2595
+ }
2044
2596
  }).catch((err) => {
2045
2597
  console.warn("[dawcore] Failed to generate peaks for recorded clip: " + String(err));
2046
2598
  const next = new Map(host._clipBuffers);
@@ -2058,6 +2610,169 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
2058
2610
  });
2059
2611
  }
2060
2612
 
2613
+ // src/interactions/split-handler.ts
2614
+ function splitAtPlayhead(host) {
2615
+ const wasPlaying = host.isPlaying;
2616
+ const time = host.currentTime;
2617
+ if (!canSplitAtTime(host, time)) return false;
2618
+ if (wasPlaying) {
2619
+ host.stop();
2620
+ }
2621
+ let result;
2622
+ try {
2623
+ result = performSplit(host, time);
2624
+ } catch (err) {
2625
+ console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
2626
+ result = false;
2627
+ }
2628
+ if (wasPlaying) {
2629
+ host.play(time);
2630
+ }
2631
+ return result;
2632
+ }
2633
+ function canSplitAtTime(host, time) {
2634
+ const { engine } = host;
2635
+ if (!engine) return false;
2636
+ const state5 = engine.getState();
2637
+ if (!state5.selectedTrackId) return false;
2638
+ const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
2639
+ if (!track) return false;
2640
+ const atSample = Math.round(time * host.effectiveSampleRate);
2641
+ return !!findClipAtSample(track.clips, atSample);
2642
+ }
2643
+ function performSplit(host, time) {
2644
+ const { engine } = host;
2645
+ if (!engine) return false;
2646
+ const stateBefore = engine.getState();
2647
+ const { selectedTrackId, tracks } = stateBefore;
2648
+ if (!selectedTrackId) return false;
2649
+ const track = tracks.find((t) => t.id === selectedTrackId);
2650
+ if (!track) return false;
2651
+ const atSample = Math.round(time * host.effectiveSampleRate);
2652
+ const clip = findClipAtSample(track.clips, atSample);
2653
+ if (!clip) return false;
2654
+ const originalClipId = clip.id;
2655
+ const clipIdsBefore = new Set(track.clips.map((c) => c.id));
2656
+ engine.splitClip(selectedTrackId, originalClipId, atSample);
2657
+ const stateAfter = engine.getState();
2658
+ const trackAfter = stateAfter.tracks.find((t) => t.id === selectedTrackId);
2659
+ if (!trackAfter) {
2660
+ console.warn(
2661
+ '[dawcore] splitAtPlayhead: track "' + selectedTrackId + '" disappeared after split'
2662
+ );
2663
+ return false;
2664
+ }
2665
+ const newClips = trackAfter.clips.filter((c) => !clipIdsBefore.has(c.id));
2666
+ if (newClips.length !== 2) {
2667
+ if (newClips.length > 0) {
2668
+ console.warn(
2669
+ "[dawcore] splitAtPlayhead: expected 2 new clips after split but got " + newClips.length
2670
+ );
2671
+ }
2672
+ return false;
2673
+ }
2674
+ const sorted = [...newClips].sort((a, b) => a.startSample - b.startSample);
2675
+ const leftClipId = sorted[0].id;
2676
+ const rightClipId = sorted[1].id;
2677
+ host.dispatchEvent(
2678
+ new CustomEvent("daw-clip-split", {
2679
+ bubbles: true,
2680
+ composed: true,
2681
+ detail: {
2682
+ trackId: selectedTrackId,
2683
+ originalClipId,
2684
+ leftClipId,
2685
+ rightClipId
2686
+ }
2687
+ })
2688
+ );
2689
+ return true;
2690
+ }
2691
+ function findClipAtSample(clips, atSample) {
2692
+ return clips.find(
2693
+ (c) => atSample > c.startSample && atSample < c.startSample + c.durationSamples
2694
+ );
2695
+ }
2696
+
2697
+ // src/interactions/clip-peak-sync.ts
2698
+ function syncPeaksForChangedClips(host, tracks) {
2699
+ const currentClipIds = /* @__PURE__ */ new Set();
2700
+ for (const track of tracks) {
2701
+ for (const clip of track.clips) {
2702
+ currentClipIds.add(clip.id);
2703
+ const cached = host._clipOffsets.get(clip.id);
2704
+ const needsPeaks = !host._peaksData.has(clip.id) || !cached || cached.offsetSamples !== clip.offsetSamples || cached.durationSamples !== clip.durationSamples;
2705
+ if (!needsPeaks) continue;
2706
+ const audioBuffer = clip.audioBuffer ?? host._clipBuffers.get(clip.id) ?? findAudioBufferForClip(host, clip, track);
2707
+ if (!audioBuffer) {
2708
+ console.warn(
2709
+ "[dawcore] syncPeaksForChangedClips: no AudioBuffer for clip " + clip.id + " \u2014 waveform will be blank"
2710
+ );
2711
+ continue;
2712
+ }
2713
+ host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
2714
+ host._clipOffsets.set(clip.id, {
2715
+ offsetSamples: clip.offsetSamples,
2716
+ durationSamples: clip.durationSamples
2717
+ });
2718
+ host._peakPipeline.generatePeaks(
2719
+ audioBuffer,
2720
+ host.samplesPerPixel,
2721
+ host.mono,
2722
+ clip.offsetSamples,
2723
+ clip.durationSamples
2724
+ ).then((peakData) => {
2725
+ host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
2726
+ }).catch((err) => {
2727
+ console.warn(
2728
+ "[dawcore] Failed to generate peaks for clip " + clip.id + ": " + String(err)
2729
+ );
2730
+ });
2731
+ }
2732
+ }
2733
+ cleanupOrphanedClipData(host, currentClipIds);
2734
+ }
2735
+ function cleanupOrphanedClipData(host, currentClipIds) {
2736
+ let buffersChanged = false;
2737
+ let peaksChanged = false;
2738
+ for (const id of host._clipBuffers.keys()) {
2739
+ if (!currentClipIds.has(id)) {
2740
+ host._clipBuffers.delete(id);
2741
+ buffersChanged = true;
2742
+ }
2743
+ }
2744
+ let offsetsChanged = false;
2745
+ for (const id of host._clipOffsets.keys()) {
2746
+ if (!currentClipIds.has(id)) {
2747
+ host._clipOffsets.delete(id);
2748
+ offsetsChanged = true;
2749
+ }
2750
+ }
2751
+ for (const id of host._peaksData.keys()) {
2752
+ if (!currentClipIds.has(id)) {
2753
+ host._peaksData.delete(id);
2754
+ peaksChanged = true;
2755
+ }
2756
+ }
2757
+ if (buffersChanged) {
2758
+ host._clipBuffers = new Map(host._clipBuffers);
2759
+ }
2760
+ if (offsetsChanged) {
2761
+ host._clipOffsets = new Map(host._clipOffsets);
2762
+ }
2763
+ if (peaksChanged) {
2764
+ host._peaksData = new Map(host._peaksData);
2765
+ }
2766
+ }
2767
+ function findAudioBufferForClip(host, clip, track) {
2768
+ for (const sibling of track.clips) {
2769
+ if (sibling.id === clip.id) continue;
2770
+ const buf = host._clipBuffers.get(sibling.id);
2771
+ if (buf) return buf;
2772
+ }
2773
+ return null;
2774
+ }
2775
+
2061
2776
  // src/elements/daw-editor.ts
2062
2777
  var DawEditorElement = class extends import_lit12.LitElement {
2063
2778
  constructor() {
@@ -2069,6 +2784,9 @@ var DawEditorElement = class extends import_lit12.LitElement {
2069
2784
  this.barWidth = 1;
2070
2785
  this.barGap = 0;
2071
2786
  this.fileDrop = false;
2787
+ this.clipHeaders = false;
2788
+ this.clipHeaderHeight = 20;
2789
+ this.interactiveClips = false;
2072
2790
  this.sampleRate = 48e3;
2073
2791
  /** Resolved sample rate — falls back to sampleRate property until first audio decode. */
2074
2792
  this._resolvedSampleRate = null;
@@ -2085,14 +2803,15 @@ var DawEditorElement = class extends import_lit12.LitElement {
2085
2803
  this._currentTime = 0;
2086
2804
  this._engine = null;
2087
2805
  this._enginePromise = null;
2088
- this._audioInitialized = false;
2089
2806
  this._audioCache = /* @__PURE__ */ new Map();
2090
2807
  this._clipBuffers = /* @__PURE__ */ new Map();
2808
+ this._clipOffsets = /* @__PURE__ */ new Map();
2091
2809
  this._peakPipeline = new PeakPipeline();
2092
2810
  this._trackElements = /* @__PURE__ */ new Map();
2093
2811
  this._childObserver = null;
2094
2812
  this._audioResume = new AudioResumeController(this);
2095
2813
  this._recordingController = new RecordingController(this);
2814
+ this._clipPointer = new ClipPointerHandler(this);
2096
2815
  this._pointer = new PointerHandler(this);
2097
2816
  this._viewport = (() => {
2098
2817
  const v = new ViewportController(this);
@@ -2136,6 +2855,19 @@ var DawEditorElement = class extends import_lit12.LitElement {
2136
2855
  this._onTrackControl = (e) => {
2137
2856
  const { trackId, prop, value } = e.detail ?? {};
2138
2857
  if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
2858
+ if (this._selectedTrackId !== trackId) {
2859
+ this._setSelectedTrackId(trackId);
2860
+ if (this._engine) {
2861
+ this._engine.selectTrack(trackId);
2862
+ }
2863
+ this.dispatchEvent(
2864
+ new CustomEvent("daw-track-select", {
2865
+ bubbles: true,
2866
+ composed: true,
2867
+ detail: { trackId }
2868
+ })
2869
+ );
2870
+ }
2139
2871
  const oldDescriptor = this._tracks.get(trackId);
2140
2872
  if (oldDescriptor) {
2141
2873
  const descriptor = { ...oldDescriptor, [prop]: value };
@@ -2196,6 +2928,28 @@ var DawEditorElement = class extends import_lit12.LitElement {
2196
2928
  // --- Recording ---
2197
2929
  this.recordingStream = null;
2198
2930
  }
2931
+ get _clipHandler() {
2932
+ return this.interactiveClips ? this._clipPointer : null;
2933
+ }
2934
+ get engine() {
2935
+ return this._engine;
2936
+ }
2937
+ /** Re-extract peaks for a clip at new offset/duration from cached WaveformData. */
2938
+ reextractClipPeaks(clipId, offsetSamples, durationSamples) {
2939
+ const buf = this._clipBuffers.get(clipId);
2940
+ if (!buf) return null;
2941
+ const singleClipBuffers = /* @__PURE__ */ new Map([[clipId, buf]]);
2942
+ const singleClipOffsets = /* @__PURE__ */ new Map([[clipId, { offsetSamples, durationSamples }]]);
2943
+ const result = this._peakPipeline.reextractPeaks(
2944
+ singleClipBuffers,
2945
+ this.samplesPerPixel,
2946
+ this.mono,
2947
+ singleClipOffsets
2948
+ );
2949
+ const peakData = result.get(clipId);
2950
+ if (!peakData) return null;
2951
+ return { data: peakData.data, length: peakData.length };
2952
+ }
2199
2953
  get effectiveSampleRate() {
2200
2954
  return this._resolvedSampleRate ?? this.sampleRate;
2201
2955
  }
@@ -2270,6 +3024,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
2270
3024
  this._trackElements.clear();
2271
3025
  this._audioCache.clear();
2272
3026
  this._clipBuffers.clear();
3027
+ this._clipOffsets.clear();
2273
3028
  this._peakPipeline.terminate();
2274
3029
  try {
2275
3030
  this._disposeEngine();
@@ -2281,17 +3036,19 @@ var DawEditorElement = class extends import_lit12.LitElement {
2281
3036
  if (changedProperties.has("eagerResume")) {
2282
3037
  this._audioResume.target = this.eagerResume;
2283
3038
  }
3039
+ if (changedProperties.has("samplesPerPixel") && this._isPlaying) {
3040
+ this._startPlayhead();
3041
+ }
2284
3042
  if (changedProperties.has("samplesPerPixel") && this._clipBuffers.size > 0) {
2285
- const reextracted = this._peakPipeline.reextractPeaks(
3043
+ const re = this._peakPipeline.reextractPeaks(
2286
3044
  this._clipBuffers,
2287
3045
  this.samplesPerPixel,
2288
- this.mono
3046
+ this.mono,
3047
+ this._clipOffsets
2289
3048
  );
2290
- if (reextracted.size > 0) {
3049
+ if (re.size > 0) {
2291
3050
  const next = new Map(this._peaksData);
2292
- for (const [clipId, peakData] of reextracted) {
2293
- next.set(clipId, peakData);
2294
- }
3051
+ for (const [id, pd] of re) next.set(id, pd);
2295
3052
  this._peaksData = next;
2296
3053
  }
2297
3054
  }
@@ -2303,6 +3060,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
2303
3060
  const nextPeaks = new Map(this._peaksData);
2304
3061
  for (const clip of removedTrack.clips) {
2305
3062
  this._clipBuffers.delete(clip.id);
3063
+ this._clipOffsets.delete(clip.id);
2306
3064
  nextPeaks.delete(clip.id);
2307
3065
  }
2308
3066
  this._peaksData = nextPeaks;
@@ -2381,10 +3139,16 @@ var DawEditorElement = class extends import_lit12.LitElement {
2381
3139
  sourceDuration: audioBuffer.duration
2382
3140
  });
2383
3141
  this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
3142
+ this._clipOffsets.set(clip.id, {
3143
+ offsetSamples: clip.offsetSamples,
3144
+ durationSamples: clip.durationSamples
3145
+ });
2384
3146
  const peakData = await this._peakPipeline.generatePeaks(
2385
3147
  audioBuffer,
2386
3148
  this.samplesPerPixel,
2387
- this.mono
3149
+ this.mono,
3150
+ clip.offsetSamples,
3151
+ clip.durationSamples
2388
3152
  );
2389
3153
  this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
2390
3154
  clips.push(clip);
@@ -2476,10 +3240,20 @@ var DawEditorElement = class extends import_lit12.LitElement {
2476
3240
  samplesPerPixel: this.samplesPerPixel,
2477
3241
  zoomLevels: [256, 512, 1024, 2048, 4096, 8192, this.samplesPerPixel].filter((v, i, a) => a.indexOf(v) === i).sort((a, b) => a - b)
2478
3242
  });
3243
+ let lastTracksVersion = -1;
2479
3244
  engine.on("statechange", (engineState) => {
2480
3245
  this._isPlaying = engineState.isPlaying;
2481
3246
  this._duration = engineState.duration;
2482
3247
  this._selectedTrackId = engineState.selectedTrackId;
3248
+ if (engineState.tracksVersion !== lastTracksVersion) {
3249
+ lastTracksVersion = engineState.tracksVersion;
3250
+ const nextTracks = /* @__PURE__ */ new Map();
3251
+ for (const track of engineState.tracks) {
3252
+ nextTracks.set(track.id, track);
3253
+ }
3254
+ this._engineTracks = nextTracks;
3255
+ syncPeaksForChangedClips(this, engineState.tracks);
3256
+ }
2483
3257
  });
2484
3258
  engine.on("timeupdate", (time) => {
2485
3259
  this._currentTime = time;
@@ -2502,14 +3276,11 @@ var DawEditorElement = class extends import_lit12.LitElement {
2502
3276
  return loadFiles(this, files);
2503
3277
  }
2504
3278
  // --- Playback ---
2505
- async play() {
3279
+ async play(startTime) {
2506
3280
  try {
2507
3281
  const engine = await this._ensureEngine();
2508
- if (!this._audioInitialized) {
2509
- await engine.init();
2510
- this._audioInitialized = true;
2511
- }
2512
- engine.play();
3282
+ await engine.init();
3283
+ engine.play(startTime);
2513
3284
  this._startPlayhead();
2514
3285
  this.dispatchEvent(new CustomEvent("daw-play", { bubbles: true, composed: true }));
2515
3286
  } catch (err) {
@@ -2535,19 +3306,90 @@ var DawEditorElement = class extends import_lit12.LitElement {
2535
3306
  this._stopPlayhead();
2536
3307
  this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
2537
3308
  }
3309
+ /** Toggle between play and pause. */
3310
+ togglePlayPause() {
3311
+ if (this._isPlaying) {
3312
+ this.pause();
3313
+ } else {
3314
+ this.play();
3315
+ }
3316
+ }
2538
3317
  seekTo(time) {
2539
- if (!this._engine) return;
2540
- this._engine.seek(time);
2541
- this._currentTime = time;
3318
+ if (!this._engine) {
3319
+ console.warn("[dawcore] seekTo: engine not ready, call ignored");
3320
+ return;
3321
+ }
3322
+ if (this._isPlaying) {
3323
+ this.stop();
3324
+ this.play(time);
3325
+ } else {
3326
+ this._engine.seek(time);
3327
+ this._currentTime = time;
3328
+ this._stopPlayhead();
3329
+ }
3330
+ }
3331
+ /** Undo the last structural edit. */
3332
+ undo() {
3333
+ if (!this._engine) {
3334
+ console.warn("[dawcore] undo: engine not ready, call ignored");
3335
+ return;
3336
+ }
3337
+ this._engine.undo();
3338
+ }
3339
+ /** Redo the last undone edit. */
3340
+ redo() {
3341
+ if (!this._engine) {
3342
+ console.warn("[dawcore] redo: engine not ready, call ignored");
3343
+ return;
3344
+ }
3345
+ this._engine.redo();
3346
+ }
3347
+ /** Whether undo is available. */
3348
+ get canUndo() {
3349
+ return this._engine?.canUndo ?? false;
3350
+ }
3351
+ /** Whether redo is available. */
3352
+ get canRedo() {
3353
+ return this._engine?.canRedo ?? false;
3354
+ }
3355
+ /** Split the clip under the playhead on the selected track. */
3356
+ splitAtPlayhead() {
3357
+ return splitAtPlayhead({
3358
+ effectiveSampleRate: this.effectiveSampleRate,
3359
+ currentTime: this._currentTime,
3360
+ isPlaying: this._isPlaying,
3361
+ engine: this._engine,
3362
+ dispatchEvent: (e) => this.dispatchEvent(e),
3363
+ stop: () => {
3364
+ this._engine?.stop();
3365
+ this._stopPlayhead();
3366
+ },
3367
+ // Call engine.play directly (synchronous) — not the async editor play()
3368
+ // which yields to microtask queue via await engine.init(). Engine is
3369
+ // already initialized at split time; the async gap causes audio desync.
3370
+ play: (time) => {
3371
+ this._engine?.play(time);
3372
+ this._startPlayhead();
3373
+ }
3374
+ });
3375
+ }
3376
+ get currentTime() {
3377
+ return this._currentTime;
2542
3378
  }
2543
3379
  get isRecording() {
2544
3380
  return this._recordingController.isRecording;
2545
3381
  }
3382
+ pauseRecording() {
3383
+ this._recordingController.pauseRecording();
3384
+ }
3385
+ resumeRecording() {
3386
+ this._recordingController.resumeRecording();
3387
+ }
2546
3388
  stopRecording() {
2547
3389
  this._recordingController.stopRecording();
2548
3390
  }
2549
- _addRecordedClip(trackId, buf, startSample, durSamples) {
2550
- addRecordedClip(this, trackId, buf, startSample, durSamples);
3391
+ _addRecordedClip(trackId, buf, startSample, durSamples, offsetSamples = 0) {
3392
+ addRecordedClip(this, trackId, buf, startSample, durSamples, offsetSamples);
2551
3393
  }
2552
3394
  async startRecording(stream, options) {
2553
3395
  const s = stream ?? this.recordingStream;
@@ -2560,15 +3402,19 @@ var DawEditorElement = class extends import_lit12.LitElement {
2560
3402
  _renderRecordingPreview(trackId, chH) {
2561
3403
  const rs = this._recordingController.getSession(trackId);
2562
3404
  if (!rs) return "";
3405
+ const audibleSamples = Math.max(0, rs.totalSamples - rs.latencySamples);
3406
+ if (audibleSamples === 0) return "";
3407
+ const latencyPixels = Math.floor(rs.latencySamples / this.samplesPerPixel);
2563
3408
  const left = Math.floor(rs.startSample / this.samplesPerPixel);
2564
- const w = Math.floor(rs.totalSamples / this.samplesPerPixel);
2565
- return rs.peaks.map(
2566
- (chPeaks, ch) => import_lit12.html`
3409
+ const w = Math.floor(audibleSamples / this.samplesPerPixel);
3410
+ return rs.peaks.map((chPeaks, ch) => {
3411
+ const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
3412
+ return import_lit12.html`
2567
3413
  <daw-waveform
2568
3414
  data-recording-track=${trackId}
2569
3415
  data-recording-channel=${ch}
2570
3416
  style="position:absolute;left:${left}px;top:${ch * chH}px;"
2571
- .peaks=${chPeaks}
3417
+ .peaks=${slicedPeaks}
2572
3418
  .length=${w}
2573
3419
  .waveHeight=${chH}
2574
3420
  .barWidth=${this.barWidth}
@@ -2577,8 +3423,8 @@ var DawEditorElement = class extends import_lit12.LitElement {
2577
3423
  .visibleEnd=${this._viewport.visibleEnd}
2578
3424
  .originX=${left}
2579
3425
  ></daw-waveform>
2580
- `
2581
- );
3426
+ `;
3427
+ });
2582
3428
  }
2583
3429
  // --- Playhead ---
2584
3430
  _startPlayhead() {
@@ -2620,13 +3466,14 @@ var DawEditorElement = class extends import_lit12.LitElement {
2620
3466
  const orderedTracks = this._getOrderedTracks().map(([trackId, track]) => {
2621
3467
  const descriptor = this._tracks.get(trackId);
2622
3468
  const firstPeaks = track.clips.map((c) => this._peaksData.get(c.id)).find((p) => p && p.data.length > 0);
2623
- const numChannels = firstPeaks ? firstPeaks.data.length : 1;
3469
+ const recSession = this._recordingController.getSession(trackId);
3470
+ const numChannels = firstPeaks ? firstPeaks.data.length : recSession ? recSession.channelCount : 1;
2624
3471
  return {
2625
3472
  trackId,
2626
3473
  track,
2627
3474
  descriptor,
2628
3475
  numChannels,
2629
- trackHeight: this.waveHeight * numChannels
3476
+ trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
2630
3477
  };
2631
3478
  });
2632
3479
  return import_lit12.html`
@@ -2680,21 +3527,47 @@ var DawEditorElement = class extends import_lit12.LitElement {
2680
3527
  );
2681
3528
  const clipLeft = Math.floor(clip.startSample / this.samplesPerPixel);
2682
3529
  const channels = peakData?.data ?? [new Int16Array(0)];
2683
- return channels.map(
2684
- (channelPeaks, chIdx) => import_lit12.html`
2685
- <daw-waveform
2686
- style="position: absolute; left: ${clipLeft}px; top: ${chIdx * channelHeight}px;"
2687
- .peaks=${channelPeaks}
2688
- .length=${peakData?.length ?? width}
2689
- .waveHeight=${channelHeight}
2690
- .barWidth=${this.barWidth}
2691
- .barGap=${this.barGap}
2692
- .visibleStart=${this._viewport.visibleStart}
2693
- .visibleEnd=${this._viewport.visibleEnd}
2694
- .originX=${clipLeft}
2695
- ></daw-waveform>
2696
- `
2697
- );
3530
+ const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
3531
+ const chH = this.waveHeight;
3532
+ return import_lit12.html` <div
3533
+ class="clip-container"
3534
+ style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
3535
+ data-clip-id=${clip.id}
3536
+ >
3537
+ ${hdrH > 0 ? import_lit12.html`<div
3538
+ class="clip-header"
3539
+ data-clip-id=${clip.id}
3540
+ data-track-id=${t.trackId}
3541
+ ?data-interactive=${this.interactiveClips}
3542
+ >
3543
+ <span>${clip.name || t.descriptor?.name || ""}</span>
3544
+ </div>` : ""}
3545
+ ${channels.map(
3546
+ (chPeaks, chIdx) => import_lit12.html` <daw-waveform
3547
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
3548
+ .peaks=${chPeaks}
3549
+ .length=${peakData?.length ?? width}
3550
+ .waveHeight=${chH}
3551
+ .barWidth=${this.barWidth}
3552
+ .barGap=${this.barGap}
3553
+ .visibleStart=${this._viewport.visibleStart}
3554
+ .visibleEnd=${this._viewport.visibleEnd}
3555
+ .originX=${clipLeft}
3556
+ ></daw-waveform>`
3557
+ )}
3558
+ ${this.interactiveClips ? import_lit12.html` <div
3559
+ class="clip-boundary"
3560
+ data-boundary-edge="left"
3561
+ data-clip-id=${clip.id}
3562
+ data-track-id=${t.trackId}
3563
+ ></div>
3564
+ <div
3565
+ class="clip-boundary"
3566
+ data-boundary-edge="right"
3567
+ data-clip-id=${clip.id}
3568
+ data-track-id=${t.trackId}
3569
+ ></div>` : ""}
3570
+ </div>`;
2698
3571
  })}
2699
3572
  ${this._renderRecordingPreview(t.trackId, channelHeight)}
2700
3573
  </div>
@@ -2742,7 +3615,8 @@ DawEditorElement.styles = [
2742
3615
  outline: 2px dashed var(--daw-selection-color, rgba(99, 199, 95, 0.3));
2743
3616
  outline-offset: -2px;
2744
3617
  }
2745
- `
3618
+ `,
3619
+ clipStyles
2746
3620
  ];
2747
3621
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
2748
3622
  __decorateClass([
@@ -2766,6 +3640,15 @@ __decorateClass([
2766
3640
  __decorateClass([
2767
3641
  (0, import_decorators10.property)({ type: Boolean, attribute: "file-drop" })
2768
3642
  ], DawEditorElement.prototype, "fileDrop", 2);
3643
+ __decorateClass([
3644
+ (0, import_decorators10.property)({ type: Boolean, attribute: "clip-headers" })
3645
+ ], DawEditorElement.prototype, "clipHeaders", 2);
3646
+ __decorateClass([
3647
+ (0, import_decorators10.property)({ type: Number, attribute: "clip-header-height" })
3648
+ ], DawEditorElement.prototype, "clipHeaderHeight", 2);
3649
+ __decorateClass([
3650
+ (0, import_decorators10.property)({ type: Boolean, attribute: "interactive-clips" })
3651
+ ], DawEditorElement.prototype, "interactiveClips", 2);
2769
3652
  __decorateClass([
2770
3653
  (0, import_decorators10.property)({ type: Number, attribute: "sample-rate" })
2771
3654
  ], DawEditorElement.prototype, "sampleRate", 2);
@@ -3030,7 +3913,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
3030
3913
  }
3031
3914
  connectedCallback() {
3032
3915
  super.connectedCallback();
3033
- this._listenToTarget();
3916
+ requestAnimationFrame(() => this._listenToTarget());
3034
3917
  }
3035
3918
  disconnectedCallback() {
3036
3919
  super.disconnectedCallback();
@@ -3055,11 +3938,12 @@ var DawRecordButtonElement = class extends DawTransportButton {
3055
3938
  render() {
3056
3939
  return import_lit15.html`
3057
3940
  <button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
3058
- <slot>${this._isRecording ? "Stop Rec" : "Record"}</slot>
3941
+ <slot>Record</slot>
3059
3942
  </button>
3060
3943
  `;
3061
3944
  }
3062
3945
  _onClick() {
3946
+ if (this._isRecording) return;
3063
3947
  const target = this.target;
3064
3948
  if (!target) {
3065
3949
  console.warn(
@@ -3067,11 +3951,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
3067
3951
  );
3068
3952
  return;
3069
3953
  }
3070
- if (this._isRecording) {
3071
- target.stopRecording();
3072
- } else {
3073
- target.startRecording(target.recordingStream);
3074
- }
3954
+ target.startRecording(target.recordingStream);
3075
3955
  }
3076
3956
  };
3077
3957
  DawRecordButtonElement.styles = [
@@ -3080,6 +3960,7 @@ DawRecordButtonElement.styles = [
3080
3960
  button[data-recording] {
3081
3961
  color: #d08070;
3082
3962
  border-color: #d08070;
3963
+ background: rgba(208, 128, 112, 0.15);
3083
3964
  }
3084
3965
  `
3085
3966
  ];
@@ -3089,11 +3970,183 @@ __decorateClass([
3089
3970
  DawRecordButtonElement = __decorateClass([
3090
3971
  (0, import_decorators13.customElement)("daw-record-button")
3091
3972
  ], DawRecordButtonElement);
3973
+
3974
+ // src/elements/daw-keyboard-shortcuts.ts
3975
+ var import_lit16 = require("lit");
3976
+ var import_decorators14 = require("lit/decorators.js");
3977
+ var import_core5 = require("@waveform-playlist/core");
3978
+ var DawKeyboardShortcutsElement = class extends import_lit16.LitElement {
3979
+ constructor() {
3980
+ super(...arguments);
3981
+ this.playback = false;
3982
+ this.splitting = false;
3983
+ this.undo = false;
3984
+ // --- JS properties for remapping ---
3985
+ this.playbackShortcuts = null;
3986
+ this.splittingShortcuts = null;
3987
+ this.undoShortcuts = null;
3988
+ /** Additional custom shortcuts. */
3989
+ this.customShortcuts = [];
3990
+ this._editor = null;
3991
+ this._cachedShortcuts = null;
3992
+ // --- Event handler ---
3993
+ this._onKeyDown = (e) => {
3994
+ const shortcuts = this.shortcuts;
3995
+ if (shortcuts.length === 0) return;
3996
+ try {
3997
+ (0, import_core5.handleKeyboardEvent)(e, shortcuts, true);
3998
+ } catch (err) {
3999
+ console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
4000
+ const target = this._editor ?? this;
4001
+ target.dispatchEvent(
4002
+ new CustomEvent("daw-error", {
4003
+ bubbles: true,
4004
+ composed: true,
4005
+ detail: { operation: "keyboard-shortcut", key: e.key, error: err }
4006
+ })
4007
+ );
4008
+ }
4009
+ };
4010
+ }
4011
+ /** All active shortcuts (read-only, cached). */
4012
+ get shortcuts() {
4013
+ if (!this._cachedShortcuts) {
4014
+ this._cachedShortcuts = this._buildShortcuts();
4015
+ }
4016
+ return this._cachedShortcuts;
4017
+ }
4018
+ /** Invalidate cached shortcuts when Lit properties change. */
4019
+ updated() {
4020
+ this._cachedShortcuts = null;
4021
+ }
4022
+ // --- Lifecycle ---
4023
+ connectedCallback() {
4024
+ super.connectedCallback();
4025
+ this._editor = this.closest("daw-editor");
4026
+ if (!this._editor) {
4027
+ console.warn(
4028
+ "[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
4029
+ );
4030
+ }
4031
+ document.addEventListener("keydown", this._onKeyDown);
4032
+ }
4033
+ disconnectedCallback() {
4034
+ super.disconnectedCallback();
4035
+ document.removeEventListener("keydown", this._onKeyDown);
4036
+ this._editor = null;
4037
+ }
4038
+ // No shadow DOM — render-less element
4039
+ createRenderRoot() {
4040
+ return this;
4041
+ }
4042
+ // --- Shortcut building ---
4043
+ _buildShortcuts() {
4044
+ const editor = this._editor;
4045
+ if (!editor) return this.customShortcuts;
4046
+ const result = [];
4047
+ if (this.playback) {
4048
+ const map = this.playbackShortcuts;
4049
+ result.push(
4050
+ this._makeShortcut(
4051
+ map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
4052
+ () => editor.togglePlayPause(),
4053
+ "Play/Pause"
4054
+ ),
4055
+ this._makeShortcut(
4056
+ map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
4057
+ () => editor.stop(),
4058
+ "Stop"
4059
+ ),
4060
+ this._makeShortcut(
4061
+ map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
4062
+ () => editor.seekTo(0),
4063
+ "Rewind to start"
4064
+ )
4065
+ );
4066
+ }
4067
+ if (this.splitting) {
4068
+ const map = this.splittingShortcuts;
4069
+ const binding = map?.splitAtPlayhead ?? {
4070
+ key: "s",
4071
+ ctrlKey: false,
4072
+ metaKey: false,
4073
+ altKey: false
4074
+ };
4075
+ result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
4076
+ }
4077
+ if (this.undo) {
4078
+ const map = this.undoShortcuts;
4079
+ const undoBinding = map?.undo ?? { key: "z" };
4080
+ const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
4081
+ if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
4082
+ const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
4083
+ result.push(
4084
+ this._makeShortcut(
4085
+ { ...undoBinding, ctrlKey: true, ...undoShift },
4086
+ () => editor.undo(),
4087
+ "Undo"
4088
+ ),
4089
+ this._makeShortcut(
4090
+ { ...undoBinding, metaKey: true, ...undoShift },
4091
+ () => editor.undo(),
4092
+ "Undo"
4093
+ )
4094
+ );
4095
+ } else {
4096
+ result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
4097
+ }
4098
+ if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
4099
+ const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
4100
+ result.push(
4101
+ this._makeShortcut(
4102
+ { ...redoBinding, ctrlKey: true, ...redoShift },
4103
+ () => editor.redo(),
4104
+ "Redo"
4105
+ ),
4106
+ this._makeShortcut(
4107
+ { ...redoBinding, metaKey: true, ...redoShift },
4108
+ () => editor.redo(),
4109
+ "Redo"
4110
+ )
4111
+ );
4112
+ } else {
4113
+ result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
4114
+ }
4115
+ }
4116
+ result.push(...this.customShortcuts);
4117
+ return result;
4118
+ }
4119
+ _makeShortcut(binding, action, description) {
4120
+ return {
4121
+ key: binding.key,
4122
+ ...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
4123
+ ...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
4124
+ ...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
4125
+ ...binding.altKey !== void 0 && { altKey: binding.altKey },
4126
+ action,
4127
+ description
4128
+ };
4129
+ }
4130
+ };
4131
+ __decorateClass([
4132
+ (0, import_decorators14.property)({ type: Boolean })
4133
+ ], DawKeyboardShortcutsElement.prototype, "playback", 2);
4134
+ __decorateClass([
4135
+ (0, import_decorators14.property)({ type: Boolean })
4136
+ ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
4137
+ __decorateClass([
4138
+ (0, import_decorators14.property)({ type: Boolean })
4139
+ ], DawKeyboardShortcutsElement.prototype, "undo", 2);
4140
+ DawKeyboardShortcutsElement = __decorateClass([
4141
+ (0, import_decorators14.customElement)("daw-keyboard-shortcuts")
4142
+ ], DawKeyboardShortcutsElement);
3092
4143
  // Annotate the CommonJS export names for ESM import in node:
3093
4144
  0 && (module.exports = {
3094
4145
  AudioResumeController,
4146
+ ClipPointerHandler,
3095
4147
  DawClipElement,
3096
4148
  DawEditorElement,
4149
+ DawKeyboardShortcutsElement,
3097
4150
  DawPauseButtonElement,
3098
4151
  DawPlayButtonElement,
3099
4152
  DawPlayheadElement,
@@ -3106,6 +4159,7 @@ DawRecordButtonElement = __decorateClass([
3106
4159
  DawTransportButton,
3107
4160
  DawTransportElement,
3108
4161
  DawWaveformElement,
3109
- RecordingController
4162
+ RecordingController,
4163
+ splitAtPlayhead
3110
4164
  });
3111
4165
  //# sourceMappingURL=index.js.map