@dawcore/components 0.0.9 → 0.0.10

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
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  ClipPointerHandler: () => ClipPointerHandler,
43
43
  DawClipElement: () => DawClipElement,
44
44
  DawEditorElement: () => DawEditorElement,
45
+ DawGridElement: () => DawGridElement,
45
46
  DawKeyboardShortcutsElement: () => DawKeyboardShortcutsElement,
46
47
  DawPauseButtonElement: () => DawPauseButtonElement,
47
48
  DawPlayButtonElement: () => DawPlayButtonElement,
@@ -244,7 +245,7 @@ function getVisibleChunkIndices(totalWidth, chunkWidth, visibleStart, visibleEnd
244
245
 
245
246
  // src/elements/daw-waveform.ts
246
247
  var MAX_CANVAS_WIDTH = 1e3;
247
- var LAYOUT_PROPS = /* @__PURE__ */ new Set(["length", "waveHeight", "barWidth", "barGap"]);
248
+ var LAYOUT_PROPS = /* @__PURE__ */ new Set(["length", "waveHeight", "barWidth", "barGap", "segments"]);
248
249
  function groupDirtyByChunk(dirtyPixels, step) {
249
250
  const dirtyByChunk = /* @__PURE__ */ new Map();
250
251
  for (const peakIdx of dirtyPixels) {
@@ -351,14 +352,22 @@ var DawWaveformElement = class extends import_lit3.LitElement {
351
352
  const halfHeight = this.waveHeight / 2;
352
353
  const bits = this.bits;
353
354
  const waveColor = getComputedStyle(this).getPropertyValue("--daw-wave-color").trim() || "#c49a6c";
354
- const dirtyByChunk = groupDirtyByChunk(this._dirtyPixels, step);
355
355
  this._drawnChunks.clear();
356
- for (const canvas of canvases) {
357
- const chunkIdx = Number(canvas.dataset.index);
358
- this._drawnChunks.add(chunkIdx);
359
- const range = dirtyByChunk.get(chunkIdx);
360
- if (!range) continue;
361
- this._drawChunk(canvas, chunkIdx, range, step, dpr, halfHeight, bits, waveColor);
356
+ if (this.segments) {
357
+ for (const canvas of canvases) {
358
+ const chunkIdx = Number(canvas.dataset.index);
359
+ this._drawnChunks.add(chunkIdx);
360
+ this._drawSegments(canvas, chunkIdx, dpr, halfHeight, bits, waveColor);
361
+ }
362
+ } else {
363
+ const dirtyByChunk = groupDirtyByChunk(this._dirtyPixels, step);
364
+ for (const canvas of canvases) {
365
+ const chunkIdx = Number(canvas.dataset.index);
366
+ this._drawnChunks.add(chunkIdx);
367
+ const range = dirtyByChunk.get(chunkIdx);
368
+ if (!range) continue;
369
+ this._drawChunk(canvas, chunkIdx, range, step, dpr, halfHeight, bits, waveColor);
370
+ }
362
371
  }
363
372
  this._dirtyPixels.clear();
364
373
  }
@@ -392,6 +401,45 @@ var DawWaveformElement = class extends import_lit3.LitElement {
392
401
  }
393
402
  }
394
403
  }
404
+ _drawSegments(canvas, chunkIdx, dpr, halfHeight, bits, waveColor) {
405
+ if (!this.segments) return;
406
+ const ctx = canvas.getContext("2d");
407
+ if (!ctx) return;
408
+ const globalOffset = chunkIdx * MAX_CANVAS_WIDTH;
409
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH, this.length - globalOffset);
410
+ ctx.resetTransform();
411
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
412
+ ctx.scale(dpr, dpr);
413
+ ctx.fillStyle = waveColor;
414
+ const step = Math.max(1, Math.round(this.barWidth + this.barGap));
415
+ for (const seg of this.segments) {
416
+ if (seg.pixelEnd <= globalOffset || seg.pixelStart >= globalOffset + canvasWidth) continue;
417
+ const localStart = Math.max(0, seg.pixelStart - globalOffset);
418
+ const localEnd = Math.min(canvasWidth, seg.pixelEnd - globalOffset);
419
+ const segPixelWidth = seg.pixelEnd - seg.pixelStart;
420
+ const segPeakWidth = seg.peakEnd - seg.peakStart;
421
+ if (segPixelWidth <= 0 || segPeakWidth <= 0) continue;
422
+ const peaksPerPixel = segPeakWidth / segPixelWidth;
423
+ for (let px = Math.floor(localStart); px < Math.ceil(localEnd); px += step) {
424
+ const pxInSeg = px + globalOffset - seg.pixelStart;
425
+ const peakPos = seg.peakStart + pxInSeg * peaksPerPixel;
426
+ const peakEnd = peakPos + step * peaksPerPixel;
427
+ const peak = aggregatePeaks(this._peaks, bits, Math.floor(peakPos), Math.ceil(peakEnd));
428
+ if (!peak) continue;
429
+ const rects = calculateBarRects(
430
+ px,
431
+ this.barWidth,
432
+ halfHeight,
433
+ peak.min,
434
+ peak.max,
435
+ "normal"
436
+ );
437
+ for (const r of rects) {
438
+ ctx.fillRect(r.x, r.y, r.width, r.height);
439
+ }
440
+ }
441
+ }
442
+ }
395
443
  connectedCallback() {
396
444
  super.connectedCallback();
397
445
  if (this._dirtyPixels.size > 0) {
@@ -486,6 +534,9 @@ __decorateClass([
486
534
  __decorateClass([
487
535
  (0, import_decorators3.property)({ type: Number, attribute: false })
488
536
  ], DawWaveformElement.prototype, "originX", 2);
537
+ __decorateClass([
538
+ (0, import_decorators3.property)({ attribute: false })
539
+ ], DawWaveformElement.prototype, "segments", 2);
489
540
  DawWaveformElement = __decorateClass([
490
541
  (0, import_decorators3.customElement)("daw-waveform")
491
542
  ], DawWaveformElement);
@@ -553,6 +604,40 @@ var DawPlayheadElement = class extends import_lit4.LitElement {
553
604
  this._line.style.transform = `translate3d(${px}px, 0, 0)`;
554
605
  }
555
606
  }
607
+ startBeatsAnimation(getTime, bpm, ppqn, ticksPerPixel) {
608
+ const ticksPerSecond = bpm * ppqn / 60;
609
+ this._animation.start(() => {
610
+ const time = getTime();
611
+ const px = time * ticksPerSecond / ticksPerPixel;
612
+ if (this._line) {
613
+ this._line.style.transform = `translate3d(${px}px, 0, 0)`;
614
+ }
615
+ });
616
+ }
617
+ stopBeatsAnimation(time, bpm, ppqn, ticksPerPixel) {
618
+ this._animation.stop();
619
+ const px = time * bpm * ppqn / (60 * ticksPerPixel);
620
+ if (this._line) {
621
+ this._line.style.transform = `translate3d(${px}px, 0, 0)`;
622
+ }
623
+ }
624
+ startBeatsAnimationWithMap(getTime, secondsToTicks, ticksPerPixel) {
625
+ this._animation.start(() => {
626
+ const time = getTime();
627
+ const tick = secondsToTicks(time);
628
+ const px = tick / ticksPerPixel;
629
+ if (this._line) {
630
+ this._line.style.transform = `translate3d(${px}px, 0, 0)`;
631
+ }
632
+ });
633
+ }
634
+ stopBeatsAnimationWithMap(time, secondsToTicks, ticksPerPixel) {
635
+ this._animation.stop();
636
+ const px = secondsToTicks(time) / ticksPerPixel;
637
+ if (this._line) {
638
+ this._line.style.transform = `translate3d(${px}px, 0, 0)`;
639
+ }
640
+ }
556
641
  };
557
642
  DawPlayheadElement.styles = import_lit4.css`
558
643
  :host {
@@ -804,9 +889,9 @@ DawStopButtonElement = __decorateClass([
804
889
  ], DawStopButtonElement);
805
890
 
806
891
  // src/elements/daw-editor.ts
807
- var import_lit12 = require("lit");
808
- var import_decorators10 = require("lit/decorators.js");
809
- var import_core4 = require("@waveform-playlist/core");
892
+ var import_lit13 = require("lit");
893
+ var import_decorators11 = require("lit/decorators.js");
894
+ var import_core8 = require("@waveform-playlist/core");
810
895
 
811
896
  // src/workers/peaksWorker.ts
812
897
  var import_waveform_data = __toESM(require("waveform-data"));
@@ -1215,6 +1300,19 @@ var PeakPipeline = class {
1215
1300
  }
1216
1301
  return requestedScale;
1217
1302
  }
1303
+ /**
1304
+ * Extract peaks at the base scale from cached WaveformData.
1305
+ * Returns null if no cached data exists for this buffer.
1306
+ * Used by variable-tempo segments which handle stretching themselves.
1307
+ */
1308
+ getBaseScalePeaks(audioBuffer, isMono, offsetSamples, durationSamples) {
1309
+ const cached = this._cache.get(audioBuffer);
1310
+ if (!cached) return null;
1311
+ return {
1312
+ peaks: extractPeaks(cached, cached.scale, isMono, offsetSamples, durationSamples),
1313
+ scale: cached.scale
1314
+ };
1315
+ }
1218
1316
  /**
1219
1317
  * Return the coarsest (largest) scale among cached WaveformData entries
1220
1318
  * that correspond to the given clip buffers. Returns 0 if none are cached.
@@ -1520,9 +1618,187 @@ DawTrackControlsElement = __decorateClass([
1520
1618
  (0, import_decorators9.customElement)("daw-track-controls")
1521
1619
  ], DawTrackControlsElement);
1522
1620
 
1523
- // src/styles/theme.ts
1621
+ // src/elements/daw-grid.ts
1524
1622
  var import_lit11 = require("lit");
1525
- var hostStyles = import_lit11.css`
1623
+ var import_decorators10 = require("lit/decorators.js");
1624
+ var import_core2 = require("@waveform-playlist/core");
1625
+
1626
+ // src/utils/musical-tick-cache.ts
1627
+ var import_core = require("@waveform-playlist/core");
1628
+ var cachedParams = null;
1629
+ var cachedResult = null;
1630
+ function meterEntriesMatch(a, b) {
1631
+ if (a.length !== b.length) return false;
1632
+ for (let i = 0; i < a.length; i++) {
1633
+ if (a[i].tick !== b[i].tick || a[i].numerator !== b[i].numerator || a[i].denominator !== b[i].denominator)
1634
+ return false;
1635
+ }
1636
+ return true;
1637
+ }
1638
+ function paramsMatch(a, b) {
1639
+ return a.ticksPerPixel === b.ticksPerPixel && a.startPixel === b.startPixel && a.endPixel === b.endPixel && meterEntriesMatch(a.meterEntries, b.meterEntries) && (a.ppqn ?? 960) === (b.ppqn ?? 960);
1640
+ }
1641
+ function getCachedMusicalTicks(params) {
1642
+ if (cachedParams && cachedResult && paramsMatch(cachedParams, params)) {
1643
+ return cachedResult;
1644
+ }
1645
+ cachedResult = (0, import_core.computeMusicalTicks)(params);
1646
+ cachedParams = {
1647
+ ...params,
1648
+ meterEntries: params.meterEntries.map((e) => ({ ...e }))
1649
+ };
1650
+ return cachedResult;
1651
+ }
1652
+
1653
+ // src/elements/daw-grid.ts
1654
+ var MAX_CANVAS_WIDTH2 = 1e3;
1655
+ var DawGridElement = class extends import_lit11.LitElement {
1656
+ constructor() {
1657
+ super(...arguments);
1658
+ this.ticksPerPixel = 24;
1659
+ this.meterEntries = [
1660
+ { tick: 0, numerator: 4, denominator: 4 }
1661
+ ];
1662
+ this.ppqn = 960;
1663
+ this.visibleStart = -Infinity;
1664
+ this.visibleEnd = Infinity;
1665
+ this.length = 0;
1666
+ this.height = 200;
1667
+ this._tickData = null;
1668
+ }
1669
+ willUpdate() {
1670
+ if (this.length > 0) {
1671
+ this._tickData = getCachedMusicalTicks({
1672
+ ticksPerPixel: this.ticksPerPixel,
1673
+ meterEntries: this.meterEntries,
1674
+ ppqn: this.ppqn,
1675
+ startPixel: 0,
1676
+ endPixel: this.length
1677
+ });
1678
+ } else {
1679
+ this._tickData = null;
1680
+ }
1681
+ }
1682
+ render() {
1683
+ if (!this._tickData) return import_lit11.html``;
1684
+ const totalWidth = this.length;
1685
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1686
+ const indices = getVisibleChunkIndices(
1687
+ totalWidth,
1688
+ MAX_CANVAS_WIDTH2,
1689
+ this.visibleStart,
1690
+ this.visibleEnd
1691
+ );
1692
+ return import_lit11.html`
1693
+ <div class="container" style="width: ${totalWidth}px; height: ${this.height}px;">
1694
+ ${indices.map((i) => {
1695
+ const width = Math.min(MAX_CANVAS_WIDTH2, totalWidth - i * MAX_CANVAS_WIDTH2);
1696
+ return import_lit11.html`
1697
+ <canvas
1698
+ data-index=${i}
1699
+ width=${width * dpr}
1700
+ height=${this.height * dpr}
1701
+ style="left: ${i * MAX_CANVAS_WIDTH2}px; width: ${width}px; height: ${this.height}px;"
1702
+ ></canvas>
1703
+ `;
1704
+ })}
1705
+ </div>
1706
+ `;
1707
+ }
1708
+ updated() {
1709
+ this._drawGrid();
1710
+ }
1711
+ _drawGrid() {
1712
+ if (!this._tickData) return;
1713
+ const canvases = this.shadowRoot?.querySelectorAll("canvas");
1714
+ if (!canvases) return;
1715
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1716
+ const style = getComputedStyle(this);
1717
+ const barHighlight = style.getPropertyValue("--daw-grid-bar-highlight").trim() || "rgba(255,255,255,0.02)";
1718
+ const majorLine = style.getPropertyValue("--daw-grid-major-line").trim() || "rgba(255,255,255,0.1)";
1719
+ const minorLine = style.getPropertyValue("--daw-grid-minor-line").trim() || "rgba(255,255,255,0.06)";
1720
+ const { ticks, pixelsPerQuarterNote } = this._tickData;
1721
+ for (const canvas of canvases) {
1722
+ const idx = Number(canvas.dataset.index);
1723
+ const ctx = canvas.getContext("2d");
1724
+ if (!ctx) continue;
1725
+ const chunkLeft = idx * MAX_CANVAS_WIDTH2;
1726
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH2, this.length - chunkLeft);
1727
+ ctx.resetTransform();
1728
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1729
+ ctx.scale(dpr, dpr);
1730
+ if (pixelsPerQuarterNote * 4 >= import_core2.MIN_PIXELS_PER_UNIT) {
1731
+ ctx.fillStyle = barHighlight;
1732
+ const majorTicks = ticks.filter((t) => t.type === "major");
1733
+ for (let i = 0; i < majorTicks.length; i++) {
1734
+ if (majorTicks[i].barIndex % 2 === 1) {
1735
+ const x = majorTicks[i].pixel - chunkLeft;
1736
+ const lastMeter = this.meterEntries[this.meterEntries.length - 1];
1737
+ const lastBarWidth = pixelsPerQuarterNote * lastMeter.numerator * (4 / lastMeter.denominator);
1738
+ const nextX = i + 1 < majorTicks.length ? majorTicks[i + 1].pixel - chunkLeft : x + lastBarWidth;
1739
+ ctx.fillRect(x, 0, nextX - x, this.height);
1740
+ }
1741
+ }
1742
+ }
1743
+ ctx.lineWidth = 1;
1744
+ for (const tick of ticks) {
1745
+ if (tick.type === "minorMinor") continue;
1746
+ const localX = tick.pixel - chunkLeft;
1747
+ if (localX < 0 || localX >= canvasWidth) continue;
1748
+ ctx.strokeStyle = tick.type === "major" ? majorLine : minorLine;
1749
+ ctx.beginPath();
1750
+ ctx.moveTo(localX + 0.5, 0);
1751
+ ctx.lineTo(localX + 0.5, this.height);
1752
+ ctx.stroke();
1753
+ }
1754
+ }
1755
+ }
1756
+ };
1757
+ DawGridElement.styles = import_lit11.css`
1758
+ :host {
1759
+ display: block;
1760
+ position: absolute;
1761
+ top: 0;
1762
+ left: 0;
1763
+ pointer-events: none;
1764
+ z-index: 0;
1765
+ }
1766
+ .container {
1767
+ position: relative;
1768
+ }
1769
+ canvas {
1770
+ position: absolute;
1771
+ top: 0;
1772
+ }
1773
+ `;
1774
+ __decorateClass([
1775
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1776
+ ], DawGridElement.prototype, "ticksPerPixel", 2);
1777
+ __decorateClass([
1778
+ (0, import_decorators10.property)({ attribute: false })
1779
+ ], DawGridElement.prototype, "meterEntries", 2);
1780
+ __decorateClass([
1781
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1782
+ ], DawGridElement.prototype, "ppqn", 2);
1783
+ __decorateClass([
1784
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1785
+ ], DawGridElement.prototype, "visibleStart", 2);
1786
+ __decorateClass([
1787
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1788
+ ], DawGridElement.prototype, "visibleEnd", 2);
1789
+ __decorateClass([
1790
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1791
+ ], DawGridElement.prototype, "length", 2);
1792
+ __decorateClass([
1793
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1794
+ ], DawGridElement.prototype, "height", 2);
1795
+ DawGridElement = __decorateClass([
1796
+ (0, import_decorators10.customElement)("daw-grid")
1797
+ ], DawGridElement);
1798
+
1799
+ // src/styles/theme.ts
1800
+ var import_lit12 = require("lit");
1801
+ var hostStyles = import_lit12.css`
1526
1802
  :host {
1527
1803
  --daw-wave-color: #c49a6c;
1528
1804
  --daw-progress-color: #63c75f;
@@ -1538,7 +1814,7 @@ var hostStyles = import_lit11.css`
1538
1814
  --daw-clip-header-text: #e0d4c8;
1539
1815
  }
1540
1816
  `;
1541
- var clipStyles = import_lit11.css`
1817
+ var clipStyles = import_lit12.css`
1542
1818
  .clip-container {
1543
1819
  position: absolute;
1544
1820
  overflow: hidden;
@@ -1751,8 +2027,7 @@ var AudioResumeController = class {
1751
2027
  };
1752
2028
 
1753
2029
  // src/controllers/recording-controller.ts
1754
- var import_worklets = require("@waveform-playlist/worklets");
1755
- var import_recording = require("@waveform-playlist/recording");
2030
+ var import_core3 = require("@waveform-playlist/core");
1756
2031
  var RecordingController = class {
1757
2032
  constructor(host) {
1758
2033
  this._sessions = /* @__PURE__ */ new Map();
@@ -1789,7 +2064,15 @@ var RecordingController = class {
1789
2064
  const rawCtx = this._host.audioContext;
1790
2065
  this._host.resolveAudioContextSampleRate(rawCtx.sampleRate);
1791
2066
  if (!this._workletLoadedCtx || this._workletLoadedCtx !== rawCtx) {
1792
- await rawCtx.audioWorklet.addModule(import_worklets.recordingProcessorUrl);
2067
+ let recordingProcessorUrl;
2068
+ try {
2069
+ ({ recordingProcessorUrl } = await import("@waveform-playlist/worklets"));
2070
+ } catch {
2071
+ throw new Error(
2072
+ "Recording requires @waveform-playlist/worklets. Install it: npm install @waveform-playlist/worklets"
2073
+ );
2074
+ }
2075
+ await rawCtx.audioWorklet.addModule(recordingProcessorUrl);
1793
2076
  this._workletLoadedCtx = rawCtx;
1794
2077
  }
1795
2078
  const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount;
@@ -1905,8 +2188,8 @@ var RecordingController = class {
1905
2188
  return;
1906
2189
  }
1907
2190
  const stopCtx = this._host.audioContext;
1908
- const channelData = session.chunks.map((chunkArr) => (0, import_recording.concatenateAudioData)(chunkArr));
1909
- const audioBuffer = (0, import_recording.createAudioBuffer)(
2191
+ const channelData = session.chunks.map((chunkArr) => (0, import_core3.concatenateAudioData)(chunkArr));
2192
+ const audioBuffer = (0, import_core3.createAudioBuffer)(
1910
2193
  stopCtx,
1911
2194
  channelData,
1912
2195
  this._host.effectiveSampleRate,
@@ -1971,7 +2254,7 @@ var RecordingController = class {
1971
2254
  for (let ch = 0; ch < session.channelCount; ch++) {
1972
2255
  if (!channels[ch]) continue;
1973
2256
  const oldPeakCount = Math.floor(session.peaks[ch].length / 2);
1974
- session.peaks[ch] = (0, import_recording.appendPeaks)(
2257
+ session.peaks[ch] = (0, import_core3.appendPeaks)(
1975
2258
  session.peaks[ch],
1976
2259
  channels[ch],
1977
2260
  this._host.samplesPerPixel,
@@ -2039,7 +2322,7 @@ var RecordingController = class {
2039
2322
  };
2040
2323
 
2041
2324
  // src/interactions/pointer-handler.ts
2042
- var import_core = require("@waveform-playlist/core");
2325
+ var import_core4 = require("@waveform-playlist/core");
2043
2326
 
2044
2327
  // src/interactions/constants.ts
2045
2328
  var DRAG_THRESHOLD = 3;
@@ -2049,6 +2332,7 @@ var PointerHandler = class {
2049
2332
  constructor(host) {
2050
2333
  this._isDragging = false;
2051
2334
  this._dragStartPx = 0;
2335
+ this._dragStartTime = 0;
2052
2336
  this._timeline = null;
2053
2337
  // Cached from onPointerDown to avoid forced layout reflows at 60fps during drag
2054
2338
  this._timelineRect = null;
@@ -2095,21 +2379,18 @@ var PointerHandler = class {
2095
2379
  const currentPx = this._pxFromPointer(e);
2096
2380
  if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > DRAG_THRESHOLD) {
2097
2381
  this._isDragging = true;
2382
+ this._dragStartTime = this._pxToTime(this._dragStartPx);
2098
2383
  }
2099
2384
  if (this._isDragging) {
2100
2385
  const h = this._host;
2101
- const startTime = (0, import_core.pixelsToSeconds)(
2102
- this._dragStartPx,
2103
- h.samplesPerPixel,
2104
- h.effectiveSampleRate
2105
- );
2106
- const endTime = (0, import_core.pixelsToSeconds)(currentPx, h.samplesPerPixel, h.effectiveSampleRate);
2386
+ const startTime = this._dragStartTime;
2387
+ const endTime = this._pxToTime(currentPx);
2107
2388
  h._selectionStartTime = Math.min(startTime, endTime);
2108
2389
  h._selectionEndTime = Math.max(startTime, endTime);
2109
2390
  const sel = h.shadowRoot?.querySelector("daw-selection");
2110
2391
  if (sel) {
2111
- sel.startPx = h._selectionStartTime * h.effectiveSampleRate / h.samplesPerPixel;
2112
- sel.endPx = h._selectionEndTime * h.effectiveSampleRate / h.samplesPerPixel;
2392
+ sel.startPx = this._timeToPx(h._selectionStartTime);
2393
+ sel.endPx = this._timeToPx(h._selectionEndTime);
2113
2394
  }
2114
2395
  }
2115
2396
  };
@@ -2147,6 +2428,23 @@ var PointerHandler = class {
2147
2428
  }
2148
2429
  return e.clientX - this._timelineRect.left;
2149
2430
  }
2431
+ _pxToTime(px) {
2432
+ const h = this._host;
2433
+ if (h.scaleMode === "beats") {
2434
+ let tick = px * h.ticksPerPixel;
2435
+ tick = (0, import_core4.snapTickToGrid)(tick, h.snapTo, h._meterEntries, h.ppqn);
2436
+ return h._ticksToSeconds(tick);
2437
+ }
2438
+ return (0, import_core4.pixelsToSeconds)(px, h.samplesPerPixel, h.effectiveSampleRate);
2439
+ }
2440
+ _timeToPx(time) {
2441
+ const h = this._host;
2442
+ if (h.scaleMode === "beats") {
2443
+ const tick = h._secondsToTicks(time);
2444
+ return tick / h.ticksPerPixel;
2445
+ }
2446
+ return time * h.effectiveSampleRate / h.samplesPerPixel;
2447
+ }
2150
2448
  _finalizeSelection() {
2151
2449
  const h = this._host;
2152
2450
  if (h._engine) {
@@ -2164,7 +2462,7 @@ var PointerHandler = class {
2164
2462
  _handleSeekClick(e) {
2165
2463
  const h = this._host;
2166
2464
  const px = this._pxFromPointer(e);
2167
- const time = (0, import_core.pixelsToSeconds)(px, h.samplesPerPixel, h.effectiveSampleRate);
2465
+ const time = this._pxToTime(px);
2168
2466
  h._selectionStartTime = 0;
2169
2467
  h._selectionEndTime = 0;
2170
2468
  if (this._timeline) {
@@ -2229,6 +2527,7 @@ var PointerHandler = class {
2229
2527
  };
2230
2528
 
2231
2529
  // src/interactions/clip-pointer-handler.ts
2530
+ var import_core5 = require("@waveform-playlist/core");
2232
2531
  var ClipPointerHandler = class {
2233
2532
  constructor(host) {
2234
2533
  this._mode = null;
@@ -2236,7 +2535,6 @@ var ClipPointerHandler = class {
2236
2535
  this._trackId = "";
2237
2536
  this._startPx = 0;
2238
2537
  this._isDragging = false;
2239
- this._lastDeltaPx = 0;
2240
2538
  this._cumulativeDeltaSamples = 0;
2241
2539
  // Trim visual feedback: snapshot of original clip state
2242
2540
  this._clipContainer = null;
@@ -2245,8 +2543,31 @@ var ClipPointerHandler = class {
2245
2543
  this._originalWidth = 0;
2246
2544
  this._originalOffsetSamples = 0;
2247
2545
  this._originalDurationSamples = 0;
2546
+ this._originalStartSample = 0;
2248
2547
  this._host = host;
2249
2548
  }
2549
+ /**
2550
+ * Convert a pixel delta to samples, snapping in tick space when in beats mode.
2551
+ *
2552
+ * The anchor is the absolute sample position being moved (e.g., clip start
2553
+ * for move/left-trim, clip end for right-trim). Snapping the absolute
2554
+ * position — not just the delta — ensures clips land exactly on grid lines
2555
+ * even if they started off-grid.
2556
+ */
2557
+ _snapDeltaToSamples(totalDeltaPx, anchorSample) {
2558
+ const h = this._host;
2559
+ if (h.scaleMode === "beats") {
2560
+ const anchorSeconds = anchorSample / h.effectiveSampleRate;
2561
+ const anchorTick = h._secondsToTicks(anchorSeconds);
2562
+ const deltaTicks = totalDeltaPx * h.ticksPerPixel;
2563
+ const targetTick = anchorTick + deltaTicks;
2564
+ const snappedTick = h.snapTo !== "off" ? (0, import_core5.snapTickToGrid)(targetTick, h.snapTo, h._meterEntries, h.ppqn) : targetTick;
2565
+ const snappedSeconds = h._ticksToSeconds(snappedTick);
2566
+ const snappedSample = Math.round(snappedSeconds * h.effectiveSampleRate);
2567
+ return snappedSample - anchorSample;
2568
+ }
2569
+ return Math.round(totalDeltaPx * h.renderSamplesPerPixel);
2570
+ }
2250
2571
  /** Returns true if a drag interaction is currently in progress. */
2251
2572
  get isActive() {
2252
2573
  return this._mode !== null;
@@ -2283,10 +2604,16 @@ var ClipPointerHandler = class {
2283
2604
  this._trackId = trackId;
2284
2605
  this._startPx = e.clientX;
2285
2606
  this._isDragging = false;
2286
- this._lastDeltaPx = 0;
2287
2607
  this._cumulativeDeltaSamples = 0;
2288
- if (this._host.engine) {
2289
- this._host.engine.beginTransaction();
2608
+ const engine = this._host.engine;
2609
+ if (engine) {
2610
+ const bounds = engine.getClipBounds(trackId, clipId);
2611
+ if (bounds) {
2612
+ this._originalStartSample = bounds.startSample;
2613
+ }
2614
+ }
2615
+ if (engine) {
2616
+ engine.beginTransaction();
2290
2617
  } else {
2291
2618
  console.warn(
2292
2619
  "[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
@@ -2303,9 +2630,9 @@ var ClipPointerHandler = class {
2303
2630
  } else {
2304
2631
  console.warn("[dawcore] clip container not found for trim visual feedback: " + clipId);
2305
2632
  }
2306
- const engine = this._host.engine;
2307
- if (engine) {
2308
- const bounds = engine.getClipBounds(trackId, clipId);
2633
+ const engine2 = this._host.engine;
2634
+ if (engine2) {
2635
+ const bounds = engine2.getClipBounds(trackId, clipId);
2309
2636
  if (bounds) {
2310
2637
  this._originalOffsetSamples = bounds.offsetSamples;
2311
2638
  this._originalDurationSamples = bounds.durationSamples;
@@ -2327,21 +2654,33 @@ var ClipPointerHandler = class {
2327
2654
  const engine = this._host.engine;
2328
2655
  if (!engine) return;
2329
2656
  if (this._mode === "move") {
2330
- const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
2331
- this._lastDeltaPx = totalDeltaPx;
2332
- const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
2333
- const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2334
- this._cumulativeDeltaSamples += applied;
2657
+ const totalSnappedDelta = this._snapDeltaToSamples(totalDeltaPx, this._originalStartSample);
2658
+ const incrementalDeltaSamples = totalSnappedDelta - this._cumulativeDeltaSamples;
2659
+ if (incrementalDeltaSamples !== 0) {
2660
+ const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2661
+ this._cumulativeDeltaSamples += applied;
2662
+ }
2335
2663
  } else {
2336
2664
  const boundary = this._mode === "trim-left" ? "left" : "right";
2337
- const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
2665
+ const anchor = boundary === "left" ? this._originalStartSample : this._originalStartSample + this._originalDurationSamples;
2666
+ const rawDeltaSamples = this._snapDeltaToSamples(totalDeltaPx, anchor);
2338
2667
  const deltaSamples = engine.constrainTrimDelta(
2339
2668
  this._trackId,
2340
2669
  this._clipId,
2341
2670
  boundary,
2342
2671
  rawDeltaSamples
2343
2672
  );
2344
- const deltaPx = Math.round(deltaSamples / this._host.samplesPerPixel);
2673
+ let deltaPx;
2674
+ if (this._host.scaleMode === "beats") {
2675
+ const h = this._host;
2676
+ const anchorSec = anchor / h.effectiveSampleRate;
2677
+ const anchorTick = h._secondsToTicks(anchorSec);
2678
+ const newSec = anchorSec + deltaSamples / h.effectiveSampleRate;
2679
+ const newTick = h._secondsToTicks(newSec);
2680
+ deltaPx = Math.round((newTick - anchorTick) / h.ticksPerPixel);
2681
+ } else {
2682
+ deltaPx = Math.round(deltaSamples / this._host.renderSamplesPerPixel);
2683
+ }
2345
2684
  this._cumulativeDeltaSamples = deltaSamples;
2346
2685
  if (this._clipContainer) {
2347
2686
  if (this._mode === "trim-left") {
@@ -2473,18 +2812,18 @@ var ClipPointerHandler = class {
2473
2812
  this._trackId = "";
2474
2813
  this._startPx = 0;
2475
2814
  this._isDragging = false;
2476
- this._lastDeltaPx = 0;
2477
2815
  this._cumulativeDeltaSamples = 0;
2478
2816
  this._clipContainer = null;
2479
2817
  this._originalLeft = 0;
2480
2818
  this._originalWidth = 0;
2481
2819
  this._originalOffsetSamples = 0;
2482
2820
  this._originalDurationSamples = 0;
2821
+ this._originalStartSample = 0;
2483
2822
  }
2484
2823
  };
2485
2824
 
2486
2825
  // src/interactions/file-loader.ts
2487
- var import_core2 = require("@waveform-playlist/core");
2826
+ var import_core6 = require("@waveform-playlist/core");
2488
2827
  async function loadFiles(host, files) {
2489
2828
  if (!files) {
2490
2829
  console.warn("[dawcore] loadFiles called with null/undefined");
@@ -2506,31 +2845,31 @@ async function loadFiles(host, files) {
2506
2845
  host._audioCache.delete(blobUrl);
2507
2846
  host._resolvedSampleRate = audioBuffer.sampleRate;
2508
2847
  const name = file.name.replace(/\.\w+$/, "");
2509
- const clip = (0, import_core2.createClipFromSeconds)({
2848
+ const clip = (0, import_core6.createClip)({
2510
2849
  audioBuffer,
2511
- startTime: 0,
2512
- duration: audioBuffer.duration,
2513
- offset: 0,
2850
+ startSample: 0,
2851
+ durationSamples: audioBuffer.length,
2852
+ offsetSamples: 0,
2514
2853
  gain: 1,
2515
2854
  name,
2516
2855
  sampleRate: audioBuffer.sampleRate,
2517
- sourceDuration: audioBuffer.duration
2856
+ sourceDurationSamples: audioBuffer.length
2518
2857
  });
2519
2858
  host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
2520
- host._clipOffsets.set(clip.id, {
2859
+ host._clipOffsets = new Map(host._clipOffsets).set(clip.id, {
2521
2860
  offsetSamples: clip.offsetSamples,
2522
2861
  durationSamples: clip.durationSamples
2523
2862
  });
2524
2863
  const peakData = await host._peakPipeline.generatePeaks(
2525
2864
  audioBuffer,
2526
- host.samplesPerPixel,
2865
+ host.renderSamplesPerPixel,
2527
2866
  host.mono,
2528
2867
  clip.offsetSamples,
2529
2868
  clip.durationSamples
2530
2869
  );
2531
2870
  host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
2532
2871
  const trackId = crypto.randomUUID();
2533
- const track = (0, import_core2.createTrack)({ name, clips: [clip] });
2872
+ const track = (0, import_core6.createTrack)({ name, clips: [clip] });
2534
2873
  track.id = trackId;
2535
2874
  host._tracks = new Map(host._tracks).set(trackId, {
2536
2875
  name,
@@ -2585,7 +2924,7 @@ async function loadFiles(host, files) {
2585
2924
  }
2586
2925
 
2587
2926
  // src/interactions/recording-clip.ts
2588
- var import_core3 = require("@waveform-playlist/core");
2927
+ var import_core7 = require("@waveform-playlist/core");
2589
2928
  function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
2590
2929
  let trimmedBuf = buf;
2591
2930
  if (offsetSamples > 0 && offsetSamples < buf.length) {
@@ -2600,7 +2939,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
2600
2939
  }
2601
2940
  trimmedBuf = trimmed;
2602
2941
  }
2603
- const clip = (0, import_core3.createClip)({
2942
+ const clip = (0, import_core7.createClip)({
2604
2943
  audioBuffer: trimmedBuf,
2605
2944
  startSample,
2606
2945
  durationSamples: durSamples,
@@ -2768,13 +3107,13 @@ function syncPeaksForChangedClips(host, tracks) {
2768
3107
  continue;
2769
3108
  }
2770
3109
  host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
2771
- host._clipOffsets.set(clip.id, {
3110
+ host._clipOffsets = new Map(host._clipOffsets).set(clip.id, {
2772
3111
  offsetSamples: clip.offsetSamples,
2773
3112
  durationSamples: clip.durationSamples
2774
3113
  });
2775
3114
  host._peakPipeline.generatePeaks(
2776
3115
  audioBuffer,
2777
- host.samplesPerPixel,
3116
+ host.renderSamplesPerPixel,
2778
3117
  host.mono,
2779
3118
  clip.offsetSamples,
2780
3119
  clip.durationSamples
@@ -2849,7 +3188,7 @@ async function loadWaveformDataFromUrl(src) {
2849
3188
  }
2850
3189
 
2851
3190
  // src/elements/daw-editor.ts
2852
- var DawEditorElement = class extends import_lit12.LitElement {
3191
+ var DawEditorElement = class extends import_lit13.LitElement {
2853
3192
  constructor() {
2854
3193
  super(...arguments);
2855
3194
  this._samplesPerPixel = 1024;
@@ -2862,6 +3201,12 @@ var DawEditorElement = class extends import_lit12.LitElement {
2862
3201
  this.clipHeaders = false;
2863
3202
  this.clipHeaderHeight = 20;
2864
3203
  this.interactiveClips = false;
3204
+ this.scaleMode = "temporal";
3205
+ this._ticksPerPixel = 24;
3206
+ this._bpm = 120;
3207
+ this.timeSignature = [4, 4];
3208
+ this._ppqn = 960;
3209
+ this.snapTo = "off";
2865
3210
  this.sampleRate = 48e3;
2866
3211
  /** Resolved sample rate — falls back to sampleRate property until first audio decode. */
2867
3212
  this._resolvedSampleRate = null;
@@ -2880,6 +3225,9 @@ var DawEditorElement = class extends import_lit12.LitElement {
2880
3225
  this._externalAudioContext = null;
2881
3226
  this._ownedAudioContext = null;
2882
3227
  this._engine = null;
3228
+ this._adapter = null;
3229
+ this._warnedMissingTicksToSeconds = false;
3230
+ this._warnedMissingSecondsToTicks = false;
2883
3231
  this._enginePromise = null;
2884
3232
  this._audioCache = /* @__PURE__ */ new Map();
2885
3233
  this._peaksCache = /* @__PURE__ */ new Map();
@@ -3024,6 +3372,41 @@ var DawEditorElement = class extends import_lit12.LitElement {
3024
3372
  this._samplesPerPixel = clamped;
3025
3373
  this.requestUpdate("samplesPerPixel", old);
3026
3374
  }
3375
+ get ticksPerPixel() {
3376
+ return this._ticksPerPixel;
3377
+ }
3378
+ set ticksPerPixel(value) {
3379
+ const old = this._ticksPerPixel;
3380
+ if (!Number.isFinite(value) || value <= 0) return;
3381
+ this._ticksPerPixel = value;
3382
+ this.requestUpdate("ticksPerPixel", old);
3383
+ }
3384
+ get bpm() {
3385
+ return this._bpm;
3386
+ }
3387
+ set bpm(value) {
3388
+ const old = this._bpm;
3389
+ if (!Number.isFinite(value) || value <= 0) return;
3390
+ this._bpm = value;
3391
+ if (this._engine) {
3392
+ this._engine.setTempo(value);
3393
+ }
3394
+ this.requestUpdate("bpm", old);
3395
+ }
3396
+ /** MeterEntries for grid/ruler: explicit meterEntries if set, otherwise derived from timeSignature. */
3397
+ get _meterEntries() {
3398
+ if (this.meterEntries && this.meterEntries.length > 0) return this.meterEntries;
3399
+ return [{ tick: 0, numerator: this.timeSignature[0], denominator: this.timeSignature[1] }];
3400
+ }
3401
+ get ppqn() {
3402
+ return this._ppqn;
3403
+ }
3404
+ set ppqn(value) {
3405
+ const old = this._ppqn;
3406
+ if (!Number.isFinite(value) || value <= 0) return;
3407
+ this._ppqn = value;
3408
+ this.requestUpdate("ppqn", old);
3409
+ }
3027
3410
  /** Set an AudioContext to use for all audio operations. Must be set before tracks load. */
3028
3411
  set audioContext(ctx) {
3029
3412
  if (ctx && ctx.state === "closed") {
@@ -3055,6 +3438,13 @@ var DawEditorElement = class extends import_lit12.LitElement {
3055
3438
  get engine() {
3056
3439
  return this._engine;
3057
3440
  }
3441
+ /** The adapter's Transport — use for tempo, metronome, and effects. */
3442
+ get transport() {
3443
+ return this._adapter?.transport ?? null;
3444
+ }
3445
+ get renderSamplesPerPixel() {
3446
+ return this._renderSpp;
3447
+ }
3058
3448
  /** Re-extract peaks for a clip at new offset/duration from cached WaveformData. */
3059
3449
  reextractClipPeaks(clipId, offsetSamples, durationSamples) {
3060
3450
  const buf = this._clipBuffers.get(clipId);
@@ -3063,7 +3453,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3063
3453
  const singleClipOffsets = /* @__PURE__ */ new Map([[clipId, { offsetSamples, durationSamples }]]);
3064
3454
  const result = this._peakPipeline.reextractPeaks(
3065
3455
  singleClipBuffers,
3066
- this.samplesPerPixel,
3456
+ this._renderSpp,
3067
3457
  this.mono,
3068
3458
  singleClipOffsets
3069
3459
  );
@@ -3077,9 +3467,59 @@ var DawEditorElement = class extends import_lit12.LitElement {
3077
3467
  resolveAudioContextSampleRate(rate) {
3078
3468
  if (!this._resolvedSampleRate) this._resolvedSampleRate = rate;
3079
3469
  }
3470
+ /**
3471
+ * In beats mode, derive samplesPerPixel from ticksPerPixel so that
3472
+ * clip positions, waveforms, and the tick-space grid all align.
3473
+ */
3474
+ get _renderSpp() {
3475
+ if (this.scaleMode === "beats") {
3476
+ const spp = Math.ceil(
3477
+ 60 * this.effectiveSampleRate * this.ticksPerPixel / (this.ppqn * this.bpm)
3478
+ );
3479
+ return this._minSamplesPerPixel > 0 ? Math.max(spp, this._minSamplesPerPixel) : spp;
3480
+ }
3481
+ return this.samplesPerPixel;
3482
+ }
3483
+ /** Convert seconds to ticks — uses callback if provided, otherwise single-BPM fallback. */
3484
+ _secondsToTicks(seconds) {
3485
+ if (this.secondsToTicks) {
3486
+ if (!this.ticksToSeconds && !this._warnedMissingTicksToSeconds) {
3487
+ this._warnedMissingTicksToSeconds = true;
3488
+ console.warn(
3489
+ "[waveform-playlist] daw-editor: secondsToTicks is set but ticksToSeconds is missing. Both callbacks are required for variable tempo."
3490
+ );
3491
+ }
3492
+ return this.secondsToTicks(seconds);
3493
+ }
3494
+ return seconds * this.bpm * this.ppqn / 60;
3495
+ }
3496
+ /** Convert ticks to seconds — uses callback if provided, otherwise single-BPM fallback. */
3497
+ _ticksToSeconds(ticks) {
3498
+ if (this.ticksToSeconds) {
3499
+ if (!this.secondsToTicks && !this._warnedMissingSecondsToTicks) {
3500
+ this._warnedMissingSecondsToTicks = true;
3501
+ console.warn(
3502
+ "[waveform-playlist] daw-editor: ticksToSeconds is set but secondsToTicks is missing. Both callbacks are required for variable tempo."
3503
+ );
3504
+ }
3505
+ return this.ticksToSeconds(ticks);
3506
+ }
3507
+ return ticks * 60 / (this.bpm * this.ppqn);
3508
+ }
3080
3509
  get _totalWidth() {
3510
+ if (this.scaleMode === "beats") {
3511
+ const contentTicks = this._secondsToTicks(this._duration);
3512
+ const [num] = this.timeSignature;
3513
+ const minTicks = 32 * num * this.ppqn;
3514
+ return Math.ceil(Math.max(contentTicks, minTicks) / this.ticksPerPixel);
3515
+ }
3081
3516
  return Math.ceil(this._duration * this.effectiveSampleRate / this.samplesPerPixel);
3082
3517
  }
3518
+ /** Grid height when no tracks exist — matches scroll area's rendered height. */
3519
+ get _emptyGridHeight() {
3520
+ const scrollArea = this.shadowRoot?.querySelector(".scroll-area");
3521
+ return scrollArea?.clientHeight ?? 200;
3522
+ }
3083
3523
  _setSelectedTrackId(trackId) {
3084
3524
  this._selectedTrackId = trackId;
3085
3525
  }
@@ -3165,13 +3605,14 @@ var DawEditorElement = class extends import_lit12.LitElement {
3165
3605
  if (changedProperties.has("eagerResume")) {
3166
3606
  this._audioResume.target = this.eagerResume;
3167
3607
  }
3168
- if (changedProperties.has("samplesPerPixel") && this._isPlaying) {
3608
+ if ((changedProperties.has("samplesPerPixel") || changedProperties.has("ticksPerPixel") || changedProperties.has("bpm") || changedProperties.has("secondsToTicks")) && this._isPlaying) {
3169
3609
  this._startPlayhead();
3170
3610
  }
3171
- if (changedProperties.has("samplesPerPixel") && this._clipBuffers.size > 0) {
3611
+ const zoomChanged = changedProperties.has("samplesPerPixel") || changedProperties.has("ticksPerPixel") || changedProperties.has("bpm") || changedProperties.has("scaleMode") || changedProperties.has("secondsToTicks");
3612
+ if (zoomChanged && this._clipBuffers.size > 0) {
3172
3613
  const re = this._peakPipeline.reextractPeaks(
3173
3614
  this._clipBuffers,
3174
- this.samplesPerPixel,
3615
+ this._renderSpp,
3175
3616
  this.mono,
3176
3617
  this._clipOffsets
3177
3618
  );
@@ -3280,7 +3721,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3280
3721
  }
3281
3722
  if (waveformData) {
3282
3723
  const wdRate = waveformData.sample_rate;
3283
- const clip2 = (0, import_core4.createClip)({
3724
+ const clip2 = (0, import_core8.createClip)({
3284
3725
  waveformData,
3285
3726
  startSample: Math.round(clipDesc.start * wdRate),
3286
3727
  durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
@@ -3290,7 +3731,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3290
3731
  sampleRate: wdRate,
3291
3732
  sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
3292
3733
  });
3293
- const effectiveScale = Math.max(this.samplesPerPixel, waveformData.scale);
3734
+ const effectiveScale = Math.max(this._renderSpp, waveformData.scale);
3294
3735
  const peakData2 = extractPeaks(
3295
3736
  waveformData,
3296
3737
  effectiveScale,
@@ -3304,7 +3745,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3304
3745
  });
3305
3746
  this._peaksData = new Map(this._peaksData).set(clip2.id, peakData2);
3306
3747
  this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
3307
- const previewTrack = (0, import_core4.createTrack)({
3748
+ const previewTrack = (0, import_core8.createTrack)({
3308
3749
  name: descriptor.name,
3309
3750
  clips: [clip2],
3310
3751
  volume: descriptor.volume,
@@ -3339,7 +3780,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3339
3780
  }
3340
3781
  const audioBuffer = await audioPromise;
3341
3782
  this._resolvedSampleRate = audioBuffer.sampleRate;
3342
- const clip = (0, import_core4.createClipFromSeconds)({
3783
+ const clip = (0, import_core8.createClipFromSeconds)({
3343
3784
  audioBuffer,
3344
3785
  startTime: clipDesc.start,
3345
3786
  duration: clipDesc.duration || audioBuffer.duration,
@@ -3356,7 +3797,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3356
3797
  });
3357
3798
  const peakData = await this._peakPipeline.generatePeaks(
3358
3799
  audioBuffer,
3359
- this.samplesPerPixel,
3800
+ this._renderSpp,
3360
3801
  this.mono,
3361
3802
  clip.offsetSamples,
3362
3803
  clip.durationSamples
@@ -3364,7 +3805,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3364
3805
  this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
3365
3806
  clips.push(clip);
3366
3807
  }
3367
- const track = (0, import_core4.createTrack)({
3808
+ const track = (0, import_core8.createTrack)({
3368
3809
  name: descriptor.name,
3369
3810
  clips,
3370
3811
  volume: descriptor.volume,
@@ -3454,10 +3895,14 @@ var DawEditorElement = class extends import_lit12.LitElement {
3454
3895
  import("@dawcore/transport")
3455
3896
  ]);
3456
3897
  const adapter = new NativePlayoutAdapter(this.audioContext);
3898
+ this._adapter = adapter;
3899
+ adapter.setTempo(this._bpm);
3457
3900
  const engine = new PlaylistEngine({
3458
3901
  adapter,
3459
3902
  sampleRate: this.effectiveSampleRate,
3460
3903
  samplesPerPixel: this.samplesPerPixel,
3904
+ bpm: this._bpm,
3905
+ ppqn: this._ppqn,
3461
3906
  zoomLevels: [256, 512, 1024, 2048, 4096, 8192, this.samplesPerPixel].filter((v, i, a) => a.indexOf(v) === i).sort((a, b) => a - b)
3462
3907
  });
3463
3908
  let lastTracksVersion = -1;
@@ -3490,6 +3935,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3490
3935
  this._engine.dispose();
3491
3936
  this._engine = null;
3492
3937
  }
3938
+ this._adapter = null;
3493
3939
  this._enginePromise = null;
3494
3940
  }
3495
3941
  async loadFiles(files) {
@@ -3627,12 +4073,13 @@ var DawEditorElement = class extends import_lit12.LitElement {
3627
4073
  if (!rs) return "";
3628
4074
  const audibleSamples = Math.max(0, rs.totalSamples - rs.latencySamples);
3629
4075
  if (audibleSamples === 0) return "";
3630
- const latencyPixels = Math.floor(rs.latencySamples / this.samplesPerPixel);
3631
- const left = Math.floor(rs.startSample / this.samplesPerPixel);
3632
- const w = Math.floor(audibleSamples / this.samplesPerPixel);
4076
+ const renderSpp = this._renderSpp;
4077
+ const latencyPixels = Math.floor(rs.latencySamples / renderSpp);
4078
+ const left = Math.floor(rs.startSample / renderSpp);
4079
+ const w = Math.floor(audibleSamples / renderSpp);
3633
4080
  return rs.peaks.map((chPeaks, ch) => {
3634
4081
  const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
3635
- return import_lit12.html`
4082
+ return import_lit13.html`
3636
4083
  <daw-waveform
3637
4084
  data-recording-track=${trackId}
3638
4085
  data-recording-channel=${ch}
@@ -3655,19 +4102,39 @@ var DawEditorElement = class extends import_lit12.LitElement {
3655
4102
  if (!playhead || !this._engine) return;
3656
4103
  const engine = this._engine;
3657
4104
  const ctx = this.audioContext;
3658
- playhead.startAnimation(
3659
- () => {
3660
- const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
3661
- return Math.max(0, engine.getCurrentTime() - latency);
3662
- },
3663
- this.effectiveSampleRate,
3664
- this.samplesPerPixel
3665
- );
4105
+ if (this.scaleMode === "beats") {
4106
+ const secondsToTicksFn = (s) => this._secondsToTicks(s);
4107
+ playhead.startBeatsAnimationWithMap(
4108
+ () => {
4109
+ const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
4110
+ return Math.max(0, engine.getCurrentTime() - latency);
4111
+ },
4112
+ secondsToTicksFn,
4113
+ this.ticksPerPixel
4114
+ );
4115
+ } else {
4116
+ playhead.startAnimation(
4117
+ () => {
4118
+ const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
4119
+ return Math.max(0, engine.getCurrentTime() - latency);
4120
+ },
4121
+ this.effectiveSampleRate,
4122
+ this.samplesPerPixel
4123
+ );
4124
+ }
3666
4125
  }
3667
4126
  _stopPlayhead() {
3668
4127
  const playhead = this._getPlayhead();
3669
4128
  if (!playhead) return;
3670
- playhead.stopAnimation(this._currentTime, this.effectiveSampleRate, this.samplesPerPixel);
4129
+ if (this.scaleMode === "beats") {
4130
+ playhead.stopBeatsAnimationWithMap(
4131
+ this._currentTime,
4132
+ (s) => this._secondsToTicks(s),
4133
+ this.ticksPerPixel
4134
+ );
4135
+ } else {
4136
+ playhead.stopAnimation(this._currentTime, this.effectiveSampleRate, this.samplesPerPixel);
4137
+ }
3671
4138
  }
3672
4139
  _getPlayhead() {
3673
4140
  return this.shadowRoot?.querySelector("daw-playhead");
@@ -3688,8 +4155,18 @@ var DawEditorElement = class extends import_lit12.LitElement {
3688
4155
  // --- Render ---
3689
4156
  render() {
3690
4157
  const sr = this.effectiveSampleRate;
3691
- const selStartPx = this._selectionStartTime * sr / this.samplesPerPixel;
3692
- const selEndPx = this._selectionEndTime * sr / this.samplesPerPixel;
4158
+ const spp = this._renderSpp;
4159
+ let selStartPx;
4160
+ let selEndPx;
4161
+ if (this.scaleMode === "beats") {
4162
+ const startTick = this._secondsToTicks(this._selectionStartTime);
4163
+ const endTick = this._secondsToTicks(this._selectionEndTime);
4164
+ selStartPx = startTick / this.ticksPerPixel;
4165
+ selEndPx = endTick / this.ticksPerPixel;
4166
+ } else {
4167
+ selStartPx = this._selectionStartTime * sr / spp;
4168
+ selEndPx = this._selectionEndTime * sr / spp;
4169
+ }
3693
4170
  const orderedTracks = this._getOrderedTracks().map(([trackId, track]) => {
3694
4171
  const descriptor = this._tracks.get(trackId);
3695
4172
  const firstPeaks = track.clips.map((c) => this._peaksData.get(c.id)).find((p) => p && p.data.length > 0);
@@ -3703,11 +4180,11 @@ var DawEditorElement = class extends import_lit12.LitElement {
3703
4180
  trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
3704
4181
  };
3705
4182
  });
3706
- return import_lit12.html`
3707
- ${orderedTracks.length > 0 ? import_lit12.html`<div class="controls-column">
3708
- ${this.timescale ? import_lit12.html`<div style="height: 30px;"></div>` : ""}
4183
+ return import_lit13.html`
4184
+ ${orderedTracks.length > 0 ? import_lit13.html`<div class="controls-column">
4185
+ ${this.timescale ? import_lit13.html`<div style="height: 30px;"></div>` : ""}
3709
4186
  ${orderedTracks.map(
3710
- (t) => import_lit12.html`
4187
+ (t) => import_lit13.html`
3711
4188
  <daw-track-controls
3712
4189
  style="height: ${t.trackHeight}px;"
3713
4190
  .trackId=${t.trackId}
@@ -3730,16 +4207,31 @@ var DawEditorElement = class extends import_lit12.LitElement {
3730
4207
  @dragleave=${this._onDragLeave}
3731
4208
  @drop=${this._onDrop}
3732
4209
  >
3733
- ${orderedTracks.length > 0 && this.timescale ? import_lit12.html`<daw-ruler
3734
- .samplesPerPixel=${this.samplesPerPixel}
4210
+ ${(orderedTracks.length > 0 || this.scaleMode === "beats") && this.timescale ? import_lit13.html`<daw-ruler
4211
+ .samplesPerPixel=${spp}
3735
4212
  .sampleRate=${this.effectiveSampleRate}
3736
4213
  .duration=${this._duration}
4214
+ .scaleMode=${this.scaleMode}
4215
+ .ticksPerPixel=${this.ticksPerPixel}
4216
+ .meterEntries=${this._meterEntries}
4217
+ .ppqn=${this.ppqn}
4218
+ .totalWidth=${this._totalWidth}
3737
4219
  ></daw-ruler>` : ""}
3738
- ${orderedTracks.length > 0 ? import_lit12.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
4220
+ ${this.scaleMode === "beats" ? import_lit13.html`<daw-grid
4221
+ style="top: ${this.timescale ? 30 : 0}px;"
4222
+ .ticksPerPixel=${this.ticksPerPixel}
4223
+ .meterEntries=${this._meterEntries}
4224
+ .ppqn=${this.ppqn}
4225
+ .visibleStart=${this._viewport.visibleStart}
4226
+ .visibleEnd=${this._viewport.visibleEnd}
4227
+ .length=${this._totalWidth}
4228
+ .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
4229
+ ></daw-grid>` : ""}
4230
+ ${orderedTracks.length > 0 || this.scaleMode === "beats" ? import_lit13.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
3739
4231
  <daw-playhead></daw-playhead>` : ""}
3740
4232
  ${orderedTracks.map((t) => {
3741
4233
  const channelHeight = this.waveHeight;
3742
- return import_lit12.html`
4234
+ return import_lit13.html`
3743
4235
  <div
3744
4236
  class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
3745
4237
  style="height: ${t.trackHeight}px;"
@@ -3747,21 +4239,67 @@ var DawEditorElement = class extends import_lit12.LitElement {
3747
4239
  >
3748
4240
  ${t.track.clips.map((clip) => {
3749
4241
  const peakData = this._peaksData.get(clip.id);
3750
- const width = (0, import_core4.clipPixelWidth)(
3751
- clip.startSample,
3752
- clip.durationSamples,
3753
- this.samplesPerPixel
3754
- );
3755
- const clipLeft = Math.floor(clip.startSample / this.samplesPerPixel);
3756
- const channels = peakData?.data ?? [new Int16Array(0)];
4242
+ let clipLeft;
4243
+ let width;
4244
+ if (this.scaleMode === "beats") {
4245
+ const startTick = clip.startTick !== void 0 ? clip.startTick : this._secondsToTicks(clip.startSample / sr);
4246
+ const durSec = clip.durationSamples / sr;
4247
+ const startSec = clip.startTick !== void 0 ? this._ticksToSeconds(clip.startTick) : clip.startSample / sr;
4248
+ const endTick = this._secondsToTicks(startSec + durSec);
4249
+ clipLeft = Math.round(startTick / this.ticksPerPixel);
4250
+ width = Math.round(endTick / this.ticksPerPixel) - clipLeft;
4251
+ } else {
4252
+ clipLeft = Math.floor(clip.startSample / spp);
4253
+ width = (0, import_core8.clipPixelWidth)(clip.startSample, clip.durationSamples, spp);
4254
+ }
4255
+ let clipSegments;
4256
+ let segmentChannels;
4257
+ if (this.scaleMode === "beats" && this.secondsToTicks) {
4258
+ const audioBuffer = this._clipBuffers.get(clip.id);
4259
+ const basePeaks = audioBuffer ? this._peakPipeline.getBaseScalePeaks(
4260
+ audioBuffer,
4261
+ this.mono,
4262
+ clip.offsetSamples,
4263
+ clip.durationSamples
4264
+ ) : null;
4265
+ if (basePeaks) {
4266
+ const baseScale = basePeaks.scale;
4267
+ segmentChannels = basePeaks.peaks.data;
4268
+ const MIN_RENDER_STEP = 80;
4269
+ const stepTicks = Math.max(MIN_RENDER_STEP, Math.ceil(this.ticksPerPixel));
4270
+ const startSec = clip.startTick !== void 0 ? this._ticksToSeconds(clip.startTick) : clip.startSample / sr;
4271
+ const clipOffsetSec = clip.offsetSamples / sr;
4272
+ const segStartTick = clip.startTick !== void 0 ? clip.startTick : this._secondsToTicks(startSec);
4273
+ const endTick = this._secondsToTicks(startSec + clip.durationSamples / sr);
4274
+ clipSegments = [];
4275
+ for (let tick = segStartTick; tick < endTick; tick += stepTicks) {
4276
+ const segEndTick = Math.min(tick + stepTicks, endTick);
4277
+ const segStartAudioSec = this._ticksToSeconds(tick) - startSec + clipOffsetSec;
4278
+ const segEndAudioSec = this._ticksToSeconds(segEndTick) - startSec + clipOffsetSec;
4279
+ const segStartSample = Math.round(segStartAudioSec * sr);
4280
+ const segEndSample = Math.round(segEndAudioSec * sr);
4281
+ const totalPeaks = clip.durationSamples / baseScale;
4282
+ clipSegments.push({
4283
+ peakStart: Math.max(0, (segStartSample - clip.offsetSamples) / baseScale),
4284
+ peakEnd: Math.min(
4285
+ totalPeaks,
4286
+ (segEndSample - clip.offsetSamples) / baseScale
4287
+ ),
4288
+ pixelStart: (tick - segStartTick) / this.ticksPerPixel,
4289
+ pixelEnd: (segEndTick - segStartTick) / this.ticksPerPixel
4290
+ });
4291
+ }
4292
+ }
4293
+ }
4294
+ const channels = segmentChannels ?? peakData?.data ?? [new Int16Array(0)];
3757
4295
  const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
3758
4296
  const chH = this.waveHeight;
3759
- return import_lit12.html` <div
4297
+ return import_lit13.html` <div
3760
4298
  class="clip-container"
3761
4299
  style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
3762
4300
  data-clip-id=${clip.id}
3763
4301
  >
3764
- ${hdrH > 0 ? import_lit12.html`<div
4302
+ ${hdrH > 0 ? import_lit13.html`<div
3765
4303
  class="clip-header"
3766
4304
  data-clip-id=${clip.id}
3767
4305
  data-track-id=${t.trackId}
@@ -3770,7 +4308,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3770
4308
  <span>${clip.name || t.descriptor?.name || ""}</span>
3771
4309
  </div>` : ""}
3772
4310
  ${channels.map(
3773
- (chPeaks, chIdx) => import_lit12.html` <daw-waveform
4311
+ (chPeaks, chIdx) => import_lit13.html` <daw-waveform
3774
4312
  style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
3775
4313
  .peaks=${chPeaks}
3776
4314
  .length=${peakData?.length ?? width}
@@ -3780,9 +4318,10 @@ var DawEditorElement = class extends import_lit12.LitElement {
3780
4318
  .visibleStart=${this._viewport.visibleStart}
3781
4319
  .visibleEnd=${this._viewport.visibleEnd}
3782
4320
  .originX=${clipLeft}
4321
+ .segments=${clipSegments}
3783
4322
  ></daw-waveform>`
3784
4323
  )}
3785
- ${this.interactiveClips ? import_lit12.html` <div
4324
+ ${this.interactiveClips ? import_lit13.html` <div
3786
4325
  class="clip-boundary"
3787
4326
  data-boundary-edge="left"
3788
4327
  data-clip-id=${clip.id}
@@ -3808,7 +4347,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3808
4347
  };
3809
4348
  DawEditorElement.styles = [
3810
4349
  hostStyles,
3811
- import_lit12.css`
4350
+ import_lit13.css`
3812
4351
  :host {
3813
4352
  display: flex;
3814
4353
  position: relative;
@@ -3838,6 +4377,15 @@ DawEditorElement.styles = [
3838
4377
  .track-row.selected {
3839
4378
  background: rgba(99, 199, 95, 0.08);
3840
4379
  }
4380
+ :host([scale-mode='beats']) .track-row {
4381
+ background: transparent;
4382
+ }
4383
+ :host([scale-mode='beats']) .clip-container {
4384
+ background: var(--daw-track-background, #16213e);
4385
+ }
4386
+ :host([scale-mode='beats']) .track-row.selected .clip-container {
4387
+ box-shadow: inset 0 0 0 1000px rgba(99, 199, 95, 0.06);
4388
+ }
3841
4389
  .timeline.drag-over {
3842
4390
  outline: 2px dashed var(--daw-selection-color, rgba(99, 199, 95, 0.3));
3843
4391
  outline-offset: -2px;
@@ -3847,69 +4395,96 @@ DawEditorElement.styles = [
3847
4395
  ];
3848
4396
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
3849
4397
  __decorateClass([
3850
- (0, import_decorators10.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
4398
+ (0, import_decorators11.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
3851
4399
  ], DawEditorElement.prototype, "samplesPerPixel", 1);
3852
4400
  __decorateClass([
3853
- (0, import_decorators10.property)({ type: Number, attribute: "wave-height" })
4401
+ (0, import_decorators11.property)({ type: Number, attribute: "wave-height" })
3854
4402
  ], DawEditorElement.prototype, "waveHeight", 2);
3855
4403
  __decorateClass([
3856
- (0, import_decorators10.property)({ type: Boolean })
4404
+ (0, import_decorators11.property)({ type: Boolean })
3857
4405
  ], DawEditorElement.prototype, "timescale", 2);
3858
4406
  __decorateClass([
3859
- (0, import_decorators10.property)({ type: Boolean })
4407
+ (0, import_decorators11.property)({ type: Boolean })
3860
4408
  ], DawEditorElement.prototype, "mono", 2);
3861
4409
  __decorateClass([
3862
- (0, import_decorators10.property)({ type: Number, attribute: "bar-width" })
4410
+ (0, import_decorators11.property)({ type: Number, attribute: "bar-width" })
3863
4411
  ], DawEditorElement.prototype, "barWidth", 2);
3864
4412
  __decorateClass([
3865
- (0, import_decorators10.property)({ type: Number, attribute: "bar-gap" })
4413
+ (0, import_decorators11.property)({ type: Number, attribute: "bar-gap" })
3866
4414
  ], DawEditorElement.prototype, "barGap", 2);
3867
4415
  __decorateClass([
3868
- (0, import_decorators10.property)({ type: Boolean, attribute: "file-drop" })
4416
+ (0, import_decorators11.property)({ type: Boolean, attribute: "file-drop" })
3869
4417
  ], DawEditorElement.prototype, "fileDrop", 2);
3870
4418
  __decorateClass([
3871
- (0, import_decorators10.property)({ type: Boolean, attribute: "clip-headers" })
4419
+ (0, import_decorators11.property)({ type: Boolean, attribute: "clip-headers" })
3872
4420
  ], DawEditorElement.prototype, "clipHeaders", 2);
3873
4421
  __decorateClass([
3874
- (0, import_decorators10.property)({ type: Number, attribute: "clip-header-height" })
4422
+ (0, import_decorators11.property)({ type: Number, attribute: "clip-header-height" })
3875
4423
  ], DawEditorElement.prototype, "clipHeaderHeight", 2);
3876
4424
  __decorateClass([
3877
- (0, import_decorators10.property)({ type: Boolean, attribute: "interactive-clips" })
4425
+ (0, import_decorators11.property)({ type: Boolean, attribute: "interactive-clips" })
3878
4426
  ], DawEditorElement.prototype, "interactiveClips", 2);
3879
4427
  __decorateClass([
3880
- (0, import_decorators10.property)({ type: Number, attribute: "sample-rate" })
4428
+ (0, import_decorators11.property)({ type: String, attribute: "scale-mode" })
4429
+ ], DawEditorElement.prototype, "scaleMode", 2);
4430
+ __decorateClass([
4431
+ (0, import_decorators11.property)({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
4432
+ ], DawEditorElement.prototype, "ticksPerPixel", 1);
4433
+ __decorateClass([
4434
+ (0, import_decorators11.property)({ type: Number, noAccessor: true })
4435
+ ], DawEditorElement.prototype, "bpm", 1);
4436
+ __decorateClass([
4437
+ (0, import_decorators11.property)({ attribute: false })
4438
+ ], DawEditorElement.prototype, "timeSignature", 2);
4439
+ __decorateClass([
4440
+ (0, import_decorators11.property)({ attribute: false })
4441
+ ], DawEditorElement.prototype, "meterEntries", 2);
4442
+ __decorateClass([
4443
+ (0, import_decorators11.property)({ type: Number, noAccessor: true })
4444
+ ], DawEditorElement.prototype, "ppqn", 1);
4445
+ __decorateClass([
4446
+ (0, import_decorators11.property)({ type: String, attribute: "snap-to" })
4447
+ ], DawEditorElement.prototype, "snapTo", 2);
4448
+ __decorateClass([
4449
+ (0, import_decorators11.property)({ attribute: false })
4450
+ ], DawEditorElement.prototype, "secondsToTicks", 2);
4451
+ __decorateClass([
4452
+ (0, import_decorators11.property)({ attribute: false })
4453
+ ], DawEditorElement.prototype, "ticksToSeconds", 2);
4454
+ __decorateClass([
4455
+ (0, import_decorators11.property)({ type: Number, attribute: "sample-rate" })
3881
4456
  ], DawEditorElement.prototype, "sampleRate", 2);
3882
4457
  __decorateClass([
3883
- (0, import_decorators10.state)()
4458
+ (0, import_decorators11.state)()
3884
4459
  ], DawEditorElement.prototype, "_tracks", 2);
3885
4460
  __decorateClass([
3886
- (0, import_decorators10.state)()
4461
+ (0, import_decorators11.state)()
3887
4462
  ], DawEditorElement.prototype, "_engineTracks", 2);
3888
4463
  __decorateClass([
3889
- (0, import_decorators10.state)()
4464
+ (0, import_decorators11.state)()
3890
4465
  ], DawEditorElement.prototype, "_peaksData", 2);
3891
4466
  __decorateClass([
3892
- (0, import_decorators10.state)()
4467
+ (0, import_decorators11.state)()
3893
4468
  ], DawEditorElement.prototype, "_isPlaying", 2);
3894
4469
  __decorateClass([
3895
- (0, import_decorators10.state)()
4470
+ (0, import_decorators11.state)()
3896
4471
  ], DawEditorElement.prototype, "_duration", 2);
3897
4472
  __decorateClass([
3898
- (0, import_decorators10.state)()
4473
+ (0, import_decorators11.state)()
3899
4474
  ], DawEditorElement.prototype, "_selectedTrackId", 2);
3900
4475
  __decorateClass([
3901
- (0, import_decorators10.state)()
4476
+ (0, import_decorators11.state)()
3902
4477
  ], DawEditorElement.prototype, "_dragOver", 2);
3903
4478
  __decorateClass([
3904
- (0, import_decorators10.property)({ attribute: "eager-resume" })
4479
+ (0, import_decorators11.property)({ attribute: "eager-resume" })
3905
4480
  ], DawEditorElement.prototype, "eagerResume", 2);
3906
4481
  DawEditorElement = __decorateClass([
3907
- (0, import_decorators10.customElement)("daw-editor")
4482
+ (0, import_decorators11.customElement)("daw-editor")
3908
4483
  ], DawEditorElement);
3909
4484
 
3910
4485
  // src/elements/daw-ruler.ts
3911
- var import_lit13 = require("lit");
3912
- var import_decorators11 = require("lit/decorators.js");
4486
+ var import_lit14 = require("lit");
4487
+ var import_decorators12 = require("lit/decorators.js");
3913
4488
 
3914
4489
  // src/utils/time-format.ts
3915
4490
  function formatTime(milliseconds) {
@@ -3960,18 +4535,36 @@ function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight
3960
4535
  }
3961
4536
 
3962
4537
  // src/elements/daw-ruler.ts
3963
- var MAX_CANVAS_WIDTH2 = 1e3;
3964
- var DawRulerElement = class extends import_lit13.LitElement {
4538
+ var MAX_CANVAS_WIDTH3 = 1e3;
4539
+ var DawRulerElement = class extends import_lit14.LitElement {
3965
4540
  constructor() {
3966
4541
  super(...arguments);
3967
4542
  this.samplesPerPixel = 1024;
3968
4543
  this.sampleRate = 48e3;
3969
4544
  this.duration = 0;
3970
4545
  this.rulerHeight = 30;
4546
+ this.scaleMode = "temporal";
4547
+ this.ticksPerPixel = 4;
4548
+ this.meterEntries = [
4549
+ { tick: 0, numerator: 4, denominator: 4 }
4550
+ ];
4551
+ this.ppqn = 960;
4552
+ this.totalWidth = 0;
3971
4553
  this._tickData = null;
4554
+ this._musicalTickData = null;
3972
4555
  }
3973
4556
  willUpdate() {
3974
- if (this.duration > 0) {
4557
+ if (this.scaleMode === "beats" && this.totalWidth > 0) {
4558
+ this._musicalTickData = getCachedMusicalTicks({
4559
+ meterEntries: this.meterEntries,
4560
+ ticksPerPixel: this.ticksPerPixel,
4561
+ startPixel: 0,
4562
+ endPixel: this.totalWidth,
4563
+ ppqn: this.ppqn
4564
+ });
4565
+ this._tickData = null;
4566
+ } else if (this.duration > 0) {
4567
+ this._musicalTickData = null;
3975
4568
  this._tickData = computeTemporalTicks(
3976
4569
  this.samplesPerPixel,
3977
4570
  this.sampleRate,
@@ -3979,30 +4572,39 @@ var DawRulerElement = class extends import_lit13.LitElement {
3979
4572
  this.rulerHeight
3980
4573
  );
3981
4574
  } else {
4575
+ this._musicalTickData = null;
3982
4576
  this._tickData = null;
3983
4577
  }
3984
4578
  }
3985
4579
  render() {
3986
- if (!this._tickData) return import_lit13.html``;
3987
- const { widthX, labels } = this._tickData;
3988
- const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH2);
4580
+ const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
4581
+ if (widthX <= 0) return import_lit14.html``;
4582
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH3);
3989
4583
  const indices = Array.from({ length: totalChunks }, (_, i) => i);
3990
4584
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
3991
- return import_lit13.html`
4585
+ const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
4586
+ const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
4587
+ return import_lit14.html`
3992
4588
  <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
3993
4589
  ${indices.map((i) => {
3994
- const width = Math.min(MAX_CANVAS_WIDTH2, widthX - i * MAX_CANVAS_WIDTH2);
3995
- return import_lit13.html`
4590
+ const width = Math.min(MAX_CANVAS_WIDTH3, widthX - i * MAX_CANVAS_WIDTH3);
4591
+ return import_lit14.html`
3996
4592
  <canvas
3997
4593
  data-index=${i}
3998
4594
  width=${width * dpr}
3999
4595
  height=${this.rulerHeight * dpr}
4000
- style="left: ${i * MAX_CANVAS_WIDTH2}px; width: ${width}px; height: ${this.rulerHeight}px;"
4596
+ style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.rulerHeight}px;"
4001
4597
  ></canvas>
4002
4598
  `;
4003
4599
  })}
4004
- ${labels.map(
4005
- ({ pix, text }) => import_lit13.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
4600
+ ${this.scaleMode === "beats" ? beatsLabels.map(
4601
+ (t) => import_lit14.html`<span
4602
+ class="label ${t.pixel > 0 ? "centered" : ""}"
4603
+ style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
4604
+ >${t.label}</span
4605
+ >`
4606
+ ) : temporalLabels.map(
4607
+ ({ pix, text }) => import_lit14.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
4006
4608
  )}
4007
4609
  </div>
4008
4610
  `;
@@ -4011,37 +4613,49 @@ var DawRulerElement = class extends import_lit13.LitElement {
4011
4613
  this._drawTicks();
4012
4614
  }
4013
4615
  _drawTicks() {
4014
- if (!this._tickData) return;
4015
4616
  const canvases = this.shadowRoot?.querySelectorAll("canvas");
4016
4617
  if (!canvases) return;
4017
4618
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
4018
4619
  const rulerColor = getComputedStyle(this).getPropertyValue("--daw-ruler-color").trim() || "#c49a6c";
4620
+ const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
4019
4621
  for (const canvas of canvases) {
4020
4622
  const idx = Number(canvas.dataset.index);
4021
4623
  const ctx = canvas.getContext("2d");
4022
4624
  if (!ctx) continue;
4023
- const canvasWidth = Math.min(
4024
- MAX_CANVAS_WIDTH2,
4025
- this._tickData.widthX - idx * MAX_CANVAS_WIDTH2
4026
- );
4027
- const globalOffset = idx * MAX_CANVAS_WIDTH2;
4625
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, widthX - idx * MAX_CANVAS_WIDTH3);
4626
+ const globalOffset = idx * MAX_CANVAS_WIDTH3;
4028
4627
  ctx.resetTransform();
4029
4628
  ctx.clearRect(0, 0, canvas.width, canvas.height);
4030
4629
  ctx.scale(dpr, dpr);
4031
4630
  ctx.strokeStyle = rulerColor;
4032
4631
  ctx.lineWidth = 1;
4033
- for (const [pix, height] of this._tickData.canvasInfo) {
4034
- const localX = pix - globalOffset;
4035
- if (localX < 0 || localX >= canvasWidth) continue;
4036
- ctx.beginPath();
4037
- ctx.moveTo(localX + 0.5, this.rulerHeight);
4038
- ctx.lineTo(localX + 0.5, this.rulerHeight - height);
4039
- ctx.stroke();
4632
+ if (this.scaleMode === "beats" && this._musicalTickData) {
4633
+ const h = this.rulerHeight;
4634
+ for (const tick of this._musicalTickData.ticks) {
4635
+ const localX = tick.pixel - globalOffset;
4636
+ if (localX < 0 || localX >= canvasWidth) continue;
4637
+ const tickH = tick.type === "major" ? h * 0.6 : tick.type === "minor" ? h * 0.35 : h * 0.15;
4638
+ ctx.globalAlpha = tick.type === "major" ? 1 : 0.5;
4639
+ ctx.beginPath();
4640
+ ctx.moveTo(localX + 0.5, h);
4641
+ ctx.lineTo(localX + 0.5, h - tickH);
4642
+ ctx.stroke();
4643
+ }
4644
+ ctx.globalAlpha = 1;
4645
+ } else if (this._tickData) {
4646
+ for (const [pix, height] of this._tickData.canvasInfo) {
4647
+ const localX = pix - globalOffset;
4648
+ if (localX < 0 || localX >= canvasWidth) continue;
4649
+ ctx.beginPath();
4650
+ ctx.moveTo(localX + 0.5, this.rulerHeight);
4651
+ ctx.lineTo(localX + 0.5, this.rulerHeight - height);
4652
+ ctx.stroke();
4653
+ }
4040
4654
  }
4041
4655
  }
4042
4656
  }
4043
4657
  };
4044
- DawRulerElement.styles = import_lit13.css`
4658
+ DawRulerElement.styles = import_lit14.css`
4045
4659
  :host {
4046
4660
  display: block;
4047
4661
  position: relative;
@@ -4057,31 +4671,50 @@ DawRulerElement.styles = import_lit13.css`
4057
4671
  .label {
4058
4672
  position: absolute;
4059
4673
  font-size: 0.7rem;
4674
+ line-height: 1;
4060
4675
  white-space: nowrap;
4061
4676
  color: var(--daw-ruler-color, #c49a6c);
4062
- top: 2px;
4677
+ top: 1px;
4678
+ }
4679
+ .label.centered {
4680
+ transform: translateX(-50%);
4063
4681
  }
4064
4682
  `;
4065
4683
  __decorateClass([
4066
- (0, import_decorators11.property)({ type: Number, attribute: false })
4684
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4067
4685
  ], DawRulerElement.prototype, "samplesPerPixel", 2);
4068
4686
  __decorateClass([
4069
- (0, import_decorators11.property)({ type: Number, attribute: false })
4687
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4070
4688
  ], DawRulerElement.prototype, "sampleRate", 2);
4071
4689
  __decorateClass([
4072
- (0, import_decorators11.property)({ type: Number, attribute: false })
4690
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4073
4691
  ], DawRulerElement.prototype, "duration", 2);
4074
4692
  __decorateClass([
4075
- (0, import_decorators11.property)({ type: Number, attribute: false })
4693
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4076
4694
  ], DawRulerElement.prototype, "rulerHeight", 2);
4695
+ __decorateClass([
4696
+ (0, import_decorators12.property)({ type: String, attribute: false })
4697
+ ], DawRulerElement.prototype, "scaleMode", 2);
4698
+ __decorateClass([
4699
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4700
+ ], DawRulerElement.prototype, "ticksPerPixel", 2);
4701
+ __decorateClass([
4702
+ (0, import_decorators12.property)({ attribute: false })
4703
+ ], DawRulerElement.prototype, "meterEntries", 2);
4704
+ __decorateClass([
4705
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4706
+ ], DawRulerElement.prototype, "ppqn", 2);
4707
+ __decorateClass([
4708
+ (0, import_decorators12.property)({ type: Number, attribute: false })
4709
+ ], DawRulerElement.prototype, "totalWidth", 2);
4077
4710
  DawRulerElement = __decorateClass([
4078
- (0, import_decorators11.customElement)("daw-ruler")
4711
+ (0, import_decorators12.customElement)("daw-ruler")
4079
4712
  ], DawRulerElement);
4080
4713
 
4081
4714
  // src/elements/daw-selection.ts
4082
- var import_lit14 = require("lit");
4083
- var import_decorators12 = require("lit/decorators.js");
4084
- var DawSelectionElement = class extends import_lit14.LitElement {
4715
+ var import_lit15 = require("lit");
4716
+ var import_decorators13 = require("lit/decorators.js");
4717
+ var DawSelectionElement = class extends import_lit15.LitElement {
4085
4718
  constructor() {
4086
4719
  super(...arguments);
4087
4720
  this.startPx = 0;
@@ -4090,11 +4723,11 @@ var DawSelectionElement = class extends import_lit14.LitElement {
4090
4723
  render() {
4091
4724
  const left = Math.min(this.startPx, this.endPx);
4092
4725
  const width = Math.abs(this.endPx - this.startPx);
4093
- if (width === 0) return import_lit14.html``;
4094
- return import_lit14.html`<div style="left: ${left}px; width: ${width}px;"></div>`;
4726
+ if (width === 0) return import_lit15.html``;
4727
+ return import_lit15.html`<div style="left: ${left}px; width: ${width}px;"></div>`;
4095
4728
  }
4096
4729
  };
4097
- DawSelectionElement.styles = import_lit14.css`
4730
+ DawSelectionElement.styles = import_lit15.css`
4098
4731
  :host {
4099
4732
  position: absolute;
4100
4733
  top: 0;
@@ -4111,18 +4744,18 @@ DawSelectionElement.styles = import_lit14.css`
4111
4744
  }
4112
4745
  `;
4113
4746
  __decorateClass([
4114
- (0, import_decorators12.property)({ type: Number, attribute: false })
4747
+ (0, import_decorators13.property)({ type: Number, attribute: false })
4115
4748
  ], DawSelectionElement.prototype, "startPx", 2);
4116
4749
  __decorateClass([
4117
- (0, import_decorators12.property)({ type: Number, attribute: false })
4750
+ (0, import_decorators13.property)({ type: Number, attribute: false })
4118
4751
  ], DawSelectionElement.prototype, "endPx", 2);
4119
4752
  DawSelectionElement = __decorateClass([
4120
- (0, import_decorators12.customElement)("daw-selection")
4753
+ (0, import_decorators13.customElement)("daw-selection")
4121
4754
  ], DawSelectionElement);
4122
4755
 
4123
4756
  // src/elements/daw-record-button.ts
4124
- var import_lit15 = require("lit");
4125
- var import_decorators13 = require("lit/decorators.js");
4757
+ var import_lit16 = require("lit");
4758
+ var import_decorators14 = require("lit/decorators.js");
4126
4759
  var DawRecordButtonElement = class extends DawTransportButton {
4127
4760
  constructor() {
4128
4761
  super(...arguments);
@@ -4163,7 +4796,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
4163
4796
  }
4164
4797
  }
4165
4798
  render() {
4166
- return import_lit15.html`
4799
+ return import_lit16.html`
4167
4800
  <button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
4168
4801
  <slot>Record</slot>
4169
4802
  </button>
@@ -4183,7 +4816,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
4183
4816
  };
4184
4817
  DawRecordButtonElement.styles = [
4185
4818
  DawTransportButton.styles,
4186
- import_lit15.css`
4819
+ import_lit16.css`
4187
4820
  button[data-recording] {
4188
4821
  color: #d08070;
4189
4822
  border-color: #d08070;
@@ -4192,17 +4825,17 @@ DawRecordButtonElement.styles = [
4192
4825
  `
4193
4826
  ];
4194
4827
  __decorateClass([
4195
- (0, import_decorators13.state)()
4828
+ (0, import_decorators14.state)()
4196
4829
  ], DawRecordButtonElement.prototype, "_isRecording", 2);
4197
4830
  DawRecordButtonElement = __decorateClass([
4198
- (0, import_decorators13.customElement)("daw-record-button")
4831
+ (0, import_decorators14.customElement)("daw-record-button")
4199
4832
  ], DawRecordButtonElement);
4200
4833
 
4201
4834
  // src/elements/daw-keyboard-shortcuts.ts
4202
- var import_lit16 = require("lit");
4203
- var import_decorators14 = require("lit/decorators.js");
4204
- var import_core5 = require("@waveform-playlist/core");
4205
- var DawKeyboardShortcutsElement = class extends import_lit16.LitElement {
4835
+ var import_lit17 = require("lit");
4836
+ var import_decorators15 = require("lit/decorators.js");
4837
+ var import_core9 = require("@waveform-playlist/core");
4838
+ var DawKeyboardShortcutsElement = class extends import_lit17.LitElement {
4206
4839
  constructor() {
4207
4840
  super(...arguments);
4208
4841
  this.playback = false;
@@ -4221,7 +4854,7 @@ var DawKeyboardShortcutsElement = class extends import_lit16.LitElement {
4221
4854
  const shortcuts = this.shortcuts;
4222
4855
  if (shortcuts.length === 0) return;
4223
4856
  try {
4224
- (0, import_core5.handleKeyboardEvent)(e, shortcuts, true);
4857
+ (0, import_core9.handleKeyboardEvent)(e, shortcuts, true);
4225
4858
  } catch (err) {
4226
4859
  console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
4227
4860
  const target = this._editor ?? this;
@@ -4356,16 +4989,16 @@ var DawKeyboardShortcutsElement = class extends import_lit16.LitElement {
4356
4989
  }
4357
4990
  };
4358
4991
  __decorateClass([
4359
- (0, import_decorators14.property)({ type: Boolean })
4992
+ (0, import_decorators15.property)({ type: Boolean })
4360
4993
  ], DawKeyboardShortcutsElement.prototype, "playback", 2);
4361
4994
  __decorateClass([
4362
- (0, import_decorators14.property)({ type: Boolean })
4995
+ (0, import_decorators15.property)({ type: Boolean })
4363
4996
  ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
4364
4997
  __decorateClass([
4365
- (0, import_decorators14.property)({ type: Boolean })
4998
+ (0, import_decorators15.property)({ type: Boolean })
4366
4999
  ], DawKeyboardShortcutsElement.prototype, "undo", 2);
4367
5000
  DawKeyboardShortcutsElement = __decorateClass([
4368
- (0, import_decorators14.customElement)("daw-keyboard-shortcuts")
5001
+ (0, import_decorators15.customElement)("daw-keyboard-shortcuts")
4369
5002
  ], DawKeyboardShortcutsElement);
4370
5003
  // Annotate the CommonJS export names for ESM import in node:
4371
5004
  0 && (module.exports = {
@@ -4373,6 +5006,7 @@ DawKeyboardShortcutsElement = __decorateClass([
4373
5006
  ClipPointerHandler,
4374
5007
  DawClipElement,
4375
5008
  DawEditorElement,
5009
+ DawGridElement,
4376
5010
  DawKeyboardShortcutsElement,
4377
5011
  DawPauseButtonElement,
4378
5012
  DawPlayButtonElement,