@dawcore/components 0.0.15 → 0.0.17

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
@@ -45,11 +45,13 @@ __export(index_exports, {
45
45
  DawGridElement: () => DawGridElement,
46
46
  DawKeyboardShortcutsElement: () => DawKeyboardShortcutsElement,
47
47
  DawPauseButtonElement: () => DawPauseButtonElement,
48
+ DawPianoRollElement: () => DawPianoRollElement,
48
49
  DawPlayButtonElement: () => DawPlayButtonElement,
49
50
  DawPlayheadElement: () => DawPlayheadElement,
50
51
  DawRecordButtonElement: () => DawRecordButtonElement,
51
52
  DawRulerElement: () => DawRulerElement,
52
53
  DawSelectionElement: () => DawSelectionElement,
54
+ DawSpectrogramElement: () => DawSpectrogramElement,
53
55
  DawStopButtonElement: () => DawStopButtonElement,
54
56
  DawTrackControlsElement: () => DawTrackControlsElement,
55
57
  DawTrackElement: () => DawTrackElement,
@@ -57,6 +59,7 @@ __export(index_exports, {
57
59
  DawTransportElement: () => DawTransportElement,
58
60
  DawWaveformElement: () => DawWaveformElement,
59
61
  RecordingController: () => RecordingController,
62
+ SpectrogramController: () => SpectrogramController,
60
63
  isDomClip: () => isDomClip,
61
64
  splitAtPlayhead: () => splitAtPlayhead
62
65
  });
@@ -79,11 +82,48 @@ var DawClipElement = class extends import_lit.LitElement {
79
82
  this.fadeIn = 0;
80
83
  this.fadeOut = 0;
81
84
  this.fadeType = "linear";
85
+ this.midiNotes = null;
86
+ this._midiChannel = null;
87
+ this._midiProgram = null;
82
88
  this.clipId = crypto.randomUUID();
83
89
  // Removal is detected by the editor's MutationObserver — detached elements
84
90
  // cannot bubble events to ancestors.
85
91
  this._hasRendered = false;
86
92
  }
93
+ get midiChannel() {
94
+ return this._midiChannel;
95
+ }
96
+ set midiChannel(value) {
97
+ const old = this._midiChannel;
98
+ if (value === null) {
99
+ this._midiChannel = null;
100
+ this.requestUpdate("midiChannel", old);
101
+ return;
102
+ }
103
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 15) {
104
+ console.warn("[dawcore] daw-clip midi-channel " + value + " is out of range 0-15 \u2014 ignored");
105
+ return;
106
+ }
107
+ this._midiChannel = value;
108
+ this.requestUpdate("midiChannel", old);
109
+ }
110
+ get midiProgram() {
111
+ return this._midiProgram;
112
+ }
113
+ set midiProgram(value) {
114
+ const old = this._midiProgram;
115
+ if (value === null) {
116
+ this._midiProgram = null;
117
+ this.requestUpdate("midiProgram", old);
118
+ return;
119
+ }
120
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 127) {
121
+ console.warn("[dawcore] daw-clip midi-program " + value + " is out of range 0-127 \u2014 ignored");
122
+ return;
123
+ }
124
+ this._midiProgram = value;
125
+ this.requestUpdate("midiProgram", old);
126
+ }
87
127
  // Light DOM — no visual rendering, just a data container
88
128
  createRenderRoot() {
89
129
  return this;
@@ -115,7 +155,10 @@ var DawClipElement = class extends import_lit.LitElement {
115
155
  "name",
116
156
  "fadeIn",
117
157
  "fadeOut",
118
- "fadeType"
158
+ "fadeType",
159
+ "midiNotes",
160
+ "midiChannel",
161
+ "midiProgram"
119
162
  ];
120
163
  if (clipProps.some((p) => changed.has(p))) {
121
164
  const trackEl = this.closest("daw-track");
@@ -162,6 +205,15 @@ __decorateClass([
162
205
  __decorateClass([
163
206
  (0, import_decorators.property)({ attribute: "fade-type" })
164
207
  ], DawClipElement.prototype, "fadeType", 2);
208
+ __decorateClass([
209
+ (0, import_decorators.property)({ attribute: false })
210
+ ], DawClipElement.prototype, "midiNotes", 2);
211
+ __decorateClass([
212
+ (0, import_decorators.property)({ type: Number, attribute: "midi-channel", noAccessor: true })
213
+ ], DawClipElement.prototype, "midiChannel", 1);
214
+ __decorateClass([
215
+ (0, import_decorators.property)({ type: Number, attribute: "midi-program", noAccessor: true })
216
+ ], DawClipElement.prototype, "midiProgram", 1);
165
217
  DawClipElement = __decorateClass([
166
218
  (0, import_decorators.customElement)("daw-clip")
167
219
  ], DawClipElement);
@@ -178,6 +230,8 @@ var DawTrackElement = class extends import_lit2.LitElement {
178
230
  this.pan = 0;
179
231
  this.muted = false;
180
232
  this.soloed = false;
233
+ this.renderMode = "waveform";
234
+ this.spectrogramConfig = null;
181
235
  this.trackId = crypto.randomUUID();
182
236
  // Track removal is detected by the editor's MutationObserver,
183
237
  // not by dispatching from disconnectedCallback (detached elements
@@ -205,7 +259,16 @@ var DawTrackElement = class extends import_lit2.LitElement {
205
259
  this._hasRendered = true;
206
260
  return;
207
261
  }
208
- const trackProps = ["volume", "pan", "muted", "soloed", "src", "name"];
262
+ const trackProps = [
263
+ "volume",
264
+ "pan",
265
+ "muted",
266
+ "soloed",
267
+ "src",
268
+ "name",
269
+ "renderMode",
270
+ "spectrogramConfig"
271
+ ];
209
272
  const hasTrackChange = trackProps.some((p) => changed.has(p));
210
273
  if (hasTrackChange) {
211
274
  this.dispatchEvent(
@@ -236,6 +299,12 @@ __decorateClass([
236
299
  __decorateClass([
237
300
  (0, import_decorators2.property)({ type: Boolean })
238
301
  ], DawTrackElement.prototype, "soloed", 2);
302
+ __decorateClass([
303
+ (0, import_decorators2.property)({ attribute: "render-mode" })
304
+ ], DawTrackElement.prototype, "renderMode", 2);
305
+ __decorateClass([
306
+ (0, import_decorators2.property)({ attribute: false })
307
+ ], DawTrackElement.prototype, "spectrogramConfig", 2);
239
308
  DawTrackElement = __decorateClass([
240
309
  (0, import_decorators2.customElement)("daw-track")
241
310
  ], DawTrackElement);
@@ -585,9 +654,229 @@ DawWaveformElement = __decorateClass([
585
654
  (0, import_decorators3.customElement)("daw-waveform")
586
655
  ], DawWaveformElement);
587
656
 
588
- // src/elements/daw-playhead.ts
657
+ // src/elements/daw-piano-roll.ts
589
658
  var import_lit4 = require("lit");
590
659
  var import_decorators4 = require("lit/decorators.js");
660
+ var MAX_CANVAS_WIDTH2 = 1e3;
661
+ var LAYOUT_PROPS2 = /* @__PURE__ */ new Set([
662
+ "length",
663
+ "waveHeight",
664
+ "samplesPerPixel",
665
+ "sampleRate",
666
+ "clipOffsetSeconds",
667
+ "midiNotes",
668
+ "selected"
669
+ ]);
670
+ var DawPianoRollElement = class extends import_lit4.LitElement {
671
+ constructor() {
672
+ super(...arguments);
673
+ this.midiNotes = [];
674
+ this.length = 0;
675
+ this.waveHeight = 128;
676
+ this._samplesPerPixel = 1024;
677
+ this._sampleRate = 48e3;
678
+ this.clipOffsetSeconds = 0;
679
+ this.visibleStart = -Infinity;
680
+ this.visibleEnd = Infinity;
681
+ this.originX = 0;
682
+ this.selected = false;
683
+ this._rafHandle = null;
684
+ }
685
+ get samplesPerPixel() {
686
+ return this._samplesPerPixel;
687
+ }
688
+ set samplesPerPixel(value) {
689
+ if (!Number.isFinite(value) || value <= 0) {
690
+ console.warn("[dawcore] daw-piano-roll samplesPerPixel " + value + " is invalid \u2014 ignored");
691
+ return;
692
+ }
693
+ const old = this._samplesPerPixel;
694
+ this._samplesPerPixel = value;
695
+ this.requestUpdate("samplesPerPixel", old);
696
+ }
697
+ get sampleRate() {
698
+ return this._sampleRate;
699
+ }
700
+ set sampleRate(value) {
701
+ if (!Number.isFinite(value) || value <= 0) {
702
+ console.warn("[dawcore] daw-piano-roll sampleRate " + value + " is invalid \u2014 ignored");
703
+ return;
704
+ }
705
+ const old = this._sampleRate;
706
+ this._sampleRate = value;
707
+ this.requestUpdate("sampleRate", old);
708
+ }
709
+ _scheduleDraw() {
710
+ if (this._rafHandle !== null) return;
711
+ this._rafHandle = requestAnimationFrame(() => {
712
+ this._rafHandle = null;
713
+ this._draw();
714
+ });
715
+ }
716
+ willUpdate(_changed) {
717
+ this._scheduleDraw();
718
+ }
719
+ updated(changedProperties) {
720
+ const needsFullDraw = [...changedProperties.keys()].some((key) => LAYOUT_PROPS2.has(key));
721
+ if (needsFullDraw) return;
722
+ if (changedProperties.has("visibleStart") || changedProperties.has("visibleEnd") || changedProperties.has("originX")) {
723
+ this._scheduleDraw();
724
+ }
725
+ }
726
+ _getPitchRange() {
727
+ if (this.midiNotes.length === 0) return { minMidi: 0, maxMidi: 127 };
728
+ let min = 127;
729
+ let max = 0;
730
+ for (const note of this.midiNotes) {
731
+ if (note.midi < min) min = note.midi;
732
+ if (note.midi > max) max = note.midi;
733
+ }
734
+ return {
735
+ minMidi: Math.max(0, min - 1),
736
+ maxMidi: Math.min(127, max + 1)
737
+ };
738
+ }
739
+ _getNoteColor() {
740
+ const cs = getComputedStyle(this);
741
+ const note = cs.getPropertyValue("--daw-piano-roll-note-color").trim() || "#2a7070";
742
+ const selectedColor = cs.getPropertyValue("--daw-piano-roll-selected-note-color").trim() || "#3d9e9e";
743
+ return this.selected ? selectedColor : note;
744
+ }
745
+ _draw() {
746
+ if (!this.shadowRoot) return;
747
+ const canvases = this.shadowRoot.querySelectorAll("canvas");
748
+ if (canvases.length === 0) return;
749
+ const { minMidi, maxMidi } = this._getPitchRange();
750
+ const noteRange = maxMidi - minMidi + 1;
751
+ const noteHeight = Math.max(2, this.waveHeight / noteRange);
752
+ const pixelsPerSecond = this.sampleRate / this.samplesPerPixel;
753
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
754
+ const color = this._getNoteColor();
755
+ for (const canvas of canvases) {
756
+ const chunkIdx = Number(canvas.dataset.index);
757
+ const chunkPixelStart = chunkIdx * MAX_CANVAS_WIDTH2;
758
+ const canvasWidth = canvas.width / dpr;
759
+ const ctx = canvas.getContext("2d");
760
+ if (!ctx) continue;
761
+ ctx.resetTransform();
762
+ ctx.clearRect(
763
+ 0,
764
+ 0,
765
+ canvas.width,
766
+ canvas.height
767
+ );
768
+ ctx.imageSmoothingEnabled = false;
769
+ ctx.scale(dpr, dpr);
770
+ const chunkStartTime = chunkPixelStart * this.samplesPerPixel / this.sampleRate;
771
+ const chunkEndTime = (chunkPixelStart + canvasWidth) * this.samplesPerPixel / this.sampleRate;
772
+ for (const note of this.midiNotes) {
773
+ const noteStart = note.time - this.clipOffsetSeconds;
774
+ const noteEnd = noteStart + note.duration;
775
+ if (noteEnd <= chunkStartTime || noteStart >= chunkEndTime) continue;
776
+ const x = noteStart * pixelsPerSecond - chunkPixelStart;
777
+ const w = Math.max(2, note.duration * pixelsPerSecond);
778
+ const y = (maxMidi - note.midi) / noteRange * this.waveHeight;
779
+ const alpha = 0.3 + note.velocity * 0.7;
780
+ ctx.fillStyle = color;
781
+ ctx.globalAlpha = alpha;
782
+ ctx.beginPath();
783
+ ctx.roundRect(x, y, w, noteHeight, 1);
784
+ ctx.fill();
785
+ }
786
+ ctx.globalAlpha = 1;
787
+ }
788
+ }
789
+ connectedCallback() {
790
+ super.connectedCallback();
791
+ this._scheduleDraw();
792
+ }
793
+ disconnectedCallback() {
794
+ super.disconnectedCallback();
795
+ if (this._rafHandle !== null) {
796
+ cancelAnimationFrame(this._rafHandle);
797
+ this._rafHandle = null;
798
+ }
799
+ }
800
+ render() {
801
+ if (this.length <= 0)
802
+ return import_lit4.html`<div class="container" style="width: 0; height: ${this.waveHeight}px;"></div>`;
803
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
804
+ const visibleIndices = getVisibleChunkIndices(
805
+ this.length,
806
+ MAX_CANVAS_WIDTH2,
807
+ this.visibleStart,
808
+ this.visibleEnd,
809
+ this.originX
810
+ );
811
+ return import_lit4.html`
812
+ <div class="container" style="width: ${this.length}px; height: ${this.waveHeight}px;">
813
+ ${visibleIndices.map((i) => {
814
+ const chunkLeft = i * MAX_CANVAS_WIDTH2;
815
+ const chunkWidth = Math.min(this.length - chunkLeft, MAX_CANVAS_WIDTH2);
816
+ return import_lit4.html`<canvas
817
+ data-index=${i}
818
+ width=${chunkWidth * dpr}
819
+ height=${this.waveHeight * dpr}
820
+ style="left: ${chunkLeft}px; width: ${chunkWidth}px; height: ${this.waveHeight}px;"
821
+ ></canvas>`;
822
+ })}
823
+ </div>
824
+ `;
825
+ }
826
+ };
827
+ DawPianoRollElement.styles = import_lit4.css`
828
+ :host {
829
+ display: block;
830
+ position: relative;
831
+ }
832
+ .container {
833
+ position: relative;
834
+ background: var(--daw-piano-roll-background, #1a1a2e);
835
+ }
836
+ canvas {
837
+ position: absolute;
838
+ top: 0;
839
+ image-rendering: pixelated;
840
+ image-rendering: crisp-edges;
841
+ }
842
+ `;
843
+ __decorateClass([
844
+ (0, import_decorators4.property)({ attribute: false })
845
+ ], DawPianoRollElement.prototype, "midiNotes", 2);
846
+ __decorateClass([
847
+ (0, import_decorators4.property)({ type: Number, attribute: false })
848
+ ], DawPianoRollElement.prototype, "length", 2);
849
+ __decorateClass([
850
+ (0, import_decorators4.property)({ type: Number, attribute: false })
851
+ ], DawPianoRollElement.prototype, "waveHeight", 2);
852
+ __decorateClass([
853
+ (0, import_decorators4.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
854
+ ], DawPianoRollElement.prototype, "samplesPerPixel", 1);
855
+ __decorateClass([
856
+ (0, import_decorators4.property)({ type: Number, attribute: "sample-rate", noAccessor: true })
857
+ ], DawPianoRollElement.prototype, "sampleRate", 1);
858
+ __decorateClass([
859
+ (0, import_decorators4.property)({ type: Number, attribute: false })
860
+ ], DawPianoRollElement.prototype, "clipOffsetSeconds", 2);
861
+ __decorateClass([
862
+ (0, import_decorators4.property)({ type: Number, attribute: false })
863
+ ], DawPianoRollElement.prototype, "visibleStart", 2);
864
+ __decorateClass([
865
+ (0, import_decorators4.property)({ type: Number, attribute: false })
866
+ ], DawPianoRollElement.prototype, "visibleEnd", 2);
867
+ __decorateClass([
868
+ (0, import_decorators4.property)({ type: Number, attribute: false })
869
+ ], DawPianoRollElement.prototype, "originX", 2);
870
+ __decorateClass([
871
+ (0, import_decorators4.property)({ type: Boolean, reflect: true })
872
+ ], DawPianoRollElement.prototype, "selected", 2);
873
+ DawPianoRollElement = __decorateClass([
874
+ (0, import_decorators4.customElement)("daw-piano-roll")
875
+ ], DawPianoRollElement);
876
+
877
+ // src/elements/daw-playhead.ts
878
+ var import_lit5 = require("lit");
879
+ var import_decorators5 = require("lit/decorators.js");
591
880
 
592
881
  // src/controllers/animation-controller.ts
593
882
  var AnimationController = class {
@@ -620,14 +909,14 @@ var AnimationController = class {
620
909
  };
621
910
 
622
911
  // src/elements/daw-playhead.ts
623
- var DawPlayheadElement = class extends import_lit4.LitElement {
912
+ var DawPlayheadElement = class extends import_lit5.LitElement {
624
913
  constructor() {
625
914
  super(...arguments);
626
915
  this._animation = new AnimationController(this);
627
916
  this._line = null;
628
917
  }
629
918
  render() {
630
- return import_lit4.html`<div></div>`;
919
+ return import_lit5.html`<div></div>`;
631
920
  }
632
921
  firstUpdated() {
633
922
  this._line = this.shadowRoot.querySelector("div");
@@ -683,7 +972,7 @@ var DawPlayheadElement = class extends import_lit4.LitElement {
683
972
  }
684
973
  }
685
974
  };
686
- DawPlayheadElement.styles = import_lit4.css`
975
+ DawPlayheadElement.styles = import_lit5.css`
687
976
  :host {
688
977
  position: absolute;
689
978
  top: 0;
@@ -702,13 +991,13 @@ DawPlayheadElement.styles = import_lit4.css`
702
991
  }
703
992
  `;
704
993
  DawPlayheadElement = __decorateClass([
705
- (0, import_decorators4.customElement)("daw-playhead")
994
+ (0, import_decorators5.customElement)("daw-playhead")
706
995
  ], DawPlayheadElement);
707
996
 
708
997
  // src/elements/daw-transport.ts
709
- var import_lit5 = require("lit");
710
- var import_decorators5 = require("lit/decorators.js");
711
- var DawTransportElement = class extends import_lit5.LitElement {
998
+ var import_lit6 = require("lit");
999
+ var import_decorators6 = require("lit/decorators.js");
1000
+ var DawTransportElement = class extends import_lit6.LitElement {
712
1001
  constructor() {
713
1002
  super(...arguments);
714
1003
  this.for = "";
@@ -723,25 +1012,25 @@ var DawTransportElement = class extends import_lit5.LitElement {
723
1012
  }
724
1013
  };
725
1014
  __decorateClass([
726
- (0, import_decorators5.property)()
1015
+ (0, import_decorators6.property)()
727
1016
  ], DawTransportElement.prototype, "for", 2);
728
1017
  DawTransportElement = __decorateClass([
729
- (0, import_decorators5.customElement)("daw-transport")
1018
+ (0, import_decorators6.customElement)("daw-transport")
730
1019
  ], DawTransportElement);
731
1020
 
732
1021
  // src/elements/daw-play-button.ts
733
- var import_lit7 = require("lit");
734
- var import_decorators6 = require("lit/decorators.js");
1022
+ var import_lit8 = require("lit");
1023
+ var import_decorators7 = require("lit/decorators.js");
735
1024
 
736
1025
  // src/elements/daw-transport-button.ts
737
- var import_lit6 = require("lit");
738
- var DawTransportButton = class extends import_lit6.LitElement {
1026
+ var import_lit7 = require("lit");
1027
+ var DawTransportButton = class extends import_lit7.LitElement {
739
1028
  get target() {
740
1029
  const transport = this.closest("daw-transport");
741
1030
  return transport?.target ?? null;
742
1031
  }
743
1032
  };
744
- DawTransportButton.styles = import_lit6.css`
1033
+ DawTransportButton.styles = import_lit7.css`
745
1034
  button {
746
1035
  cursor: pointer;
747
1036
  background: var(--daw-controls-background, #1a1a2e);
@@ -793,7 +1082,7 @@ var DawPlayButtonElement = class extends DawTransportButton {
793
1082
  }
794
1083
  }
795
1084
  render() {
796
- return import_lit7.html`
1085
+ return import_lit8.html`
797
1086
  <button part="button" ?disabled=${this._isRecording} @click=${this._onClick}>
798
1087
  <slot>Play</slot>
799
1088
  </button>
@@ -811,15 +1100,15 @@ var DawPlayButtonElement = class extends DawTransportButton {
811
1100
  }
812
1101
  };
813
1102
  __decorateClass([
814
- (0, import_decorators6.state)()
1103
+ (0, import_decorators7.state)()
815
1104
  ], DawPlayButtonElement.prototype, "_isRecording", 2);
816
1105
  DawPlayButtonElement = __decorateClass([
817
- (0, import_decorators6.customElement)("daw-play-button")
1106
+ (0, import_decorators7.customElement)("daw-play-button")
818
1107
  ], DawPlayButtonElement);
819
1108
 
820
1109
  // src/elements/daw-pause-button.ts
821
- var import_lit8 = require("lit");
822
- var import_decorators7 = require("lit/decorators.js");
1110
+ var import_lit9 = require("lit");
1111
+ var import_decorators8 = require("lit/decorators.js");
823
1112
  var DawPauseButtonElement = class extends DawTransportButton {
824
1113
  constructor() {
825
1114
  super(...arguments);
@@ -833,6 +1122,12 @@ var DawPauseButtonElement = class extends DawTransportButton {
833
1122
  this._isRecording = false;
834
1123
  this._isPaused = false;
835
1124
  };
1125
+ this._onRecPause = () => {
1126
+ this._isPaused = true;
1127
+ };
1128
+ this._onRecResume = () => {
1129
+ this._isPaused = false;
1130
+ };
836
1131
  }
837
1132
  connectedCallback() {
838
1133
  super.connectedCallback();
@@ -843,6 +1138,8 @@ var DawPauseButtonElement = class extends DawTransportButton {
843
1138
  target.addEventListener("daw-recording-start", this._onRecStart);
844
1139
  target.addEventListener("daw-recording-complete", this._onRecEnd);
845
1140
  target.addEventListener("daw-recording-error", this._onRecEnd);
1141
+ target.addEventListener("daw-recording-pause", this._onRecPause);
1142
+ target.addEventListener("daw-recording-resume", this._onRecResume);
846
1143
  });
847
1144
  }
848
1145
  disconnectedCallback() {
@@ -851,11 +1148,13 @@ var DawPauseButtonElement = class extends DawTransportButton {
851
1148
  this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
852
1149
  this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
853
1150
  this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
1151
+ this._targetRef.removeEventListener("daw-recording-pause", this._onRecPause);
1152
+ this._targetRef.removeEventListener("daw-recording-resume", this._onRecResume);
854
1153
  this._targetRef = null;
855
1154
  }
856
1155
  }
857
1156
  render() {
858
- return import_lit8.html`
1157
+ return import_lit9.html`
859
1158
  <button part="button" ?data-paused=${this._isPaused} @click=${this._onClick}>
860
1159
  <slot>Pause</slot>
861
1160
  </button>
@@ -870,15 +1169,7 @@ var DawPauseButtonElement = class extends DawTransportButton {
870
1169
  return;
871
1170
  }
872
1171
  if (this._isRecording) {
873
- if (this._isPaused) {
874
- target.resumeRecording();
875
- target.play(target.currentTime);
876
- this._isPaused = false;
877
- } else {
878
- target.pauseRecording();
879
- target.pause();
880
- this._isPaused = true;
881
- }
1172
+ target.togglePauseRecording();
882
1173
  } else {
883
1174
  target.pause();
884
1175
  }
@@ -886,7 +1177,7 @@ var DawPauseButtonElement = class extends DawTransportButton {
886
1177
  };
887
1178
  DawPauseButtonElement.styles = [
888
1179
  DawTransportButton.styles,
889
- import_lit8.css`
1180
+ import_lit9.css`
890
1181
  button[data-paused] {
891
1182
  background: rgba(255, 255, 255, 0.1);
892
1183
  border-color: var(--daw-controls-text, #e0d4c8);
@@ -894,21 +1185,21 @@ DawPauseButtonElement.styles = [
894
1185
  `
895
1186
  ];
896
1187
  __decorateClass([
897
- (0, import_decorators7.state)()
1188
+ (0, import_decorators8.state)()
898
1189
  ], DawPauseButtonElement.prototype, "_isPaused", 2);
899
1190
  __decorateClass([
900
- (0, import_decorators7.state)()
1191
+ (0, import_decorators8.state)()
901
1192
  ], DawPauseButtonElement.prototype, "_isRecording", 2);
902
1193
  DawPauseButtonElement = __decorateClass([
903
- (0, import_decorators7.customElement)("daw-pause-button")
1194
+ (0, import_decorators8.customElement)("daw-pause-button")
904
1195
  ], DawPauseButtonElement);
905
1196
 
906
1197
  // src/elements/daw-stop-button.ts
907
- var import_lit9 = require("lit");
908
- var import_decorators8 = require("lit/decorators.js");
1198
+ var import_lit10 = require("lit");
1199
+ var import_decorators9 = require("lit/decorators.js");
909
1200
  var DawStopButtonElement = class extends DawTransportButton {
910
1201
  render() {
911
- return import_lit9.html`
1202
+ return import_lit10.html`
912
1203
  <button part="button" @click=${this._onClick}>
913
1204
  <slot>Stop</slot>
914
1205
  </button>
@@ -923,18 +1214,31 @@ var DawStopButtonElement = class extends DawTransportButton {
923
1214
  return;
924
1215
  }
925
1216
  if (target.isRecording) {
926
- target.stopRecording();
1217
+ target.stopRecording().catch((err) => {
1218
+ console.warn("[dawcore] stopRecording failed: " + String(err));
1219
+ }).then(() => {
1220
+ try {
1221
+ target.stop();
1222
+ } catch (err) {
1223
+ console.warn("[dawcore] stop after stopRecording failed: " + String(err));
1224
+ }
1225
+ });
1226
+ } else {
1227
+ try {
1228
+ target.stop();
1229
+ } catch (err) {
1230
+ console.warn("[dawcore] stop failed: " + String(err));
1231
+ }
927
1232
  }
928
- target.stop();
929
1233
  }
930
1234
  };
931
1235
  DawStopButtonElement = __decorateClass([
932
- (0, import_decorators8.customElement)("daw-stop-button")
1236
+ (0, import_decorators9.customElement)("daw-stop-button")
933
1237
  ], DawStopButtonElement);
934
1238
 
935
1239
  // src/elements/daw-editor.ts
936
- var import_lit13 = require("lit");
937
- var import_decorators11 = require("lit/decorators.js");
1240
+ var import_lit14 = require("lit");
1241
+ var import_decorators12 = require("lit/decorators.js");
938
1242
 
939
1243
  // src/types.ts
940
1244
  function isDomClip(desc) {
@@ -1414,9 +1718,9 @@ var PeakPipeline = class {
1414
1718
  };
1415
1719
 
1416
1720
  // src/elements/daw-track-controls.ts
1417
- var import_lit10 = require("lit");
1418
- var import_decorators9 = require("lit/decorators.js");
1419
- var DawTrackControlsElement = class extends import_lit10.LitElement {
1721
+ var import_lit11 = require("lit");
1722
+ var import_decorators10 = require("lit/decorators.js");
1723
+ var DawTrackControlsElement = class extends import_lit11.LitElement {
1420
1724
  constructor() {
1421
1725
  super(...arguments);
1422
1726
  this.trackId = null;
@@ -1464,7 +1768,7 @@ var DawTrackControlsElement = class extends import_lit10.LitElement {
1464
1768
  const volPercent = Math.round(this.volume * 100);
1465
1769
  const panPercent = Math.round(Math.abs(this.pan) * 100);
1466
1770
  const panDisplay = this.pan === 0 ? "C" : (this.pan > 0 ? "R" : "L") + panPercent;
1467
- return import_lit10.html`
1771
+ return import_lit11.html`
1468
1772
  <div class="header">
1469
1773
  <span class="name" title=${this.trackName}>${this.trackName || "Untitled"}</span>
1470
1774
  <button class="remove-btn" @click=${this._onRemoveClick} title="Remove track">
@@ -1514,7 +1818,7 @@ var DawTrackControlsElement = class extends import_lit10.LitElement {
1514
1818
  `;
1515
1819
  }
1516
1820
  };
1517
- DawTrackControlsElement.styles = import_lit10.css`
1821
+ DawTrackControlsElement.styles = import_lit11.css`
1518
1822
  :host {
1519
1823
  display: flex;
1520
1824
  flex-direction: column;
@@ -1648,30 +1952,30 @@ DawTrackControlsElement.styles = import_lit10.css`
1648
1952
  }
1649
1953
  `;
1650
1954
  __decorateClass([
1651
- (0, import_decorators9.property)({ attribute: false })
1955
+ (0, import_decorators10.property)({ attribute: false })
1652
1956
  ], DawTrackControlsElement.prototype, "trackId", 2);
1653
1957
  __decorateClass([
1654
- (0, import_decorators9.property)({ attribute: false })
1958
+ (0, import_decorators10.property)({ attribute: false })
1655
1959
  ], DawTrackControlsElement.prototype, "trackName", 2);
1656
1960
  __decorateClass([
1657
- (0, import_decorators9.property)({ type: Number, attribute: false })
1961
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1658
1962
  ], DawTrackControlsElement.prototype, "volume", 2);
1659
1963
  __decorateClass([
1660
- (0, import_decorators9.property)({ type: Number, attribute: false })
1964
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1661
1965
  ], DawTrackControlsElement.prototype, "pan", 2);
1662
1966
  __decorateClass([
1663
- (0, import_decorators9.property)({ type: Boolean, attribute: false })
1967
+ (0, import_decorators10.property)({ type: Boolean, attribute: false })
1664
1968
  ], DawTrackControlsElement.prototype, "muted", 2);
1665
1969
  __decorateClass([
1666
- (0, import_decorators9.property)({ type: Boolean, attribute: false })
1970
+ (0, import_decorators10.property)({ type: Boolean, attribute: false })
1667
1971
  ], DawTrackControlsElement.prototype, "soloed", 2);
1668
1972
  DawTrackControlsElement = __decorateClass([
1669
- (0, import_decorators9.customElement)("daw-track-controls")
1973
+ (0, import_decorators10.customElement)("daw-track-controls")
1670
1974
  ], DawTrackControlsElement);
1671
1975
 
1672
1976
  // src/elements/daw-grid.ts
1673
- var import_lit11 = require("lit");
1674
- var import_decorators10 = require("lit/decorators.js");
1977
+ var import_lit12 = require("lit");
1978
+ var import_decorators11 = require("lit/decorators.js");
1675
1979
  var import_core2 = require("@waveform-playlist/core");
1676
1980
 
1677
1981
  // src/utils/musical-tick-cache.ts
@@ -1702,8 +2006,8 @@ function getCachedMusicalTicks(params) {
1702
2006
  }
1703
2007
 
1704
2008
  // src/elements/daw-grid.ts
1705
- var MAX_CANVAS_WIDTH2 = 1e3;
1706
- var DawGridElement = class extends import_lit11.LitElement {
2009
+ var MAX_CANVAS_WIDTH3 = 1e3;
2010
+ var DawGridElement = class extends import_lit12.LitElement {
1707
2011
  constructor() {
1708
2012
  super(...arguments);
1709
2013
  this.ticksPerPixel = 24;
@@ -1731,25 +2035,25 @@ var DawGridElement = class extends import_lit11.LitElement {
1731
2035
  }
1732
2036
  }
1733
2037
  render() {
1734
- if (!this._tickData) return import_lit11.html``;
2038
+ if (!this._tickData) return import_lit12.html``;
1735
2039
  const totalWidth = this.length;
1736
2040
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1737
2041
  const indices = getVisibleChunkIndices(
1738
2042
  totalWidth,
1739
- MAX_CANVAS_WIDTH2,
2043
+ MAX_CANVAS_WIDTH3,
1740
2044
  this.visibleStart,
1741
2045
  this.visibleEnd
1742
2046
  );
1743
- return import_lit11.html`
2047
+ return import_lit12.html`
1744
2048
  <div class="container" style="width: ${totalWidth}px; height: ${this.height}px;">
1745
2049
  ${indices.map((i) => {
1746
- const width = Math.min(MAX_CANVAS_WIDTH2, totalWidth - i * MAX_CANVAS_WIDTH2);
1747
- return import_lit11.html`
2050
+ const width = Math.min(MAX_CANVAS_WIDTH3, totalWidth - i * MAX_CANVAS_WIDTH3);
2051
+ return import_lit12.html`
1748
2052
  <canvas
1749
2053
  data-index=${i}
1750
2054
  width=${width * dpr}
1751
2055
  height=${this.height * dpr}
1752
- style="left: ${i * MAX_CANVAS_WIDTH2}px; width: ${width}px; height: ${this.height}px;"
2056
+ style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.height}px;"
1753
2057
  ></canvas>
1754
2058
  `;
1755
2059
  })}
@@ -1773,8 +2077,8 @@ var DawGridElement = class extends import_lit11.LitElement {
1773
2077
  const idx = Number(canvas.dataset.index);
1774
2078
  const ctx = canvas.getContext("2d");
1775
2079
  if (!ctx) continue;
1776
- const chunkLeft = idx * MAX_CANVAS_WIDTH2;
1777
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH2, this.length - chunkLeft);
2080
+ const chunkLeft = idx * MAX_CANVAS_WIDTH3;
2081
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, this.length - chunkLeft);
1778
2082
  ctx.resetTransform();
1779
2083
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1780
2084
  ctx.scale(dpr, dpr);
@@ -1805,7 +2109,7 @@ var DawGridElement = class extends import_lit11.LitElement {
1805
2109
  }
1806
2110
  }
1807
2111
  };
1808
- DawGridElement.styles = import_lit11.css`
2112
+ DawGridElement.styles = import_lit12.css`
1809
2113
  :host {
1810
2114
  display: block;
1811
2115
  position: absolute;
@@ -1823,33 +2127,33 @@ DawGridElement.styles = import_lit11.css`
1823
2127
  }
1824
2128
  `;
1825
2129
  __decorateClass([
1826
- (0, import_decorators10.property)({ type: Number, attribute: false })
2130
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1827
2131
  ], DawGridElement.prototype, "ticksPerPixel", 2);
1828
2132
  __decorateClass([
1829
- (0, import_decorators10.property)({ attribute: false })
2133
+ (0, import_decorators11.property)({ attribute: false })
1830
2134
  ], DawGridElement.prototype, "meterEntries", 2);
1831
2135
  __decorateClass([
1832
- (0, import_decorators10.property)({ type: Number, attribute: false })
2136
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1833
2137
  ], DawGridElement.prototype, "ppqn", 2);
1834
2138
  __decorateClass([
1835
- (0, import_decorators10.property)({ type: Number, attribute: false })
2139
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1836
2140
  ], DawGridElement.prototype, "visibleStart", 2);
1837
2141
  __decorateClass([
1838
- (0, import_decorators10.property)({ type: Number, attribute: false })
2142
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1839
2143
  ], DawGridElement.prototype, "visibleEnd", 2);
1840
2144
  __decorateClass([
1841
- (0, import_decorators10.property)({ type: Number, attribute: false })
2145
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1842
2146
  ], DawGridElement.prototype, "length", 2);
1843
2147
  __decorateClass([
1844
- (0, import_decorators10.property)({ type: Number, attribute: false })
2148
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1845
2149
  ], DawGridElement.prototype, "height", 2);
1846
2150
  DawGridElement = __decorateClass([
1847
- (0, import_decorators10.customElement)("daw-grid")
2151
+ (0, import_decorators11.customElement)("daw-grid")
1848
2152
  ], DawGridElement);
1849
2153
 
1850
2154
  // src/styles/theme.ts
1851
- var import_lit12 = require("lit");
1852
- var hostStyles = import_lit12.css`
2155
+ var import_lit13 = require("lit");
2156
+ var hostStyles = import_lit13.css`
1853
2157
  :host {
1854
2158
  --daw-wave-color: #c49a6c;
1855
2159
  --daw-progress-color: #63c75f;
@@ -1865,7 +2169,7 @@ var hostStyles = import_lit12.css`
1865
2169
  --daw-clip-header-text: #e0d4c8;
1866
2170
  }
1867
2171
  `;
1868
- var clipStyles = import_lit12.css`
2172
+ var clipStyles = import_lit13.css`
1869
2173
  .clip-container {
1870
2174
  position: absolute;
1871
2175
  overflow: hidden;
@@ -2110,6 +2414,9 @@ var RecordingController = class {
2110
2414
  constructor(host) {
2111
2415
  this._sessions = /* @__PURE__ */ new Map();
2112
2416
  this._workletLoadedCtx = null;
2417
+ /** Tracks worklet pause state explicitly so external consumers (editor,
2418
+ * pause button, spacebar) can share one source of truth. */
2419
+ this._isPaused = false;
2113
2420
  this._host = host;
2114
2421
  host.addController(this);
2115
2422
  }
@@ -2124,6 +2431,9 @@ var RecordingController = class {
2124
2431
  get isRecording() {
2125
2432
  return this._sessions.size > 0;
2126
2433
  }
2434
+ get isPaused() {
2435
+ return this._isPaused && this._sessions.size > 0;
2436
+ }
2127
2437
  getSession(trackId) {
2128
2438
  return this._sessions.get(trackId);
2129
2439
  }
@@ -2196,7 +2506,9 @@ var RecordingController = class {
2196
2506
  latencySamples,
2197
2507
  wasOverdub: options.overdub ?? false,
2198
2508
  _onTrackEnded: onTrackEnded,
2199
- _audioTrack: audioTrack
2509
+ _audioTrack: audioTrack,
2510
+ stopAckResolve: null,
2511
+ stopping: false
2200
2512
  };
2201
2513
  this._sessions.set(trackId, session);
2202
2514
  workletNode.port.onmessage = (e) => {
@@ -2234,25 +2546,69 @@ var RecordingController = class {
2234
2546
  const id = trackId ?? [...this._sessions.keys()][0];
2235
2547
  if (!id) return;
2236
2548
  const session = this._sessions.get(id);
2237
- if (!session) return;
2549
+ if (!session || session.stopping) return;
2238
2550
  session.workletNode.port.postMessage({ command: "pause" });
2551
+ this._isPaused = true;
2552
+ this._host.dispatchEvent(
2553
+ new CustomEvent("daw-recording-pause", {
2554
+ bubbles: true,
2555
+ composed: true,
2556
+ detail: { trackId: id }
2557
+ })
2558
+ );
2239
2559
  }
2240
2560
  resumeRecording(trackId) {
2241
2561
  const id = trackId ?? [...this._sessions.keys()][0];
2242
2562
  if (!id) return;
2243
2563
  const session = this._sessions.get(id);
2244
- if (!session) return;
2564
+ if (!session || session.stopping) return;
2245
2565
  session.workletNode.port.postMessage({ command: "resume" });
2566
+ this._isPaused = false;
2567
+ this._host.dispatchEvent(
2568
+ new CustomEvent("daw-recording-resume", {
2569
+ bubbles: true,
2570
+ composed: true,
2571
+ detail: { trackId: id }
2572
+ })
2573
+ );
2246
2574
  }
2247
- stopRecording(trackId) {
2575
+ async stopRecording(trackId) {
2248
2576
  const id = trackId ?? [...this._sessions.keys()][0];
2249
2577
  if (!id) return;
2250
2578
  const session = this._sessions.get(id);
2251
2579
  if (!session) return;
2580
+ const wasPaused = this._isPaused;
2581
+ this._isPaused = false;
2582
+ session.stopping = true;
2252
2583
  if (session.wasOverdub && typeof this._host.stop === "function") {
2253
2584
  this._host.stop();
2254
2585
  }
2255
- session.workletNode.port.postMessage({ command: "stop" });
2586
+ if (wasPaused) {
2587
+ session.workletNode.port.postMessage({ command: "stop" });
2588
+ } else {
2589
+ const stopAck = new Promise((resolve) => {
2590
+ session.stopAckResolve = resolve;
2591
+ });
2592
+ let timeoutId;
2593
+ const timeout = new Promise((resolve) => {
2594
+ timeoutId = setTimeout(resolve, 1e3);
2595
+ });
2596
+ session.workletNode.port.postMessage({ command: "stop" });
2597
+ await Promise.race([stopAck, timeout]);
2598
+ clearTimeout(timeoutId);
2599
+ session.stopAckResolve = null;
2600
+ let lastSamples = -1;
2601
+ let stable = 0;
2602
+ for (let i = 0; i < 50; i++) {
2603
+ if (session.totalSamples === lastSamples) {
2604
+ if (++stable >= 3) break;
2605
+ } else {
2606
+ stable = 0;
2607
+ lastSamples = session.totalSamples;
2608
+ }
2609
+ await new Promise((r) => setTimeout(r, 5));
2610
+ }
2611
+ }
2256
2612
  session.source.disconnect();
2257
2613
  session.workletNode.disconnect();
2258
2614
  this._removeTrackEndedListener(session);
@@ -2324,8 +2680,20 @@ var RecordingController = class {
2324
2680
  _onWorkletMessage(trackId, data) {
2325
2681
  const session = this._sessions.get(trackId);
2326
2682
  if (!session) return;
2327
- const { channels } = data;
2328
- if (!channels || channels.length === 0 || !channels[0]) return;
2683
+ const { channels, done } = data;
2684
+ try {
2685
+ const hasSamples = !!(channels && channels.length > 0 && channels[0] && channels[0].length > 0);
2686
+ if (!hasSamples) return;
2687
+ this._processWorkletSamples(trackId, session, channels);
2688
+ } finally {
2689
+ if (done && session.stopAckResolve) {
2690
+ const resolve = session.stopAckResolve;
2691
+ session.stopAckResolve = null;
2692
+ resolve();
2693
+ }
2694
+ }
2695
+ }
2696
+ _processWorkletSamples(trackId, session, channels) {
2329
2697
  const samplesProcessedBefore = session.totalSamples;
2330
2698
  for (let ch = 0; ch < session.channelCount; ch++) {
2331
2699
  if (channels[ch]) {
@@ -2333,6 +2701,7 @@ var RecordingController = class {
2333
2701
  }
2334
2702
  }
2335
2703
  session.totalSamples += channels[0].length;
2704
+ if (session.stopAckResolve !== null) return;
2336
2705
  for (let ch = 0; ch < session.channelCount; ch++) {
2337
2706
  if (!channels[ch]) continue;
2338
2707
  const oldPeakCount = Math.floor(session.peaks[ch].length / 2);
@@ -2403,6 +2772,121 @@ var RecordingController = class {
2403
2772
  }
2404
2773
  };
2405
2774
 
2775
+ // src/controllers/spectrogram-controller.ts
2776
+ var import_spectrogram = require("@dawcore/spectrogram");
2777
+ var LIBRARY_DEFAULTS = {
2778
+ fftSize: 2048,
2779
+ windowFunction: "hann",
2780
+ frequencyScale: "mel",
2781
+ minFrequency: 0,
2782
+ gainDb: 20,
2783
+ rangeDb: 80
2784
+ };
2785
+ var LIBRARY_DEFAULT_COLOR_MAP = "viridis";
2786
+ var SpectrogramController = class {
2787
+ constructor(host, workerFactory) {
2788
+ this.orchestrator = null;
2789
+ this.editorConfig = null;
2790
+ this.editorColorMap = null;
2791
+ this.trackConfigs = /* @__PURE__ */ new Map();
2792
+ this.trackColorMaps = /* @__PURE__ */ new Map();
2793
+ this.host = host;
2794
+ this.workerFactory = workerFactory;
2795
+ this.host.addController(this);
2796
+ }
2797
+ hostConnected() {
2798
+ }
2799
+ hostDisconnected() {
2800
+ this.dispose();
2801
+ }
2802
+ setEditorConfig(config) {
2803
+ this.editorConfig = config;
2804
+ this.reapply();
2805
+ }
2806
+ setEditorColorMap(colorMap) {
2807
+ this.editorColorMap = colorMap;
2808
+ this.reapply();
2809
+ }
2810
+ setTrackConfig(trackId, config) {
2811
+ if (config === null) {
2812
+ this.trackConfigs.delete(trackId);
2813
+ } else {
2814
+ this.trackConfigs.set(trackId, config);
2815
+ }
2816
+ this.reapply();
2817
+ }
2818
+ setTrackColorMap(trackId, colorMap) {
2819
+ if (colorMap === null) {
2820
+ this.trackColorMaps.delete(trackId);
2821
+ } else {
2822
+ this.trackColorMaps.set(trackId, colorMap);
2823
+ }
2824
+ this.reapply();
2825
+ }
2826
+ registerClipAudio(reg) {
2827
+ this.ensureOrchestrator().registerClip(reg);
2828
+ }
2829
+ unregisterClipAudio(clipId) {
2830
+ this.orchestrator?.unregisterClip(clipId);
2831
+ }
2832
+ registerCanvas(reg) {
2833
+ this.ensureOrchestrator().registerCanvas(reg);
2834
+ }
2835
+ unregisterCanvas(canvasId) {
2836
+ this.orchestrator?.unregisterCanvas(canvasId);
2837
+ }
2838
+ setViewport(state5) {
2839
+ this.orchestrator?.setViewport(state5);
2840
+ }
2841
+ dispose() {
2842
+ if (this.orchestrator) {
2843
+ this.orchestrator.dispose();
2844
+ this.orchestrator = null;
2845
+ }
2846
+ }
2847
+ ensureOrchestrator() {
2848
+ if (!this.orchestrator) {
2849
+ this.orchestrator = new import_spectrogram.SpectrogramOrchestrator({
2850
+ workerFactory: this.workerFactory,
2851
+ workerPoolSize: 2,
2852
+ config: this.mergedConfig(),
2853
+ colorMap: this.mergedColorMap()
2854
+ });
2855
+ this.orchestrator.addEventListener("viewport-ready", (e) => {
2856
+ const detail = e.detail;
2857
+ this.host.dispatchEvent(
2858
+ new CustomEvent("daw-spectrogram-ready", {
2859
+ detail,
2860
+ bubbles: true,
2861
+ composed: true
2862
+ })
2863
+ );
2864
+ });
2865
+ this.reapply();
2866
+ }
2867
+ return this.orchestrator;
2868
+ }
2869
+ reapply() {
2870
+ if (!this.orchestrator) return;
2871
+ this.orchestrator.setConfig(this.mergedConfig());
2872
+ this.orchestrator.setColorMap(this.mergedColorMap());
2873
+ }
2874
+ mergedConfig() {
2875
+ let track = null;
2876
+ for (const c of this.trackConfigs.values()) {
2877
+ track = c;
2878
+ break;
2879
+ }
2880
+ return { ...LIBRARY_DEFAULTS, ...this.editorConfig ?? {}, ...track ?? {} };
2881
+ }
2882
+ mergedColorMap() {
2883
+ for (const c of this.trackColorMaps.values()) {
2884
+ return c ?? LIBRARY_DEFAULT_COLOR_MAP;
2885
+ }
2886
+ return this.editorColorMap ?? LIBRARY_DEFAULT_COLOR_MAP;
2887
+ }
2888
+ };
2889
+
2406
2890
  // src/interactions/pointer-handler.ts
2407
2891
  var import_core4 = require("@waveform-playlist/core");
2408
2892
 
@@ -2667,6 +3151,7 @@ var ClipPointerHandler = class {
2667
3151
  const trackId = boundary.dataset.trackId;
2668
3152
  const edge = boundary.dataset.boundaryEdge;
2669
3153
  if (!clipId || !trackId || edge !== "left" && edge !== "right") return false;
3154
+ if (this._host.isMidiClip(trackId, clipId)) return true;
2670
3155
  this._beginDrag(edge === "left" ? "trim-left" : "trim-right", clipId, trackId, e);
2671
3156
  this._boundaryEl = boundary;
2672
3157
  return true;
@@ -2960,6 +3445,7 @@ async function loadFiles(host, files) {
2960
3445
  pan: 0,
2961
3446
  muted: false,
2962
3447
  soloed: false,
3448
+ renderMode: "waveform",
2963
3449
  clips: [
2964
3450
  {
2965
3451
  kind: "drop",
@@ -2972,7 +3458,10 @@ async function loadFiles(host, files) {
2972
3458
  name,
2973
3459
  fadeIn: 0,
2974
3460
  fadeOut: 0,
2975
- fadeType: "linear"
3461
+ fadeType: "linear",
3462
+ midiNotes: null,
3463
+ midiChannel: null,
3464
+ midiProgram: null
2976
3465
  }
2977
3466
  ]
2978
3467
  });
@@ -3059,7 +3548,10 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
3059
3548
  name: "Recording",
3060
3549
  fadeIn: 0,
3061
3550
  fadeOut: 0,
3062
- fadeType: "linear"
3551
+ fadeType: "linear",
3552
+ midiNotes: null,
3553
+ midiChannel: null,
3554
+ midiProgram: null
3063
3555
  };
3064
3556
  host._tracks = new Map(host._tracks).set(trackId, {
3065
3557
  ...desc,
@@ -3118,7 +3610,10 @@ function canSplitAtTime(host, time) {
3118
3610
  const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
3119
3611
  if (!track) return false;
3120
3612
  const atSample = Math.round(time * host.effectiveSampleRate);
3121
- return !!findClipAtSample(track.clips, atSample);
3613
+ const clip = findClipAtSample(track.clips, atSample);
3614
+ if (!clip) return false;
3615
+ if (clip.midiNotes != null) return false;
3616
+ return true;
3122
3617
  }
3123
3618
  function performSplit(host, time) {
3124
3619
  const { engine } = host;
@@ -3272,8 +3767,9 @@ async function loadWaveformDataFromUrl(src) {
3272
3767
  }
3273
3768
 
3274
3769
  // src/elements/daw-editor.ts
3770
+ var import_meta = {};
3275
3771
  var NO_ADAPTER_ERROR = "No PlayoutAdapter set on <daw-editor>. Set editor.adapter before use.\n\n // Option 1: Native Web Audio (no Tone.js)\n npm install @dawcore/transport\n import { NativePlayoutAdapter } from '@dawcore/transport';\n editor.adapter = new NativePlayoutAdapter(new AudioContext());\n\n // Option 2: Tone.js (effects, MIDI synths)\n npm install @waveform-playlist/playout\n import { createToneAdapter } from '@waveform-playlist/playout';\n editor.adapter = createToneAdapter();";
3276
- var DawEditorElement = class extends import_lit13.LitElement {
3772
+ var DawEditorElement = class extends import_lit14.LitElement {
3277
3773
  constructor() {
3278
3774
  super(...arguments);
3279
3775
  this._samplesPerPixel = 1024;
@@ -3287,6 +3783,8 @@ var DawEditorElement = class extends import_lit13.LitElement {
3287
3783
  this.clipHeaderHeight = 20;
3288
3784
  this.interactiveClips = false;
3289
3785
  this.indefinitePlayback = false;
3786
+ this._spectrogramConfig = null;
3787
+ this._spectrogramColorMap = null;
3290
3788
  this.scaleMode = "temporal";
3291
3789
  this._ticksPerPixel = 24;
3292
3790
  this._bpm = 120;
@@ -3322,6 +3820,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
3322
3820
  this._childObserver = null;
3323
3821
  this._audioResume = new AudioResumeController(this);
3324
3822
  this._recordingController = new RecordingController(this);
3823
+ this._spectrogramController = null;
3325
3824
  this._clipPointer = new ClipPointerHandler(this);
3326
3825
  this._pointer = new PointerHandler(this);
3327
3826
  this._viewport = (() => {
@@ -3362,6 +3861,26 @@ var DawEditorElement = class extends import_lit13.LitElement {
3362
3861
  if (oldDescriptor?.src !== descriptor.src) {
3363
3862
  this._loadTrack(trackId, descriptor);
3364
3863
  }
3864
+ if (descriptor.renderMode === "spectrogram" && oldDescriptor?.renderMode !== "spectrogram") {
3865
+ const engineTrack = this._engineTracks.get(trackId);
3866
+ if (engineTrack) {
3867
+ for (const clip of engineTrack.clips) {
3868
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
3869
+ }
3870
+ }
3871
+ }
3872
+ if (descriptor.renderMode !== "spectrogram" && oldDescriptor?.renderMode === "spectrogram") {
3873
+ const engineTrack = this._engineTracks.get(trackId);
3874
+ if (engineTrack && this._spectrogramController) {
3875
+ for (const clip of engineTrack.clips) {
3876
+ this._spectrogramController.unregisterClipAudio(clip.id);
3877
+ }
3878
+ }
3879
+ this._disposeSpectrogramControllerIfEmpty();
3880
+ }
3881
+ if (descriptor.spectrogramConfig !== oldDescriptor?.spectrogramConfig) {
3882
+ this._spectrogramController?.setTrackConfig(trackId, descriptor.spectrogramConfig ?? null);
3883
+ }
3365
3884
  };
3366
3885
  this._onTrackControl = (e) => {
3367
3886
  const { trackId, prop, value } = e.detail ?? {};
@@ -3432,7 +3951,10 @@ var DawEditorElement = class extends import_lit13.LitElement {
3432
3951
  name: clipEl.name,
3433
3952
  fadeIn: clipEl.fadeIn,
3434
3953
  fadeOut: clipEl.fadeOut,
3435
- fadeType: clipEl.fadeType
3954
+ fadeType: clipEl.fadeType,
3955
+ midiNotes: clipEl.midiNotes,
3956
+ midiChannel: clipEl.midiChannel,
3957
+ midiProgram: clipEl.midiProgram
3436
3958
  };
3437
3959
  this._loadAndAppendClip(trackId, clipDesc);
3438
3960
  };
@@ -3483,6 +4005,9 @@ var DawEditorElement = class extends import_lit13.LitElement {
3483
4005
  };
3484
4006
  // --- Recording ---
3485
4007
  this.recordingStream = null;
4008
+ /** Set in togglePauseRecording when Transport is paused alongside the
4009
+ * worklet, so resume can restart it. Cleared on resume and on stop. */
4010
+ this._wasPlayingDuringRecording = false;
3486
4011
  }
3487
4012
  get samplesPerPixel() {
3488
4013
  return this._samplesPerPixel;
@@ -3499,6 +4024,72 @@ var DawEditorElement = class extends import_lit13.LitElement {
3499
4024
  this._samplesPerPixel = clamped;
3500
4025
  this.requestUpdate("samplesPerPixel", old);
3501
4026
  }
4027
+ get spectrogramConfig() {
4028
+ return this._spectrogramConfig;
4029
+ }
4030
+ set spectrogramConfig(value) {
4031
+ const old = this._spectrogramConfig;
4032
+ this._spectrogramConfig = value;
4033
+ this._spectrogramController?.setEditorConfig(value);
4034
+ this.requestUpdate("spectrogramConfig", old);
4035
+ }
4036
+ get spectrogramColorMap() {
4037
+ return this._spectrogramColorMap;
4038
+ }
4039
+ set spectrogramColorMap(value) {
4040
+ const old = this._spectrogramColorMap;
4041
+ this._spectrogramColorMap = value;
4042
+ this._spectrogramController?.setEditorColorMap(value);
4043
+ this.requestUpdate("spectrogramColorMap", old);
4044
+ }
4045
+ _ensureSpectrogramController() {
4046
+ if (!this._spectrogramController) {
4047
+ this._spectrogramController = new SpectrogramController(
4048
+ this,
4049
+ () => new Worker(new URL("@dawcore/spectrogram/worker/spectrogram.worker", import_meta.url), {
4050
+ type: "module"
4051
+ })
4052
+ );
4053
+ if (this._spectrogramConfig) {
4054
+ this._spectrogramController.setEditorConfig(this._spectrogramConfig);
4055
+ }
4056
+ if (this._spectrogramColorMap) {
4057
+ this._spectrogramController.setEditorColorMap(this._spectrogramColorMap);
4058
+ }
4059
+ }
4060
+ return this._spectrogramController;
4061
+ }
4062
+ /** Called by <daw-spectrogram> after transferControlToOffscreen. */
4063
+ _spectrogramRegisterCanvas(reg) {
4064
+ this._ensureSpectrogramController().registerCanvas(reg);
4065
+ }
4066
+ /** Called by <daw-spectrogram> on chunk unmount / element disconnect. */
4067
+ _spectrogramUnregisterCanvas(canvasId) {
4068
+ this._spectrogramController?.unregisterCanvas(canvasId);
4069
+ }
4070
+ /**
4071
+ * Push a clip's decoded audio into the spectrogram controller. No-op
4072
+ * unless the track is in spectrogram render-mode and the controller
4073
+ * already exists (it bootstraps from canvas registration).
4074
+ */
4075
+ _maybeRegisterSpectrogramClipAudio(trackId, clip) {
4076
+ const descriptor = this._tracks.get(trackId);
4077
+ if (descriptor?.renderMode !== "spectrogram") return;
4078
+ const buffer = clip.audioBuffer ?? this._clipBuffers.get(clip.id);
4079
+ if (!buffer) return;
4080
+ const channelData = [];
4081
+ for (let i = 0; i < buffer.numberOfChannels; i++) {
4082
+ channelData.push(buffer.getChannelData(i));
4083
+ }
4084
+ this._ensureSpectrogramController().registerClipAudio({
4085
+ clipId: clip.id,
4086
+ trackId,
4087
+ channelData,
4088
+ sampleRate: buffer.sampleRate,
4089
+ durationSamples: clip.durationSamples,
4090
+ offsetSamples: clip.offsetSamples
4091
+ });
4092
+ }
3502
4093
  get ticksPerPixel() {
3503
4094
  return this._ticksPerPixel;
3504
4095
  }
@@ -3582,6 +4173,17 @@ var DawEditorElement = class extends import_lit13.LitElement {
3582
4173
  );
3583
4174
  return result.get(clipId) ?? null;
3584
4175
  }
4176
+ /**
4177
+ * Returns true if the clip is a MIDI clip (has midiNotes).
4178
+ * Used by ClipPointerHandler to make trim handles inert for MIDI clips.
4179
+ * Returns false for unknown track/clip IDs (defensive).
4180
+ */
4181
+ isMidiClip(trackId, clipId) {
4182
+ const track = this._engineTracks.get(trackId);
4183
+ if (!track) return false;
4184
+ const clip = track.clips.find((c) => c.id === clipId);
4185
+ return clip?.midiNotes != null;
4186
+ }
3585
4187
  get effectiveSampleRate() {
3586
4188
  return this._resolvedSampleRate ?? this.sampleRate;
3587
4189
  }
@@ -3728,6 +4330,8 @@ var DawEditorElement = class extends import_lit13.LitElement {
3728
4330
  this._clipOffsets.clear();
3729
4331
  this._peakPipeline.terminate();
3730
4332
  this._minSamplesPerPixel = 0;
4333
+ this._spectrogramController?.dispose();
4334
+ this._spectrogramController = null;
3731
4335
  try {
3732
4336
  this._disposeEngine();
3733
4337
  } catch (err) {
@@ -3756,6 +4360,23 @@ var DawEditorElement = class extends import_lit13.LitElement {
3756
4360
  }
3757
4361
  }
3758
4362
  }
4363
+ updated(_changed) {
4364
+ if (this._spectrogramController) {
4365
+ const vs = this._viewport.visibleStart;
4366
+ const ve = this._viewport.visibleEnd;
4367
+ if (Number.isFinite(vs) && Number.isFinite(ve)) {
4368
+ const span = ve - vs;
4369
+ const bufferPad = span * 0.25;
4370
+ this._spectrogramController.setViewport({
4371
+ visibleStartPx: vs,
4372
+ visibleEndPx: ve,
4373
+ bufferStartPx: Math.max(0, vs - bufferPad),
4374
+ bufferEndPx: ve + bufferPad,
4375
+ samplesPerPixel: this._renderSpp
4376
+ });
4377
+ }
4378
+ }
4379
+ }
3759
4380
  _onTrackRemoved(trackId) {
3760
4381
  this._trackElements.delete(trackId);
3761
4382
  const removedTrack = this._engineTracks.get(trackId);
@@ -3765,6 +4386,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
3765
4386
  this._clipBuffers.delete(clip.id);
3766
4387
  this._clipOffsets.delete(clip.id);
3767
4388
  nextPeaks.delete(clip.id);
4389
+ this._spectrogramController?.unregisterClipAudio(clip.id);
3768
4390
  }
3769
4391
  this._peaksData = nextPeaks;
3770
4392
  }
@@ -3779,11 +4401,23 @@ var DawEditorElement = class extends import_lit13.LitElement {
3779
4401
  this._engine.removeTrack(trackId);
3780
4402
  }
3781
4403
  this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
4404
+ this._disposeSpectrogramControllerIfEmpty();
3782
4405
  if (nextEngine.size === 0) {
3783
4406
  this._currentTime = 0;
3784
4407
  this._stopPlayhead();
3785
4408
  }
3786
4409
  }
4410
+ /** Drop the controller when no spectrogram tracks remain. */
4411
+ _disposeSpectrogramControllerIfEmpty() {
4412
+ if (!this._spectrogramController) return;
4413
+ const stillNeeded = Array.from(this._tracks.values()).some(
4414
+ (d) => d.renderMode === "spectrogram"
4415
+ );
4416
+ if (!stillNeeded) {
4417
+ this._spectrogramController.dispose();
4418
+ this._spectrogramController = null;
4419
+ }
4420
+ }
3787
4421
  _onClipRemovedFromDom(clipEl) {
3788
4422
  const clipId = clipEl.clipId;
3789
4423
  for (const [trackId, t] of this._engineTracks.entries()) {
@@ -3825,6 +4459,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
3825
4459
  });
3826
4460
  }
3827
4461
  this._commitTrackChange(trackId, updatedTrack);
4462
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
3828
4463
  this.dispatchEvent(
3829
4464
  new CustomEvent("daw-clip-ready", {
3830
4465
  bubbles: true,
@@ -3931,6 +4566,65 @@ var DawEditorElement = class extends import_lit13.LitElement {
3931
4566
  }
3932
4567
  return clip;
3933
4568
  }
4569
+ /**
4570
+ * Filter MIDI notes to only those with finite, in-range fields. Logs a
4571
+ * warning for each dropped note. Used by _buildMidiClip and the
4572
+ * _applyClipUpdate MIDI branch to prevent NaN propagation through the
4573
+ * timeline.
4574
+ */
4575
+ _validMidiNotes(notes) {
4576
+ return notes.filter((n) => {
4577
+ const ok = Number.isFinite(n.time) && n.time >= 0 && Number.isFinite(n.duration) && n.duration > 0 && Number.isInteger(n.midi) && n.midi >= 0 && n.midi <= 127 && Number.isFinite(n.velocity) && n.velocity >= 0 && n.velocity <= 1;
4578
+ if (!ok) {
4579
+ console.warn("[dawcore] dropping malformed MIDI note: " + JSON.stringify(n));
4580
+ }
4581
+ return ok;
4582
+ });
4583
+ }
4584
+ /**
4585
+ * A clip descriptor is treated as MIDI when it has no audio src.
4586
+ * Includes placeholder MIDI clips (no notes, no duration yet — registered
4587
+ * with a 1s default span; notes upgrade via _applyClipUpdate). Warns when
4588
+ * a clip ambiguously has both src and midiNotes — the audio path runs
4589
+ * and notes would be silently ignored.
4590
+ */
4591
+ _isMidiDescriptor(clipDesc) {
4592
+ if (clipDesc.src) {
4593
+ if (clipDesc.midiNotes != null) {
4594
+ console.warn(
4595
+ '[dawcore] clip "' + (clipDesc.name || (isDomClip(clipDesc) ? clipDesc.clipId : "?")) + '" has both src and midiNotes \u2014 treating as audio (notes will be ignored)'
4596
+ );
4597
+ }
4598
+ return false;
4599
+ }
4600
+ return true;
4601
+ }
4602
+ /**
4603
+ * Build an engine clip from a MIDI clip descriptor. Always returns a clip
4604
+ * — empty notes / no declared duration get a 1-second placeholder span so
4605
+ * the clip is reachable via `engine.updateTrack` once notes arrive.
4606
+ */
4607
+ _buildMidiClip(clipDesc) {
4608
+ const sr = this.effectiveSampleRate;
4609
+ const notes = this._validMidiNotes(clipDesc.midiNotes ?? []);
4610
+ const noteSpanSeconds = notes.length ? notes.reduce((max, n) => Math.max(max, n.time + n.duration), 0) : 0;
4611
+ const sourceDurationSamples = Math.ceil(Math.max(noteSpanSeconds, clipDesc.duration, 1) * sr);
4612
+ const requestedDurationSamples = clipDesc.duration > 0 ? Math.round(clipDesc.duration * sr) : sourceDurationSamples;
4613
+ const clip = (0, import_core8.createClip)({
4614
+ startSample: Math.round(clipDesc.start * sr),
4615
+ durationSamples: requestedDurationSamples,
4616
+ offsetSamples: Math.round(clipDesc.offset * sr),
4617
+ sampleRate: sr,
4618
+ sourceDurationSamples,
4619
+ gain: clipDesc.gain,
4620
+ name: clipDesc.name,
4621
+ midiNotes: notes,
4622
+ midiChannel: clipDesc.midiChannel ?? void 0,
4623
+ midiProgram: clipDesc.midiProgram ?? void 0
4624
+ });
4625
+ if (isDomClip(clipDesc)) clip.id = clipDesc.clipId;
4626
+ return clip;
4627
+ }
3934
4628
  /** Remove a single clip from all per-clip caches. Used by error rollbacks. */
3935
4629
  _purgeClipCaches(clipId) {
3936
4630
  const nextBuffers = new Map(this._clipBuffers);
@@ -3940,6 +4634,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
3940
4634
  nextPeaks.delete(clipId);
3941
4635
  this._peaksData = nextPeaks;
3942
4636
  this._clipOffsets.delete(clipId);
4637
+ this._spectrogramController?.unregisterClipAudio(clipId);
3943
4638
  }
3944
4639
  /**
3945
4640
  * Recompute duration and forward an updated track to the engine. Single
@@ -3968,6 +4663,34 @@ var DawEditorElement = class extends import_lit13.LitElement {
3968
4663
  }
3969
4664
  const oldClip = t.clips[idx];
3970
4665
  const sr = oldClip.sampleRate ?? this.effectiveSampleRate;
4666
+ const isMidiNow = clipEl.midiNotes != null;
4667
+ const wasMidi = oldClip.midiNotes != null;
4668
+ if (isMidiNow || wasMidi) {
4669
+ const notes = this._validMidiNotes(clipEl.midiNotes ?? []);
4670
+ const noteSpanSeconds = notes.length ? notes.reduce((max, n) => Math.max(max, n.time + n.duration), 0) : 0;
4671
+ const sourceDurationSamples = Math.ceil(Math.max(noteSpanSeconds, clipEl.duration, 1) * sr);
4672
+ const requestedDurationSamples = clipEl.duration > 0 ? Math.round(clipEl.duration * sr) : sourceDurationSamples;
4673
+ const updatedClip2 = {
4674
+ ...oldClip,
4675
+ audioBuffer: void 0,
4676
+ startSample: Math.round(clipEl.start * sr),
4677
+ offsetSamples: Math.round(clipEl.offset * sr),
4678
+ durationSamples: requestedDurationSamples,
4679
+ sourceDurationSamples,
4680
+ gain: clipEl.gain,
4681
+ name: clipEl.name || oldClip.name,
4682
+ midiNotes: notes,
4683
+ midiChannel: clipEl.midiChannel ?? void 0,
4684
+ midiProgram: clipEl.midiProgram ?? void 0
4685
+ };
4686
+ const updatedClips2 = [...t.clips];
4687
+ updatedClips2[idx] = updatedClip2;
4688
+ const updatedTrack2 = { ...t, clips: updatedClips2 };
4689
+ this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack2);
4690
+ this._purgeClipCaches(clipId);
4691
+ this._commitTrackChange(trackId, updatedTrack2);
4692
+ return;
4693
+ }
3971
4694
  const newStartSample = Math.round(clipEl.start * sr);
3972
4695
  const newDurationSamples = clipEl.duration > 0 ? Math.round(clipEl.duration * sr) : oldClip.durationSamples;
3973
4696
  const newOffsetSamples = Math.round(clipEl.offset * sr);
@@ -4044,7 +4767,10 @@ var DawEditorElement = class extends import_lit13.LitElement {
4044
4767
  name: trackEl.name || "",
4045
4768
  fadeIn: 0,
4046
4769
  fadeOut: 0,
4047
- fadeType: "linear"
4770
+ fadeType: "linear",
4771
+ midiNotes: null,
4772
+ midiChannel: null,
4773
+ midiProgram: null
4048
4774
  });
4049
4775
  } else {
4050
4776
  for (const clipEl of clipEls) {
@@ -4060,7 +4786,10 @@ var DawEditorElement = class extends import_lit13.LitElement {
4060
4786
  name: clipEl.name,
4061
4787
  fadeIn: clipEl.fadeIn,
4062
4788
  fadeOut: clipEl.fadeOut,
4063
- fadeType: clipEl.fadeType
4789
+ fadeType: clipEl.fadeType,
4790
+ midiNotes: clipEl.midiNotes,
4791
+ midiChannel: clipEl.midiChannel,
4792
+ midiProgram: clipEl.midiProgram
4064
4793
  });
4065
4794
  }
4066
4795
  }
@@ -4071,6 +4800,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
4071
4800
  pan: trackEl.pan,
4072
4801
  muted: trackEl.muted,
4073
4802
  soloed: trackEl.soloed,
4803
+ renderMode: trackEl.renderMode,
4074
4804
  clips
4075
4805
  };
4076
4806
  }
@@ -4079,7 +4809,10 @@ var DawEditorElement = class extends import_lit13.LitElement {
4079
4809
  try {
4080
4810
  const clips = [];
4081
4811
  for (const clipDesc of descriptor.clips) {
4082
- if (!clipDesc.src) continue;
4812
+ if (this._isMidiDescriptor(clipDesc)) {
4813
+ clips.push(this._buildMidiClip(clipDesc));
4814
+ continue;
4815
+ }
4083
4816
  try {
4084
4817
  const waveformDataPromise = clipDesc.peaksSrc ? this._resolvePeaks(clipDesc.peaksSrc) : Promise.resolve(null);
4085
4818
  const audioPromise = this._fetchAndDecode(clipDesc.src);
@@ -4184,6 +4917,9 @@ var DawEditorElement = class extends import_lit13.LitElement {
4184
4917
  track.id = trackId;
4185
4918
  this._engineTracks = new Map(this._engineTracks).set(trackId, track);
4186
4919
  this._recomputeDuration();
4920
+ for (const c of clips) {
4921
+ this._maybeRegisterSpectrogramClipAudio(trackId, c);
4922
+ }
4187
4923
  const engine = await this._ensureEngine();
4188
4924
  engine.setTracks([...this._engineTracks.values()]);
4189
4925
  this.dispatchEvent(
@@ -4291,7 +5027,11 @@ var DawEditorElement = class extends import_lit13.LitElement {
4291
5027
  nextTracks.set(track.id, track);
4292
5028
  }
4293
5029
  this._engineTracks = nextTracks;
4294
- syncPeaksForChangedClips(this, engineState.tracks);
5030
+ const audioTracks = engineState.tracks.filter((t) => {
5031
+ const desc = this._tracks.get(t.id);
5032
+ return desc?.renderMode !== "piano-roll";
5033
+ });
5034
+ syncPeaksForChangedClips(this, audioTracks);
4295
5035
  }
4296
5036
  });
4297
5037
  engine.on("pause", () => {
@@ -4366,7 +5106,17 @@ var DawEditorElement = class extends import_lit13.LitElement {
4366
5106
  if (config.pan !== void 0) trackEl.pan = config.pan;
4367
5107
  if (config.muted) trackEl.setAttribute("muted", "");
4368
5108
  if (config.soloed) trackEl.setAttribute("soloed", "");
4369
- for (const clipConfig of config.clips ?? []) {
5109
+ const renderMode = config.renderMode ?? (config.midi ? "piano-roll" : void 0);
5110
+ if (renderMode !== void 0) trackEl.setAttribute("render-mode", renderMode);
5111
+ const clipConfigs = [...config.clips ?? []];
5112
+ if (config.midi) {
5113
+ clipConfigs.push({
5114
+ midiNotes: config.midi.notes,
5115
+ midiChannel: config.midi.channel,
5116
+ midiProgram: config.midi.program
5117
+ });
5118
+ }
5119
+ for (const clipConfig of clipConfigs) {
4370
5120
  trackEl.appendChild(this._buildClipElement(clipConfig));
4371
5121
  }
4372
5122
  return this._awaitId(
@@ -4412,6 +5162,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
4412
5162
  if (partial.soloed) trackEl.setAttribute("soloed", "");
4413
5163
  else trackEl.removeAttribute("soloed");
4414
5164
  }
5165
+ if (partial.renderMode !== void 0) trackEl.setAttribute("render-mode", partial.renderMode);
4415
5166
  return;
4416
5167
  }
4417
5168
  const oldDesc = this._tracks.get(trackId);
@@ -4422,7 +5173,8 @@ var DawEditorElement = class extends import_lit13.LitElement {
4422
5173
  ...partial.volume !== void 0 && { volume: partial.volume },
4423
5174
  ...partial.pan !== void 0 && { pan: partial.pan },
4424
5175
  ...partial.muted !== void 0 && { muted: partial.muted },
4425
- ...partial.soloed !== void 0 && { soloed: partial.soloed }
5176
+ ...partial.soloed !== void 0 && { soloed: partial.soloed },
5177
+ ...partial.renderMode !== void 0 && { renderMode: partial.renderMode }
4426
5178
  };
4427
5179
  this._tracks = new Map(this._tracks).set(trackId, newDesc);
4428
5180
  if (this._engine) {
@@ -4557,6 +5309,11 @@ var DawEditorElement = class extends import_lit13.LitElement {
4557
5309
  if (config.fadeIn !== void 0) clipEl.fadeIn = config.fadeIn;
4558
5310
  if (config.fadeOut !== void 0) clipEl.fadeOut = config.fadeOut;
4559
5311
  if (config.fadeType !== void 0) clipEl.setAttribute("fade-type", config.fadeType);
5312
+ if (config.midiNotes !== void 0) clipEl.midiNotes = config.midiNotes;
5313
+ if (config.midiChannel !== void 0)
5314
+ clipEl.setAttribute("midi-channel", String(config.midiChannel));
5315
+ if (config.midiProgram !== void 0)
5316
+ clipEl.setAttribute("midi-program", String(config.midiProgram));
4560
5317
  return clipEl;
4561
5318
  }
4562
5319
  // --- Playback ---
@@ -4592,7 +5349,9 @@ var DawEditorElement = class extends import_lit13.LitElement {
4592
5349
  }
4593
5350
  /** Toggle between play and pause. */
4594
5351
  togglePlayPause() {
4595
- if (this._isPlaying) {
5352
+ if (this.isRecording) {
5353
+ this.togglePauseRecording();
5354
+ } else if (this._isPlaying) {
4596
5355
  this.pause();
4597
5356
  } else {
4598
5357
  this.play();
@@ -4666,14 +5425,41 @@ var DawEditorElement = class extends import_lit13.LitElement {
4666
5425
  get isRecording() {
4667
5426
  return this._recordingController.isRecording;
4668
5427
  }
5428
+ get isRecordingPaused() {
5429
+ return this._recordingController.isPaused;
5430
+ }
4669
5431
  pauseRecording() {
4670
5432
  this._recordingController.pauseRecording();
4671
5433
  }
4672
5434
  resumeRecording() {
4673
5435
  this._recordingController.resumeRecording();
5436
+ this._wasPlayingDuringRecording = false;
5437
+ }
5438
+ /**
5439
+ * Audacity-style pause toggle for active recordings: pauses both the
5440
+ * worklet capture and (if running) the playback Transport. On resume,
5441
+ * Transport restarts only if it was running before — non-overdub
5442
+ * recordings stay silent on resume.
5443
+ */
5444
+ togglePauseRecording() {
5445
+ if (!this.isRecording) return;
5446
+ if (this.isRecordingPaused) {
5447
+ const wasPlaying = this._wasPlayingDuringRecording;
5448
+ this.resumeRecording();
5449
+ if (wasPlaying) {
5450
+ void this.play(this.currentTime);
5451
+ }
5452
+ } else {
5453
+ this.pauseRecording();
5454
+ if (this._isPlaying) {
5455
+ this._wasPlayingDuringRecording = true;
5456
+ this.pause();
5457
+ }
5458
+ }
4674
5459
  }
4675
5460
  stopRecording() {
4676
- this._recordingController.stopRecording();
5461
+ this._wasPlayingDuringRecording = false;
5462
+ return this._recordingController.stopRecording();
4677
5463
  }
4678
5464
  _addRecordedClip(trackId, buf, startSample, durSamples, offsetSamples = 0) {
4679
5465
  addRecordedClip(this, trackId, buf, startSample, durSamples, offsetSamples);
@@ -4709,7 +5495,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
4709
5495
  const w = Math.floor(audibleSamples / renderSpp);
4710
5496
  return rs.peaks.map((chPeaks, ch) => {
4711
5497
  const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
4712
- return import_lit13.html`
5498
+ return import_lit14.html`
4713
5499
  <daw-waveform
4714
5500
  data-recording-track=${trackId}
4715
5501
  data-recording-channel=${ch}
@@ -4806,11 +5592,11 @@ var DawEditorElement = class extends import_lit13.LitElement {
4806
5592
  trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
4807
5593
  };
4808
5594
  });
4809
- return import_lit13.html`
4810
- ${orderedTracks.length > 0 || this.indefinitePlayback ? import_lit13.html`<div class="controls-column">
4811
- ${this.timescale ? import_lit13.html`<div style="height: 30px;"></div>` : ""}
5595
+ return import_lit14.html`
5596
+ ${orderedTracks.length > 0 || this.indefinitePlayback ? import_lit14.html`<div class="controls-column">
5597
+ ${this.timescale ? import_lit14.html`<div style="height: 30px;"></div>` : ""}
4812
5598
  ${orderedTracks.map(
4813
- (t) => import_lit13.html`
5599
+ (t) => import_lit14.html`
4814
5600
  <daw-track-controls
4815
5601
  style="height: ${t.trackHeight}px;"
4816
5602
  .trackId=${t.trackId}
@@ -4833,7 +5619,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
4833
5619
  @dragleave=${this._onDragLeave}
4834
5620
  @drop=${this._onDrop}
4835
5621
  >
4836
- ${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? import_lit13.html`<daw-ruler
5622
+ ${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? import_lit14.html`<daw-ruler
4837
5623
  .samplesPerPixel=${spp}
4838
5624
  .sampleRate=${this.effectiveSampleRate}
4839
5625
  .duration=${this._duration}
@@ -4843,7 +5629,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
4843
5629
  .ppqn=${this.ppqn}
4844
5630
  .totalWidth=${this._totalWidth}
4845
5631
  ></daw-ruler>` : ""}
4846
- ${this.scaleMode === "beats" ? import_lit13.html`<daw-grid
5632
+ ${this.scaleMode === "beats" ? import_lit14.html`<daw-grid
4847
5633
  style="top: ${this.timescale ? 30 : 0}px;"
4848
5634
  .ticksPerPixel=${this.ticksPerPixel}
4849
5635
  .meterEntries=${this._meterEntries}
@@ -4853,11 +5639,11 @@ var DawEditorElement = class extends import_lit13.LitElement {
4853
5639
  .length=${this._totalWidth}
4854
5640
  .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
4855
5641
  ></daw-grid>` : ""}
4856
- ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? import_lit13.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
5642
+ ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? import_lit14.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
4857
5643
  <daw-playhead></daw-playhead>` : ""}
4858
5644
  ${orderedTracks.map((t) => {
4859
5645
  const channelHeight = this.waveHeight;
4860
- return import_lit13.html`
5646
+ return import_lit14.html`
4861
5647
  <div
4862
5648
  class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
4863
5649
  style="height: ${t.trackHeight}px;"
@@ -4920,12 +5706,12 @@ var DawEditorElement = class extends import_lit13.LitElement {
4920
5706
  const channels = segmentChannels ?? peakData?.data ?? [new Int16Array(0)];
4921
5707
  const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
4922
5708
  const chH = this.waveHeight;
4923
- return import_lit13.html` <div
5709
+ return import_lit14.html` <div
4924
5710
  class="clip-container"
4925
5711
  style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
4926
5712
  data-clip-id=${clip.id}
4927
5713
  >
4928
- ${hdrH > 0 ? import_lit13.html`<div
5714
+ ${hdrH > 0 ? import_lit14.html`<div
4929
5715
  class="clip-header"
4930
5716
  data-clip-id=${clip.id}
4931
5717
  data-track-id=${t.trackId}
@@ -4933,21 +5719,48 @@ var DawEditorElement = class extends import_lit13.LitElement {
4933
5719
  >
4934
5720
  <span>${clip.name || t.descriptor?.name || ""}</span>
4935
5721
  </div>` : ""}
4936
- ${channels.map(
4937
- (chPeaks, chIdx) => import_lit13.html` <daw-waveform
4938
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
4939
- .peaks=${chPeaks}
5722
+ ${t.descriptor?.renderMode === "piano-roll" ? import_lit14.html`<daw-piano-roll
5723
+ style="position:absolute;left:0;top:${hdrH}px;"
5724
+ .midiNotes=${clip.midiNotes ?? []}
4940
5725
  .length=${peakData?.length ?? width}
4941
- .waveHeight=${chH}
4942
- .barWidth=${this.barWidth}
4943
- .barGap=${this.barGap}
5726
+ .waveHeight=${chH * channels.length}
5727
+ .samplesPerPixel=${this._renderSpp}
5728
+ .sampleRate=${this.effectiveSampleRate}
5729
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
4944
5730
  .visibleStart=${this._viewport.visibleStart}
4945
5731
  .visibleEnd=${this._viewport.visibleEnd}
4946
5732
  .originX=${clipLeft}
4947
- .segments=${clipSegments}
4948
- ></daw-waveform>`
5733
+ ?selected=${t.trackId === this._selectedTrackId}
5734
+ ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
5735
+ (_chPeaks, chIdx) => import_lit14.html`<daw-spectrogram
5736
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
5737
+ .clipId=${clip.id}
5738
+ .trackId=${t.trackId}
5739
+ .channelIndex=${chIdx}
5740
+ .length=${peakData?.length ?? width}
5741
+ .waveHeight=${chH}
5742
+ .samplesPerPixel=${this._renderSpp}
5743
+ .sampleRate=${this.effectiveSampleRate}
5744
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5745
+ .visibleStart=${this._viewport.visibleStart}
5746
+ .visibleEnd=${this._viewport.visibleEnd}
5747
+ .originX=${clipLeft}
5748
+ ></daw-spectrogram>`
5749
+ ) : channels.map(
5750
+ (chPeaks, chIdx) => import_lit14.html` <daw-waveform
5751
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5752
+ .peaks=${chPeaks}
5753
+ .length=${peakData?.length ?? width}
5754
+ .waveHeight=${chH}
5755
+ .barWidth=${this.barWidth}
5756
+ .barGap=${this.barGap}
5757
+ .visibleStart=${this._viewport.visibleStart}
5758
+ .visibleEnd=${this._viewport.visibleEnd}
5759
+ .originX=${clipLeft}
5760
+ .segments=${clipSegments}
5761
+ ></daw-waveform>`
4949
5762
  )}
4950
- ${this.interactiveClips ? import_lit13.html` <div
5763
+ ${this.interactiveClips ? import_lit14.html` <div
4951
5764
  class="clip-boundary"
4952
5765
  data-boundary-edge="left"
4953
5766
  data-clip-id=${clip.id}
@@ -4973,7 +5786,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
4973
5786
  };
4974
5787
  DawEditorElement.styles = [
4975
5788
  hostStyles,
4976
- import_lit13.css`
5789
+ import_lit14.css`
4977
5790
  :host {
4978
5791
  display: flex;
4979
5792
  position: relative;
@@ -5021,99 +5834,105 @@ DawEditorElement.styles = [
5021
5834
  ];
5022
5835
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
5023
5836
  __decorateClass([
5024
- (0, import_decorators11.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
5837
+ (0, import_decorators12.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
5025
5838
  ], DawEditorElement.prototype, "samplesPerPixel", 1);
5026
5839
  __decorateClass([
5027
- (0, import_decorators11.property)({ type: Number, attribute: "wave-height" })
5840
+ (0, import_decorators12.property)({ type: Number, attribute: "wave-height" })
5028
5841
  ], DawEditorElement.prototype, "waveHeight", 2);
5029
5842
  __decorateClass([
5030
- (0, import_decorators11.property)({ type: Boolean })
5843
+ (0, import_decorators12.property)({ type: Boolean })
5031
5844
  ], DawEditorElement.prototype, "timescale", 2);
5032
5845
  __decorateClass([
5033
- (0, import_decorators11.property)({ type: Boolean })
5846
+ (0, import_decorators12.property)({ type: Boolean })
5034
5847
  ], DawEditorElement.prototype, "mono", 2);
5035
5848
  __decorateClass([
5036
- (0, import_decorators11.property)({ type: Number, attribute: "bar-width" })
5849
+ (0, import_decorators12.property)({ type: Number, attribute: "bar-width" })
5037
5850
  ], DawEditorElement.prototype, "barWidth", 2);
5038
5851
  __decorateClass([
5039
- (0, import_decorators11.property)({ type: Number, attribute: "bar-gap" })
5852
+ (0, import_decorators12.property)({ type: Number, attribute: "bar-gap" })
5040
5853
  ], DawEditorElement.prototype, "barGap", 2);
5041
5854
  __decorateClass([
5042
- (0, import_decorators11.property)({ type: Boolean, attribute: "file-drop" })
5855
+ (0, import_decorators12.property)({ type: Boolean, attribute: "file-drop" })
5043
5856
  ], DawEditorElement.prototype, "fileDrop", 2);
5044
5857
  __decorateClass([
5045
- (0, import_decorators11.property)({ type: Boolean, attribute: "clip-headers" })
5858
+ (0, import_decorators12.property)({ type: Boolean, attribute: "clip-headers" })
5046
5859
  ], DawEditorElement.prototype, "clipHeaders", 2);
5047
5860
  __decorateClass([
5048
- (0, import_decorators11.property)({ type: Number, attribute: "clip-header-height" })
5861
+ (0, import_decorators12.property)({ type: Number, attribute: "clip-header-height" })
5049
5862
  ], DawEditorElement.prototype, "clipHeaderHeight", 2);
5050
5863
  __decorateClass([
5051
- (0, import_decorators11.property)({ type: Boolean, attribute: "interactive-clips" })
5864
+ (0, import_decorators12.property)({ type: Boolean, attribute: "interactive-clips" })
5052
5865
  ], DawEditorElement.prototype, "interactiveClips", 2);
5053
5866
  __decorateClass([
5054
- (0, import_decorators11.property)({ type: Boolean, attribute: "indefinite-playback" })
5867
+ (0, import_decorators12.property)({ type: Boolean, attribute: "indefinite-playback" })
5055
5868
  ], DawEditorElement.prototype, "indefinitePlayback", 2);
5056
5869
  __decorateClass([
5057
- (0, import_decorators11.property)({ type: String, attribute: "scale-mode" })
5870
+ (0, import_decorators12.property)({ attribute: false, noAccessor: true })
5871
+ ], DawEditorElement.prototype, "spectrogramConfig", 1);
5872
+ __decorateClass([
5873
+ (0, import_decorators12.property)({ attribute: false, noAccessor: true })
5874
+ ], DawEditorElement.prototype, "spectrogramColorMap", 1);
5875
+ __decorateClass([
5876
+ (0, import_decorators12.property)({ type: String, attribute: "scale-mode" })
5058
5877
  ], DawEditorElement.prototype, "scaleMode", 2);
5059
5878
  __decorateClass([
5060
- (0, import_decorators11.property)({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
5879
+ (0, import_decorators12.property)({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
5061
5880
  ], DawEditorElement.prototype, "ticksPerPixel", 1);
5062
5881
  __decorateClass([
5063
- (0, import_decorators11.property)({ type: Number, noAccessor: true })
5882
+ (0, import_decorators12.property)({ type: Number, noAccessor: true })
5064
5883
  ], DawEditorElement.prototype, "bpm", 1);
5065
5884
  __decorateClass([
5066
- (0, import_decorators11.property)({ attribute: false })
5885
+ (0, import_decorators12.property)({ attribute: false })
5067
5886
  ], DawEditorElement.prototype, "timeSignature", 2);
5068
5887
  __decorateClass([
5069
- (0, import_decorators11.property)({ attribute: false })
5888
+ (0, import_decorators12.property)({ attribute: false })
5070
5889
  ], DawEditorElement.prototype, "meterEntries", 2);
5071
5890
  __decorateClass([
5072
- (0, import_decorators11.property)({ type: Number, noAccessor: true })
5891
+ (0, import_decorators12.property)({ type: Number, noAccessor: true })
5073
5892
  ], DawEditorElement.prototype, "ppqn", 1);
5074
5893
  __decorateClass([
5075
- (0, import_decorators11.property)({ type: String, attribute: "snap-to" })
5894
+ (0, import_decorators12.property)({ type: String, attribute: "snap-to" })
5076
5895
  ], DawEditorElement.prototype, "snapTo", 2);
5077
5896
  __decorateClass([
5078
- (0, import_decorators11.property)({ attribute: false })
5897
+ (0, import_decorators12.property)({ attribute: false })
5079
5898
  ], DawEditorElement.prototype, "secondsToTicks", 2);
5080
5899
  __decorateClass([
5081
- (0, import_decorators11.property)({ attribute: false })
5900
+ (0, import_decorators12.property)({ attribute: false })
5082
5901
  ], DawEditorElement.prototype, "ticksToSeconds", 2);
5083
5902
  __decorateClass([
5084
- (0, import_decorators11.state)()
5903
+ (0, import_decorators12.state)()
5085
5904
  ], DawEditorElement.prototype, "_tracks", 2);
5086
5905
  __decorateClass([
5087
- (0, import_decorators11.state)()
5906
+ (0, import_decorators12.state)()
5088
5907
  ], DawEditorElement.prototype, "_engineTracks", 2);
5089
5908
  __decorateClass([
5090
- (0, import_decorators11.state)()
5909
+ (0, import_decorators12.state)()
5091
5910
  ], DawEditorElement.prototype, "_peaksData", 2);
5092
5911
  __decorateClass([
5093
- (0, import_decorators11.state)()
5912
+ (0, import_decorators12.state)()
5094
5913
  ], DawEditorElement.prototype, "_isPlaying", 2);
5095
5914
  __decorateClass([
5096
- (0, import_decorators11.state)()
5915
+ (0, import_decorators12.state)()
5097
5916
  ], DawEditorElement.prototype, "_duration", 2);
5098
5917
  __decorateClass([
5099
- (0, import_decorators11.state)()
5918
+ (0, import_decorators12.state)()
5100
5919
  ], DawEditorElement.prototype, "_selectedTrackId", 2);
5101
5920
  __decorateClass([
5102
- (0, import_decorators11.state)()
5921
+ (0, import_decorators12.state)()
5103
5922
  ], DawEditorElement.prototype, "_dragOver", 2);
5104
5923
  __decorateClass([
5105
- (0, import_decorators11.property)({ attribute: false })
5924
+ (0, import_decorators12.property)({ attribute: false })
5106
5925
  ], DawEditorElement.prototype, "adapter", 1);
5107
5926
  __decorateClass([
5108
- (0, import_decorators11.property)({ attribute: "eager-resume" })
5927
+ (0, import_decorators12.property)({ attribute: "eager-resume" })
5109
5928
  ], DawEditorElement.prototype, "eagerResume", 2);
5110
5929
  DawEditorElement = __decorateClass([
5111
- (0, import_decorators11.customElement)("daw-editor")
5930
+ (0, import_decorators12.customElement)("daw-editor")
5112
5931
  ], DawEditorElement);
5113
5932
 
5114
5933
  // src/elements/daw-ruler.ts
5115
- var import_lit14 = require("lit");
5116
- var import_decorators12 = require("lit/decorators.js");
5934
+ var import_lit15 = require("lit");
5935
+ var import_decorators13 = require("lit/decorators.js");
5117
5936
 
5118
5937
  // src/utils/time-format.ts
5119
5938
  function formatTime(milliseconds) {
@@ -5164,8 +5983,8 @@ function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight
5164
5983
  }
5165
5984
 
5166
5985
  // src/elements/daw-ruler.ts
5167
- var MAX_CANVAS_WIDTH3 = 1e3;
5168
- var DawRulerElement = class extends import_lit14.LitElement {
5986
+ var MAX_CANVAS_WIDTH4 = 1e3;
5987
+ var DawRulerElement = class extends import_lit15.LitElement {
5169
5988
  constructor() {
5170
5989
  super(...arguments);
5171
5990
  this.samplesPerPixel = 1024;
@@ -5209,33 +6028,33 @@ var DawRulerElement = class extends import_lit14.LitElement {
5209
6028
  }
5210
6029
  render() {
5211
6030
  const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
5212
- if (widthX <= 0) return import_lit14.html``;
5213
- const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH3);
6031
+ if (widthX <= 0) return import_lit15.html``;
6032
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH4);
5214
6033
  const indices = Array.from({ length: totalChunks }, (_, i) => i);
5215
6034
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
5216
6035
  const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
5217
6036
  const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
5218
- return import_lit14.html`
6037
+ return import_lit15.html`
5219
6038
  <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
5220
6039
  ${indices.map((i) => {
5221
- const width = Math.min(MAX_CANVAS_WIDTH3, widthX - i * MAX_CANVAS_WIDTH3);
5222
- return import_lit14.html`
6040
+ const width = Math.min(MAX_CANVAS_WIDTH4, widthX - i * MAX_CANVAS_WIDTH4);
6041
+ return import_lit15.html`
5223
6042
  <canvas
5224
6043
  data-index=${i}
5225
6044
  width=${width * dpr}
5226
6045
  height=${this.rulerHeight * dpr}
5227
- style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.rulerHeight}px;"
6046
+ style="left: ${i * MAX_CANVAS_WIDTH4}px; width: ${width}px; height: ${this.rulerHeight}px;"
5228
6047
  ></canvas>
5229
6048
  `;
5230
6049
  })}
5231
6050
  ${this.scaleMode === "beats" ? beatsLabels.map(
5232
- (t) => import_lit14.html`<span
6051
+ (t) => import_lit15.html`<span
5233
6052
  class="label ${t.pixel > 0 ? "centered" : ""}"
5234
6053
  style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
5235
6054
  >${t.label}</span
5236
6055
  >`
5237
6056
  ) : temporalLabels.map(
5238
- ({ pix, text }) => import_lit14.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
6057
+ ({ pix, text }) => import_lit15.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
5239
6058
  )}
5240
6059
  </div>
5241
6060
  `;
@@ -5253,8 +6072,8 @@ var DawRulerElement = class extends import_lit14.LitElement {
5253
6072
  const idx = Number(canvas.dataset.index);
5254
6073
  const ctx = canvas.getContext("2d");
5255
6074
  if (!ctx) continue;
5256
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, widthX - idx * MAX_CANVAS_WIDTH3);
5257
- const globalOffset = idx * MAX_CANVAS_WIDTH3;
6075
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH4, widthX - idx * MAX_CANVAS_WIDTH4);
6076
+ const globalOffset = idx * MAX_CANVAS_WIDTH4;
5258
6077
  ctx.resetTransform();
5259
6078
  ctx.clearRect(0, 0, canvas.width, canvas.height);
5260
6079
  ctx.scale(dpr, dpr);
@@ -5286,7 +6105,7 @@ var DawRulerElement = class extends import_lit14.LitElement {
5286
6105
  }
5287
6106
  }
5288
6107
  };
5289
- DawRulerElement.styles = import_lit14.css`
6108
+ DawRulerElement.styles = import_lit15.css`
5290
6109
  :host {
5291
6110
  display: block;
5292
6111
  position: relative;
@@ -5312,40 +6131,40 @@ DawRulerElement.styles = import_lit14.css`
5312
6131
  }
5313
6132
  `;
5314
6133
  __decorateClass([
5315
- (0, import_decorators12.property)({ type: Number, attribute: false })
6134
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5316
6135
  ], DawRulerElement.prototype, "samplesPerPixel", 2);
5317
6136
  __decorateClass([
5318
- (0, import_decorators12.property)({ type: Number, attribute: false })
6137
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5319
6138
  ], DawRulerElement.prototype, "sampleRate", 2);
5320
6139
  __decorateClass([
5321
- (0, import_decorators12.property)({ type: Number, attribute: false })
6140
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5322
6141
  ], DawRulerElement.prototype, "duration", 2);
5323
6142
  __decorateClass([
5324
- (0, import_decorators12.property)({ type: Number, attribute: false })
6143
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5325
6144
  ], DawRulerElement.prototype, "rulerHeight", 2);
5326
6145
  __decorateClass([
5327
- (0, import_decorators12.property)({ type: String, attribute: false })
6146
+ (0, import_decorators13.property)({ type: String, attribute: false })
5328
6147
  ], DawRulerElement.prototype, "scaleMode", 2);
5329
6148
  __decorateClass([
5330
- (0, import_decorators12.property)({ type: Number, attribute: false })
6149
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5331
6150
  ], DawRulerElement.prototype, "ticksPerPixel", 2);
5332
6151
  __decorateClass([
5333
- (0, import_decorators12.property)({ attribute: false })
6152
+ (0, import_decorators13.property)({ attribute: false })
5334
6153
  ], DawRulerElement.prototype, "meterEntries", 2);
5335
6154
  __decorateClass([
5336
- (0, import_decorators12.property)({ type: Number, attribute: false })
6155
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5337
6156
  ], DawRulerElement.prototype, "ppqn", 2);
5338
6157
  __decorateClass([
5339
- (0, import_decorators12.property)({ type: Number, attribute: false })
6158
+ (0, import_decorators13.property)({ type: Number, attribute: false })
5340
6159
  ], DawRulerElement.prototype, "totalWidth", 2);
5341
6160
  DawRulerElement = __decorateClass([
5342
- (0, import_decorators12.customElement)("daw-ruler")
6161
+ (0, import_decorators13.customElement)("daw-ruler")
5343
6162
  ], DawRulerElement);
5344
6163
 
5345
6164
  // src/elements/daw-selection.ts
5346
- var import_lit15 = require("lit");
5347
- var import_decorators13 = require("lit/decorators.js");
5348
- var DawSelectionElement = class extends import_lit15.LitElement {
6165
+ var import_lit16 = require("lit");
6166
+ var import_decorators14 = require("lit/decorators.js");
6167
+ var DawSelectionElement = class extends import_lit16.LitElement {
5349
6168
  constructor() {
5350
6169
  super(...arguments);
5351
6170
  this.startPx = 0;
@@ -5354,11 +6173,11 @@ var DawSelectionElement = class extends import_lit15.LitElement {
5354
6173
  render() {
5355
6174
  const left = Math.min(this.startPx, this.endPx);
5356
6175
  const width = Math.abs(this.endPx - this.startPx);
5357
- if (width === 0) return import_lit15.html``;
5358
- return import_lit15.html`<div style="left: ${left}px; width: ${width}px;"></div>`;
6176
+ if (width === 0) return import_lit16.html``;
6177
+ return import_lit16.html`<div style="left: ${left}px; width: ${width}px;"></div>`;
5359
6178
  }
5360
6179
  };
5361
- DawSelectionElement.styles = import_lit15.css`
6180
+ DawSelectionElement.styles = import_lit16.css`
5362
6181
  :host {
5363
6182
  position: absolute;
5364
6183
  top: 0;
@@ -5375,18 +6194,18 @@ DawSelectionElement.styles = import_lit15.css`
5375
6194
  }
5376
6195
  `;
5377
6196
  __decorateClass([
5378
- (0, import_decorators13.property)({ type: Number, attribute: false })
6197
+ (0, import_decorators14.property)({ type: Number, attribute: false })
5379
6198
  ], DawSelectionElement.prototype, "startPx", 2);
5380
6199
  __decorateClass([
5381
- (0, import_decorators13.property)({ type: Number, attribute: false })
6200
+ (0, import_decorators14.property)({ type: Number, attribute: false })
5382
6201
  ], DawSelectionElement.prototype, "endPx", 2);
5383
6202
  DawSelectionElement = __decorateClass([
5384
- (0, import_decorators13.customElement)("daw-selection")
6203
+ (0, import_decorators14.customElement)("daw-selection")
5385
6204
  ], DawSelectionElement);
5386
6205
 
5387
6206
  // src/elements/daw-record-button.ts
5388
- var import_lit16 = require("lit");
5389
- var import_decorators14 = require("lit/decorators.js");
6207
+ var import_lit17 = require("lit");
6208
+ var import_decorators15 = require("lit/decorators.js");
5390
6209
  var DawRecordButtonElement = class extends DawTransportButton {
5391
6210
  constructor() {
5392
6211
  super(...arguments);
@@ -5427,7 +6246,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
5427
6246
  }
5428
6247
  }
5429
6248
  render() {
5430
- return import_lit16.html`
6249
+ return import_lit17.html`
5431
6250
  <button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
5432
6251
  <slot>Record</slot>
5433
6252
  </button>
@@ -5447,7 +6266,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
5447
6266
  };
5448
6267
  DawRecordButtonElement.styles = [
5449
6268
  DawTransportButton.styles,
5450
- import_lit16.css`
6269
+ import_lit17.css`
5451
6270
  button[data-recording] {
5452
6271
  color: #d08070;
5453
6272
  border-color: #d08070;
@@ -5456,17 +6275,17 @@ DawRecordButtonElement.styles = [
5456
6275
  `
5457
6276
  ];
5458
6277
  __decorateClass([
5459
- (0, import_decorators14.state)()
6278
+ (0, import_decorators15.state)()
5460
6279
  ], DawRecordButtonElement.prototype, "_isRecording", 2);
5461
6280
  DawRecordButtonElement = __decorateClass([
5462
- (0, import_decorators14.customElement)("daw-record-button")
6281
+ (0, import_decorators15.customElement)("daw-record-button")
5463
6282
  ], DawRecordButtonElement);
5464
6283
 
5465
6284
  // src/elements/daw-keyboard-shortcuts.ts
5466
- var import_lit17 = require("lit");
5467
- var import_decorators15 = require("lit/decorators.js");
6285
+ var import_lit18 = require("lit");
6286
+ var import_decorators16 = require("lit/decorators.js");
5468
6287
  var import_core9 = require("@waveform-playlist/core");
5469
- var DawKeyboardShortcutsElement = class extends import_lit17.LitElement {
6288
+ var DawKeyboardShortcutsElement = class extends import_lit18.LitElement {
5470
6289
  constructor() {
5471
6290
  super(...arguments);
5472
6291
  this.playback = false;
@@ -5620,17 +6439,198 @@ var DawKeyboardShortcutsElement = class extends import_lit17.LitElement {
5620
6439
  }
5621
6440
  };
5622
6441
  __decorateClass([
5623
- (0, import_decorators15.property)({ type: Boolean })
6442
+ (0, import_decorators16.property)({ type: Boolean })
5624
6443
  ], DawKeyboardShortcutsElement.prototype, "playback", 2);
5625
6444
  __decorateClass([
5626
- (0, import_decorators15.property)({ type: Boolean })
6445
+ (0, import_decorators16.property)({ type: Boolean })
5627
6446
  ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
5628
6447
  __decorateClass([
5629
- (0, import_decorators15.property)({ type: Boolean })
6448
+ (0, import_decorators16.property)({ type: Boolean })
5630
6449
  ], DawKeyboardShortcutsElement.prototype, "undo", 2);
5631
6450
  DawKeyboardShortcutsElement = __decorateClass([
5632
- (0, import_decorators15.customElement)("daw-keyboard-shortcuts")
6451
+ (0, import_decorators16.customElement)("daw-keyboard-shortcuts")
5633
6452
  ], DawKeyboardShortcutsElement);
6453
+
6454
+ // src/elements/daw-spectrogram.ts
6455
+ var import_lit19 = require("lit");
6456
+ var import_decorators17 = require("lit/decorators.js");
6457
+ var MAX_CANVAS_WIDTH5 = 1e3;
6458
+ var DawSpectrogramElement = class extends import_lit19.LitElement {
6459
+ constructor() {
6460
+ super(...arguments);
6461
+ this.clipId = "";
6462
+ this.trackId = "";
6463
+ this.channelIndex = 0;
6464
+ this.length = 0;
6465
+ this.waveHeight = 128;
6466
+ this._samplesPerPixel = 1024;
6467
+ this._sampleRate = 44100;
6468
+ this.clipOffsetSeconds = 0;
6469
+ this.visibleStart = -Infinity;
6470
+ this.visibleEnd = Infinity;
6471
+ this.originX = 0;
6472
+ this._canvases = [];
6473
+ this._registeredCanvasIds = [];
6474
+ }
6475
+ get samplesPerPixel() {
6476
+ return this._samplesPerPixel;
6477
+ }
6478
+ set samplesPerPixel(value) {
6479
+ if (!Number.isFinite(value) || value <= 0) {
6480
+ console.warn("[dawcore] daw-spectrogram samplesPerPixel " + value + " is invalid \u2014 ignored");
6481
+ return;
6482
+ }
6483
+ const old = this._samplesPerPixel;
6484
+ this._samplesPerPixel = value;
6485
+ this.requestUpdate("samplesPerPixel", old);
6486
+ }
6487
+ get sampleRate() {
6488
+ return this._sampleRate;
6489
+ }
6490
+ set sampleRate(value) {
6491
+ if (!Number.isFinite(value) || value <= 0) {
6492
+ console.warn("[dawcore] daw-spectrogram sampleRate " + value + " is invalid \u2014 ignored");
6493
+ return;
6494
+ }
6495
+ const old = this._sampleRate;
6496
+ this._sampleRate = value;
6497
+ this.requestUpdate("sampleRate", old);
6498
+ }
6499
+ /**
6500
+ * Walk up to the editor host. `closest('daw-editor')` does NOT cross
6501
+ * shadow boundaries — and this element lives inside the editor's shadow
6502
+ * DOM — so use getRootNode().host to step out.
6503
+ */
6504
+ _findHostEditor() {
6505
+ const root = this.getRootNode();
6506
+ const host = root instanceof ShadowRoot ? root.host : null;
6507
+ if (!host) return null;
6508
+ if (host.tagName === "DAW-EDITOR") return host;
6509
+ return host.closest("daw-editor");
6510
+ }
6511
+ willUpdate(changed) {
6512
+ const layoutChanged = changed.has("length") || changed.has("waveHeight") || changed.has("samplesPerPixel") || changed.has("clipId") || changed.has("channelIndex");
6513
+ if (layoutChanged) {
6514
+ this._rebuildChunks();
6515
+ }
6516
+ }
6517
+ _rebuildChunks() {
6518
+ this._unregisterAllCanvases();
6519
+ this._canvases = [];
6520
+ if (this.length <= 0) return;
6521
+ const chunkCount = Math.ceil(this.length / MAX_CANVAS_WIDTH5);
6522
+ for (let i = 0; i < chunkCount; i++) {
6523
+ const widthPx = Math.min(MAX_CANVAS_WIDTH5, this.length - i * MAX_CANVAS_WIDTH5);
6524
+ const canvas = document.createElement("canvas");
6525
+ canvas.style.left = i * MAX_CANVAS_WIDTH5 + "px";
6526
+ canvas.style.width = widthPx + "px";
6527
+ const dpr = window.devicePixelRatio || 1;
6528
+ canvas.width = widthPx * dpr;
6529
+ canvas.height = this.waveHeight * dpr;
6530
+ this._canvases.push(canvas);
6531
+ }
6532
+ }
6533
+ updated(_changed) {
6534
+ if (this._registeredCanvasIds.length === 0 && this._canvases.length > 0) {
6535
+ requestAnimationFrame(() => this._registerCanvases());
6536
+ }
6537
+ }
6538
+ _registerCanvases() {
6539
+ const editor = this._findHostEditor();
6540
+ if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") return;
6541
+ for (let i = 0; i < this._canvases.length; i++) {
6542
+ const canvas = this._canvases[i];
6543
+ const canvasId = this.clipId + "-ch" + this.channelIndex + "-chunk" + i;
6544
+ let offscreen;
6545
+ try {
6546
+ offscreen = canvas.transferControlToOffscreen();
6547
+ } catch (err) {
6548
+ console.warn(
6549
+ "[dawcore] daw-spectrogram transferControlToOffscreen failed for " + canvasId + ": " + (err instanceof Error ? err.message : String(err))
6550
+ );
6551
+ continue;
6552
+ }
6553
+ editor._spectrogramRegisterCanvas({
6554
+ canvasId,
6555
+ canvas: offscreen,
6556
+ clipId: this.clipId,
6557
+ trackId: this.trackId,
6558
+ channelIndex: this.channelIndex,
6559
+ chunkIndex: i,
6560
+ globalPixelOffset: this.originX + i * MAX_CANVAS_WIDTH5,
6561
+ widthPx: parseFloat(canvas.style.width),
6562
+ heightPx: this.waveHeight
6563
+ });
6564
+ this._registeredCanvasIds.push(canvasId);
6565
+ }
6566
+ }
6567
+ _unregisterAllCanvases() {
6568
+ const editor = this._findHostEditor();
6569
+ if (editor && typeof editor._spectrogramUnregisterCanvas === "function") {
6570
+ for (const id of this._registeredCanvasIds) {
6571
+ editor._spectrogramUnregisterCanvas(id);
6572
+ }
6573
+ }
6574
+ this._registeredCanvasIds = [];
6575
+ }
6576
+ disconnectedCallback() {
6577
+ super.disconnectedCallback();
6578
+ this._unregisterAllCanvases();
6579
+ }
6580
+ render() {
6581
+ return import_lit19.html`${this._canvases.map((c) => c)}`;
6582
+ }
6583
+ };
6584
+ DawSpectrogramElement.styles = import_lit19.css`
6585
+ :host {
6586
+ display: block;
6587
+ position: relative;
6588
+ background: var(--daw-spectrogram-background, #000);
6589
+ }
6590
+ canvas {
6591
+ position: absolute;
6592
+ top: 0;
6593
+ left: 0;
6594
+ height: 100%;
6595
+ pointer-events: none;
6596
+ }
6597
+ `;
6598
+ __decorateClass([
6599
+ (0, import_decorators17.property)({ attribute: false })
6600
+ ], DawSpectrogramElement.prototype, "clipId", 2);
6601
+ __decorateClass([
6602
+ (0, import_decorators17.property)({ attribute: false })
6603
+ ], DawSpectrogramElement.prototype, "trackId", 2);
6604
+ __decorateClass([
6605
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6606
+ ], DawSpectrogramElement.prototype, "channelIndex", 2);
6607
+ __decorateClass([
6608
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6609
+ ], DawSpectrogramElement.prototype, "length", 2);
6610
+ __decorateClass([
6611
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6612
+ ], DawSpectrogramElement.prototype, "waveHeight", 2);
6613
+ __decorateClass([
6614
+ (0, import_decorators17.property)({ type: Number, attribute: false, noAccessor: true })
6615
+ ], DawSpectrogramElement.prototype, "samplesPerPixel", 1);
6616
+ __decorateClass([
6617
+ (0, import_decorators17.property)({ type: Number, attribute: false, noAccessor: true })
6618
+ ], DawSpectrogramElement.prototype, "sampleRate", 1);
6619
+ __decorateClass([
6620
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6621
+ ], DawSpectrogramElement.prototype, "clipOffsetSeconds", 2);
6622
+ __decorateClass([
6623
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6624
+ ], DawSpectrogramElement.prototype, "visibleStart", 2);
6625
+ __decorateClass([
6626
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6627
+ ], DawSpectrogramElement.prototype, "visibleEnd", 2);
6628
+ __decorateClass([
6629
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6630
+ ], DawSpectrogramElement.prototype, "originX", 2);
6631
+ DawSpectrogramElement = __decorateClass([
6632
+ (0, import_decorators17.customElement)("daw-spectrogram")
6633
+ ], DawSpectrogramElement);
5634
6634
  // Annotate the CommonJS export names for ESM import in node:
5635
6635
  0 && (module.exports = {
5636
6636
  AudioResumeController,
@@ -5640,11 +6640,13 @@ DawKeyboardShortcutsElement = __decorateClass([
5640
6640
  DawGridElement,
5641
6641
  DawKeyboardShortcutsElement,
5642
6642
  DawPauseButtonElement,
6643
+ DawPianoRollElement,
5643
6644
  DawPlayButtonElement,
5644
6645
  DawPlayheadElement,
5645
6646
  DawRecordButtonElement,
5646
6647
  DawRulerElement,
5647
6648
  DawSelectionElement,
6649
+ DawSpectrogramElement,
5648
6650
  DawStopButtonElement,
5649
6651
  DawTrackControlsElement,
5650
6652
  DawTrackElement,
@@ -5652,6 +6654,7 @@ DawKeyboardShortcutsElement = __decorateClass([
5652
6654
  DawTransportElement,
5653
6655
  DawWaveformElement,
5654
6656
  RecordingController,
6657
+ SpectrogramController,
5655
6658
  isDomClip,
5656
6659
  splitAtPlayhead
5657
6660
  });