@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.mjs CHANGED
@@ -26,11 +26,48 @@ var DawClipElement = class extends LitElement {
26
26
  this.fadeIn = 0;
27
27
  this.fadeOut = 0;
28
28
  this.fadeType = "linear";
29
+ this.midiNotes = null;
30
+ this._midiChannel = null;
31
+ this._midiProgram = null;
29
32
  this.clipId = crypto.randomUUID();
30
33
  // Removal is detected by the editor's MutationObserver — detached elements
31
34
  // cannot bubble events to ancestors.
32
35
  this._hasRendered = false;
33
36
  }
37
+ get midiChannel() {
38
+ return this._midiChannel;
39
+ }
40
+ set midiChannel(value) {
41
+ const old = this._midiChannel;
42
+ if (value === null) {
43
+ this._midiChannel = null;
44
+ this.requestUpdate("midiChannel", old);
45
+ return;
46
+ }
47
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 15) {
48
+ console.warn("[dawcore] daw-clip midi-channel " + value + " is out of range 0-15 \u2014 ignored");
49
+ return;
50
+ }
51
+ this._midiChannel = value;
52
+ this.requestUpdate("midiChannel", old);
53
+ }
54
+ get midiProgram() {
55
+ return this._midiProgram;
56
+ }
57
+ set midiProgram(value) {
58
+ const old = this._midiProgram;
59
+ if (value === null) {
60
+ this._midiProgram = null;
61
+ this.requestUpdate("midiProgram", old);
62
+ return;
63
+ }
64
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 127) {
65
+ console.warn("[dawcore] daw-clip midi-program " + value + " is out of range 0-127 \u2014 ignored");
66
+ return;
67
+ }
68
+ this._midiProgram = value;
69
+ this.requestUpdate("midiProgram", old);
70
+ }
34
71
  // Light DOM — no visual rendering, just a data container
35
72
  createRenderRoot() {
36
73
  return this;
@@ -62,7 +99,10 @@ var DawClipElement = class extends LitElement {
62
99
  "name",
63
100
  "fadeIn",
64
101
  "fadeOut",
65
- "fadeType"
102
+ "fadeType",
103
+ "midiNotes",
104
+ "midiChannel",
105
+ "midiProgram"
66
106
  ];
67
107
  if (clipProps.some((p) => changed.has(p))) {
68
108
  const trackEl = this.closest("daw-track");
@@ -109,6 +149,15 @@ __decorateClass([
109
149
  __decorateClass([
110
150
  property({ attribute: "fade-type" })
111
151
  ], DawClipElement.prototype, "fadeType", 2);
152
+ __decorateClass([
153
+ property({ attribute: false })
154
+ ], DawClipElement.prototype, "midiNotes", 2);
155
+ __decorateClass([
156
+ property({ type: Number, attribute: "midi-channel", noAccessor: true })
157
+ ], DawClipElement.prototype, "midiChannel", 1);
158
+ __decorateClass([
159
+ property({ type: Number, attribute: "midi-program", noAccessor: true })
160
+ ], DawClipElement.prototype, "midiProgram", 1);
112
161
  DawClipElement = __decorateClass([
113
162
  customElement("daw-clip")
114
163
  ], DawClipElement);
@@ -125,6 +174,8 @@ var DawTrackElement = class extends LitElement2 {
125
174
  this.pan = 0;
126
175
  this.muted = false;
127
176
  this.soloed = false;
177
+ this.renderMode = "waveform";
178
+ this.spectrogramConfig = null;
128
179
  this.trackId = crypto.randomUUID();
129
180
  // Track removal is detected by the editor's MutationObserver,
130
181
  // not by dispatching from disconnectedCallback (detached elements
@@ -152,7 +203,16 @@ var DawTrackElement = class extends LitElement2 {
152
203
  this._hasRendered = true;
153
204
  return;
154
205
  }
155
- const trackProps = ["volume", "pan", "muted", "soloed", "src", "name"];
206
+ const trackProps = [
207
+ "volume",
208
+ "pan",
209
+ "muted",
210
+ "soloed",
211
+ "src",
212
+ "name",
213
+ "renderMode",
214
+ "spectrogramConfig"
215
+ ];
156
216
  const hasTrackChange = trackProps.some((p) => changed.has(p));
157
217
  if (hasTrackChange) {
158
218
  this.dispatchEvent(
@@ -183,6 +243,12 @@ __decorateClass([
183
243
  __decorateClass([
184
244
  property2({ type: Boolean })
185
245
  ], DawTrackElement.prototype, "soloed", 2);
246
+ __decorateClass([
247
+ property2({ attribute: "render-mode" })
248
+ ], DawTrackElement.prototype, "renderMode", 2);
249
+ __decorateClass([
250
+ property2({ attribute: false })
251
+ ], DawTrackElement.prototype, "spectrogramConfig", 2);
186
252
  DawTrackElement = __decorateClass([
187
253
  customElement2("daw-track")
188
254
  ], DawTrackElement);
@@ -532,9 +598,229 @@ DawWaveformElement = __decorateClass([
532
598
  customElement3("daw-waveform")
533
599
  ], DawWaveformElement);
534
600
 
535
- // src/elements/daw-playhead.ts
601
+ // src/elements/daw-piano-roll.ts
536
602
  import { LitElement as LitElement4, html as html2, css as css2 } from "lit";
537
- import { customElement as customElement4 } from "lit/decorators.js";
603
+ import { customElement as customElement4, property as property4 } from "lit/decorators.js";
604
+ var MAX_CANVAS_WIDTH2 = 1e3;
605
+ var LAYOUT_PROPS2 = /* @__PURE__ */ new Set([
606
+ "length",
607
+ "waveHeight",
608
+ "samplesPerPixel",
609
+ "sampleRate",
610
+ "clipOffsetSeconds",
611
+ "midiNotes",
612
+ "selected"
613
+ ]);
614
+ var DawPianoRollElement = class extends LitElement4 {
615
+ constructor() {
616
+ super(...arguments);
617
+ this.midiNotes = [];
618
+ this.length = 0;
619
+ this.waveHeight = 128;
620
+ this._samplesPerPixel = 1024;
621
+ this._sampleRate = 48e3;
622
+ this.clipOffsetSeconds = 0;
623
+ this.visibleStart = -Infinity;
624
+ this.visibleEnd = Infinity;
625
+ this.originX = 0;
626
+ this.selected = false;
627
+ this._rafHandle = null;
628
+ }
629
+ get samplesPerPixel() {
630
+ return this._samplesPerPixel;
631
+ }
632
+ set samplesPerPixel(value) {
633
+ if (!Number.isFinite(value) || value <= 0) {
634
+ console.warn("[dawcore] daw-piano-roll samplesPerPixel " + value + " is invalid \u2014 ignored");
635
+ return;
636
+ }
637
+ const old = this._samplesPerPixel;
638
+ this._samplesPerPixel = value;
639
+ this.requestUpdate("samplesPerPixel", old);
640
+ }
641
+ get sampleRate() {
642
+ return this._sampleRate;
643
+ }
644
+ set sampleRate(value) {
645
+ if (!Number.isFinite(value) || value <= 0) {
646
+ console.warn("[dawcore] daw-piano-roll sampleRate " + value + " is invalid \u2014 ignored");
647
+ return;
648
+ }
649
+ const old = this._sampleRate;
650
+ this._sampleRate = value;
651
+ this.requestUpdate("sampleRate", old);
652
+ }
653
+ _scheduleDraw() {
654
+ if (this._rafHandle !== null) return;
655
+ this._rafHandle = requestAnimationFrame(() => {
656
+ this._rafHandle = null;
657
+ this._draw();
658
+ });
659
+ }
660
+ willUpdate(_changed) {
661
+ this._scheduleDraw();
662
+ }
663
+ updated(changedProperties) {
664
+ const needsFullDraw = [...changedProperties.keys()].some((key) => LAYOUT_PROPS2.has(key));
665
+ if (needsFullDraw) return;
666
+ if (changedProperties.has("visibleStart") || changedProperties.has("visibleEnd") || changedProperties.has("originX")) {
667
+ this._scheduleDraw();
668
+ }
669
+ }
670
+ _getPitchRange() {
671
+ if (this.midiNotes.length === 0) return { minMidi: 0, maxMidi: 127 };
672
+ let min = 127;
673
+ let max = 0;
674
+ for (const note of this.midiNotes) {
675
+ if (note.midi < min) min = note.midi;
676
+ if (note.midi > max) max = note.midi;
677
+ }
678
+ return {
679
+ minMidi: Math.max(0, min - 1),
680
+ maxMidi: Math.min(127, max + 1)
681
+ };
682
+ }
683
+ _getNoteColor() {
684
+ const cs = getComputedStyle(this);
685
+ const note = cs.getPropertyValue("--daw-piano-roll-note-color").trim() || "#2a7070";
686
+ const selectedColor = cs.getPropertyValue("--daw-piano-roll-selected-note-color").trim() || "#3d9e9e";
687
+ return this.selected ? selectedColor : note;
688
+ }
689
+ _draw() {
690
+ if (!this.shadowRoot) return;
691
+ const canvases = this.shadowRoot.querySelectorAll("canvas");
692
+ if (canvases.length === 0) return;
693
+ const { minMidi, maxMidi } = this._getPitchRange();
694
+ const noteRange = maxMidi - minMidi + 1;
695
+ const noteHeight = Math.max(2, this.waveHeight / noteRange);
696
+ const pixelsPerSecond = this.sampleRate / this.samplesPerPixel;
697
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
698
+ const color = this._getNoteColor();
699
+ for (const canvas of canvases) {
700
+ const chunkIdx = Number(canvas.dataset.index);
701
+ const chunkPixelStart = chunkIdx * MAX_CANVAS_WIDTH2;
702
+ const canvasWidth = canvas.width / dpr;
703
+ const ctx = canvas.getContext("2d");
704
+ if (!ctx) continue;
705
+ ctx.resetTransform();
706
+ ctx.clearRect(
707
+ 0,
708
+ 0,
709
+ canvas.width,
710
+ canvas.height
711
+ );
712
+ ctx.imageSmoothingEnabled = false;
713
+ ctx.scale(dpr, dpr);
714
+ const chunkStartTime = chunkPixelStart * this.samplesPerPixel / this.sampleRate;
715
+ const chunkEndTime = (chunkPixelStart + canvasWidth) * this.samplesPerPixel / this.sampleRate;
716
+ for (const note of this.midiNotes) {
717
+ const noteStart = note.time - this.clipOffsetSeconds;
718
+ const noteEnd = noteStart + note.duration;
719
+ if (noteEnd <= chunkStartTime || noteStart >= chunkEndTime) continue;
720
+ const x = noteStart * pixelsPerSecond - chunkPixelStart;
721
+ const w = Math.max(2, note.duration * pixelsPerSecond);
722
+ const y = (maxMidi - note.midi) / noteRange * this.waveHeight;
723
+ const alpha = 0.3 + note.velocity * 0.7;
724
+ ctx.fillStyle = color;
725
+ ctx.globalAlpha = alpha;
726
+ ctx.beginPath();
727
+ ctx.roundRect(x, y, w, noteHeight, 1);
728
+ ctx.fill();
729
+ }
730
+ ctx.globalAlpha = 1;
731
+ }
732
+ }
733
+ connectedCallback() {
734
+ super.connectedCallback();
735
+ this._scheduleDraw();
736
+ }
737
+ disconnectedCallback() {
738
+ super.disconnectedCallback();
739
+ if (this._rafHandle !== null) {
740
+ cancelAnimationFrame(this._rafHandle);
741
+ this._rafHandle = null;
742
+ }
743
+ }
744
+ render() {
745
+ if (this.length <= 0)
746
+ return html2`<div class="container" style="width: 0; height: ${this.waveHeight}px;"></div>`;
747
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
748
+ const visibleIndices = getVisibleChunkIndices(
749
+ this.length,
750
+ MAX_CANVAS_WIDTH2,
751
+ this.visibleStart,
752
+ this.visibleEnd,
753
+ this.originX
754
+ );
755
+ return html2`
756
+ <div class="container" style="width: ${this.length}px; height: ${this.waveHeight}px;">
757
+ ${visibleIndices.map((i) => {
758
+ const chunkLeft = i * MAX_CANVAS_WIDTH2;
759
+ const chunkWidth = Math.min(this.length - chunkLeft, MAX_CANVAS_WIDTH2);
760
+ return html2`<canvas
761
+ data-index=${i}
762
+ width=${chunkWidth * dpr}
763
+ height=${this.waveHeight * dpr}
764
+ style="left: ${chunkLeft}px; width: ${chunkWidth}px; height: ${this.waveHeight}px;"
765
+ ></canvas>`;
766
+ })}
767
+ </div>
768
+ `;
769
+ }
770
+ };
771
+ DawPianoRollElement.styles = css2`
772
+ :host {
773
+ display: block;
774
+ position: relative;
775
+ }
776
+ .container {
777
+ position: relative;
778
+ background: var(--daw-piano-roll-background, #1a1a2e);
779
+ }
780
+ canvas {
781
+ position: absolute;
782
+ top: 0;
783
+ image-rendering: pixelated;
784
+ image-rendering: crisp-edges;
785
+ }
786
+ `;
787
+ __decorateClass([
788
+ property4({ attribute: false })
789
+ ], DawPianoRollElement.prototype, "midiNotes", 2);
790
+ __decorateClass([
791
+ property4({ type: Number, attribute: false })
792
+ ], DawPianoRollElement.prototype, "length", 2);
793
+ __decorateClass([
794
+ property4({ type: Number, attribute: false })
795
+ ], DawPianoRollElement.prototype, "waveHeight", 2);
796
+ __decorateClass([
797
+ property4({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
798
+ ], DawPianoRollElement.prototype, "samplesPerPixel", 1);
799
+ __decorateClass([
800
+ property4({ type: Number, attribute: "sample-rate", noAccessor: true })
801
+ ], DawPianoRollElement.prototype, "sampleRate", 1);
802
+ __decorateClass([
803
+ property4({ type: Number, attribute: false })
804
+ ], DawPianoRollElement.prototype, "clipOffsetSeconds", 2);
805
+ __decorateClass([
806
+ property4({ type: Number, attribute: false })
807
+ ], DawPianoRollElement.prototype, "visibleStart", 2);
808
+ __decorateClass([
809
+ property4({ type: Number, attribute: false })
810
+ ], DawPianoRollElement.prototype, "visibleEnd", 2);
811
+ __decorateClass([
812
+ property4({ type: Number, attribute: false })
813
+ ], DawPianoRollElement.prototype, "originX", 2);
814
+ __decorateClass([
815
+ property4({ type: Boolean, reflect: true })
816
+ ], DawPianoRollElement.prototype, "selected", 2);
817
+ DawPianoRollElement = __decorateClass([
818
+ customElement4("daw-piano-roll")
819
+ ], DawPianoRollElement);
820
+
821
+ // src/elements/daw-playhead.ts
822
+ import { LitElement as LitElement5, html as html3, css as css3 } from "lit";
823
+ import { customElement as customElement5 } from "lit/decorators.js";
538
824
 
539
825
  // src/controllers/animation-controller.ts
540
826
  var AnimationController = class {
@@ -567,14 +853,14 @@ var AnimationController = class {
567
853
  };
568
854
 
569
855
  // src/elements/daw-playhead.ts
570
- var DawPlayheadElement = class extends LitElement4 {
856
+ var DawPlayheadElement = class extends LitElement5 {
571
857
  constructor() {
572
858
  super(...arguments);
573
859
  this._animation = new AnimationController(this);
574
860
  this._line = null;
575
861
  }
576
862
  render() {
577
- return html2`<div></div>`;
863
+ return html3`<div></div>`;
578
864
  }
579
865
  firstUpdated() {
580
866
  this._line = this.shadowRoot.querySelector("div");
@@ -630,7 +916,7 @@ var DawPlayheadElement = class extends LitElement4 {
630
916
  }
631
917
  }
632
918
  };
633
- DawPlayheadElement.styles = css2`
919
+ DawPlayheadElement.styles = css3`
634
920
  :host {
635
921
  position: absolute;
636
922
  top: 0;
@@ -649,13 +935,13 @@ DawPlayheadElement.styles = css2`
649
935
  }
650
936
  `;
651
937
  DawPlayheadElement = __decorateClass([
652
- customElement4("daw-playhead")
938
+ customElement5("daw-playhead")
653
939
  ], DawPlayheadElement);
654
940
 
655
941
  // src/elements/daw-transport.ts
656
- import { LitElement as LitElement5 } from "lit";
657
- import { customElement as customElement5, property as property4 } from "lit/decorators.js";
658
- var DawTransportElement = class extends LitElement5 {
942
+ import { LitElement as LitElement6 } from "lit";
943
+ import { customElement as customElement6, property as property5 } from "lit/decorators.js";
944
+ var DawTransportElement = class extends LitElement6 {
659
945
  constructor() {
660
946
  super(...arguments);
661
947
  this.for = "";
@@ -670,25 +956,25 @@ var DawTransportElement = class extends LitElement5 {
670
956
  }
671
957
  };
672
958
  __decorateClass([
673
- property4()
959
+ property5()
674
960
  ], DawTransportElement.prototype, "for", 2);
675
961
  DawTransportElement = __decorateClass([
676
- customElement5("daw-transport")
962
+ customElement6("daw-transport")
677
963
  ], DawTransportElement);
678
964
 
679
965
  // src/elements/daw-play-button.ts
680
- import { html as html3 } from "lit";
681
- import { customElement as customElement6, state } from "lit/decorators.js";
966
+ import { html as html4 } from "lit";
967
+ import { customElement as customElement7, state } from "lit/decorators.js";
682
968
 
683
969
  // src/elements/daw-transport-button.ts
684
- import { LitElement as LitElement6, css as css3 } from "lit";
685
- var DawTransportButton = class extends LitElement6 {
970
+ import { LitElement as LitElement7, css as css4 } from "lit";
971
+ var DawTransportButton = class extends LitElement7 {
686
972
  get target() {
687
973
  const transport = this.closest("daw-transport");
688
974
  return transport?.target ?? null;
689
975
  }
690
976
  };
691
- DawTransportButton.styles = css3`
977
+ DawTransportButton.styles = css4`
692
978
  button {
693
979
  cursor: pointer;
694
980
  background: var(--daw-controls-background, #1a1a2e);
@@ -740,7 +1026,7 @@ var DawPlayButtonElement = class extends DawTransportButton {
740
1026
  }
741
1027
  }
742
1028
  render() {
743
- return html3`
1029
+ return html4`
744
1030
  <button part="button" ?disabled=${this._isRecording} @click=${this._onClick}>
745
1031
  <slot>Play</slot>
746
1032
  </button>
@@ -761,12 +1047,12 @@ __decorateClass([
761
1047
  state()
762
1048
  ], DawPlayButtonElement.prototype, "_isRecording", 2);
763
1049
  DawPlayButtonElement = __decorateClass([
764
- customElement6("daw-play-button")
1050
+ customElement7("daw-play-button")
765
1051
  ], DawPlayButtonElement);
766
1052
 
767
1053
  // src/elements/daw-pause-button.ts
768
- import { html as html4, css as css4 } from "lit";
769
- import { customElement as customElement7, state as state2 } from "lit/decorators.js";
1054
+ import { html as html5, css as css5 } from "lit";
1055
+ import { customElement as customElement8, state as state2 } from "lit/decorators.js";
770
1056
  var DawPauseButtonElement = class extends DawTransportButton {
771
1057
  constructor() {
772
1058
  super(...arguments);
@@ -780,6 +1066,12 @@ var DawPauseButtonElement = class extends DawTransportButton {
780
1066
  this._isRecording = false;
781
1067
  this._isPaused = false;
782
1068
  };
1069
+ this._onRecPause = () => {
1070
+ this._isPaused = true;
1071
+ };
1072
+ this._onRecResume = () => {
1073
+ this._isPaused = false;
1074
+ };
783
1075
  }
784
1076
  connectedCallback() {
785
1077
  super.connectedCallback();
@@ -790,6 +1082,8 @@ var DawPauseButtonElement = class extends DawTransportButton {
790
1082
  target.addEventListener("daw-recording-start", this._onRecStart);
791
1083
  target.addEventListener("daw-recording-complete", this._onRecEnd);
792
1084
  target.addEventListener("daw-recording-error", this._onRecEnd);
1085
+ target.addEventListener("daw-recording-pause", this._onRecPause);
1086
+ target.addEventListener("daw-recording-resume", this._onRecResume);
793
1087
  });
794
1088
  }
795
1089
  disconnectedCallback() {
@@ -798,11 +1092,13 @@ var DawPauseButtonElement = class extends DawTransportButton {
798
1092
  this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
799
1093
  this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
800
1094
  this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
1095
+ this._targetRef.removeEventListener("daw-recording-pause", this._onRecPause);
1096
+ this._targetRef.removeEventListener("daw-recording-resume", this._onRecResume);
801
1097
  this._targetRef = null;
802
1098
  }
803
1099
  }
804
1100
  render() {
805
- return html4`
1101
+ return html5`
806
1102
  <button part="button" ?data-paused=${this._isPaused} @click=${this._onClick}>
807
1103
  <slot>Pause</slot>
808
1104
  </button>
@@ -817,15 +1113,7 @@ var DawPauseButtonElement = class extends DawTransportButton {
817
1113
  return;
818
1114
  }
819
1115
  if (this._isRecording) {
820
- if (this._isPaused) {
821
- target.resumeRecording();
822
- target.play(target.currentTime);
823
- this._isPaused = false;
824
- } else {
825
- target.pauseRecording();
826
- target.pause();
827
- this._isPaused = true;
828
- }
1116
+ target.togglePauseRecording();
829
1117
  } else {
830
1118
  target.pause();
831
1119
  }
@@ -833,7 +1121,7 @@ var DawPauseButtonElement = class extends DawTransportButton {
833
1121
  };
834
1122
  DawPauseButtonElement.styles = [
835
1123
  DawTransportButton.styles,
836
- css4`
1124
+ css5`
837
1125
  button[data-paused] {
838
1126
  background: rgba(255, 255, 255, 0.1);
839
1127
  border-color: var(--daw-controls-text, #e0d4c8);
@@ -847,15 +1135,15 @@ __decorateClass([
847
1135
  state2()
848
1136
  ], DawPauseButtonElement.prototype, "_isRecording", 2);
849
1137
  DawPauseButtonElement = __decorateClass([
850
- customElement7("daw-pause-button")
1138
+ customElement8("daw-pause-button")
851
1139
  ], DawPauseButtonElement);
852
1140
 
853
1141
  // src/elements/daw-stop-button.ts
854
- import { html as html5 } from "lit";
855
- import { customElement as customElement8 } from "lit/decorators.js";
1142
+ import { html as html6 } from "lit";
1143
+ import { customElement as customElement9 } from "lit/decorators.js";
856
1144
  var DawStopButtonElement = class extends DawTransportButton {
857
1145
  render() {
858
- return html5`
1146
+ return html6`
859
1147
  <button part="button" @click=${this._onClick}>
860
1148
  <slot>Stop</slot>
861
1149
  </button>
@@ -870,18 +1158,31 @@ var DawStopButtonElement = class extends DawTransportButton {
870
1158
  return;
871
1159
  }
872
1160
  if (target.isRecording) {
873
- target.stopRecording();
1161
+ target.stopRecording().catch((err) => {
1162
+ console.warn("[dawcore] stopRecording failed: " + String(err));
1163
+ }).then(() => {
1164
+ try {
1165
+ target.stop();
1166
+ } catch (err) {
1167
+ console.warn("[dawcore] stop after stopRecording failed: " + String(err));
1168
+ }
1169
+ });
1170
+ } else {
1171
+ try {
1172
+ target.stop();
1173
+ } catch (err) {
1174
+ console.warn("[dawcore] stop failed: " + String(err));
1175
+ }
874
1176
  }
875
- target.stop();
876
1177
  }
877
1178
  };
878
1179
  DawStopButtonElement = __decorateClass([
879
- customElement8("daw-stop-button")
1180
+ customElement9("daw-stop-button")
880
1181
  ], DawStopButtonElement);
881
1182
 
882
1183
  // src/elements/daw-editor.ts
883
- import { LitElement as LitElement9, html as html8, css as css8 } from "lit";
884
- import { customElement as customElement11, property as property7, state as state3 } from "lit/decorators.js";
1184
+ import { LitElement as LitElement10, html as html9, css as css9 } from "lit";
1185
+ import { customElement as customElement12, property as property8, state as state3 } from "lit/decorators.js";
885
1186
 
886
1187
  // src/types.ts
887
1188
  function isDomClip(desc) {
@@ -1366,9 +1667,9 @@ var PeakPipeline = class {
1366
1667
  };
1367
1668
 
1368
1669
  // src/elements/daw-track-controls.ts
1369
- import { LitElement as LitElement7, html as html6, css as css5 } from "lit";
1370
- import { customElement as customElement9, property as property5 } from "lit/decorators.js";
1371
- var DawTrackControlsElement = class extends LitElement7 {
1670
+ import { LitElement as LitElement8, html as html7, css as css6 } from "lit";
1671
+ import { customElement as customElement10, property as property6 } from "lit/decorators.js";
1672
+ var DawTrackControlsElement = class extends LitElement8 {
1372
1673
  constructor() {
1373
1674
  super(...arguments);
1374
1675
  this.trackId = null;
@@ -1416,7 +1717,7 @@ var DawTrackControlsElement = class extends LitElement7 {
1416
1717
  const volPercent = Math.round(this.volume * 100);
1417
1718
  const panPercent = Math.round(Math.abs(this.pan) * 100);
1418
1719
  const panDisplay = this.pan === 0 ? "C" : (this.pan > 0 ? "R" : "L") + panPercent;
1419
- return html6`
1720
+ return html7`
1420
1721
  <div class="header">
1421
1722
  <span class="name" title=${this.trackName}>${this.trackName || "Untitled"}</span>
1422
1723
  <button class="remove-btn" @click=${this._onRemoveClick} title="Remove track">
@@ -1466,7 +1767,7 @@ var DawTrackControlsElement = class extends LitElement7 {
1466
1767
  `;
1467
1768
  }
1468
1769
  };
1469
- DawTrackControlsElement.styles = css5`
1770
+ DawTrackControlsElement.styles = css6`
1470
1771
  :host {
1471
1772
  display: flex;
1472
1773
  flex-direction: column;
@@ -1600,30 +1901,30 @@ DawTrackControlsElement.styles = css5`
1600
1901
  }
1601
1902
  `;
1602
1903
  __decorateClass([
1603
- property5({ attribute: false })
1904
+ property6({ attribute: false })
1604
1905
  ], DawTrackControlsElement.prototype, "trackId", 2);
1605
1906
  __decorateClass([
1606
- property5({ attribute: false })
1907
+ property6({ attribute: false })
1607
1908
  ], DawTrackControlsElement.prototype, "trackName", 2);
1608
1909
  __decorateClass([
1609
- property5({ type: Number, attribute: false })
1910
+ property6({ type: Number, attribute: false })
1610
1911
  ], DawTrackControlsElement.prototype, "volume", 2);
1611
1912
  __decorateClass([
1612
- property5({ type: Number, attribute: false })
1913
+ property6({ type: Number, attribute: false })
1613
1914
  ], DawTrackControlsElement.prototype, "pan", 2);
1614
1915
  __decorateClass([
1615
- property5({ type: Boolean, attribute: false })
1916
+ property6({ type: Boolean, attribute: false })
1616
1917
  ], DawTrackControlsElement.prototype, "muted", 2);
1617
1918
  __decorateClass([
1618
- property5({ type: Boolean, attribute: false })
1919
+ property6({ type: Boolean, attribute: false })
1619
1920
  ], DawTrackControlsElement.prototype, "soloed", 2);
1620
1921
  DawTrackControlsElement = __decorateClass([
1621
- customElement9("daw-track-controls")
1922
+ customElement10("daw-track-controls")
1622
1923
  ], DawTrackControlsElement);
1623
1924
 
1624
1925
  // src/elements/daw-grid.ts
1625
- import { LitElement as LitElement8, html as html7, css as css6 } from "lit";
1626
- import { customElement as customElement10, property as property6 } from "lit/decorators.js";
1926
+ import { LitElement as LitElement9, html as html8, css as css7 } from "lit";
1927
+ import { customElement as customElement11, property as property7 } from "lit/decorators.js";
1627
1928
  import { MIN_PIXELS_PER_UNIT } from "@waveform-playlist/core";
1628
1929
 
1629
1930
  // src/utils/musical-tick-cache.ts
@@ -1654,8 +1955,8 @@ function getCachedMusicalTicks(params) {
1654
1955
  }
1655
1956
 
1656
1957
  // src/elements/daw-grid.ts
1657
- var MAX_CANVAS_WIDTH2 = 1e3;
1658
- var DawGridElement = class extends LitElement8 {
1958
+ var MAX_CANVAS_WIDTH3 = 1e3;
1959
+ var DawGridElement = class extends LitElement9 {
1659
1960
  constructor() {
1660
1961
  super(...arguments);
1661
1962
  this.ticksPerPixel = 24;
@@ -1683,25 +1984,25 @@ var DawGridElement = class extends LitElement8 {
1683
1984
  }
1684
1985
  }
1685
1986
  render() {
1686
- if (!this._tickData) return html7``;
1987
+ if (!this._tickData) return html8``;
1687
1988
  const totalWidth = this.length;
1688
1989
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1689
1990
  const indices = getVisibleChunkIndices(
1690
1991
  totalWidth,
1691
- MAX_CANVAS_WIDTH2,
1992
+ MAX_CANVAS_WIDTH3,
1692
1993
  this.visibleStart,
1693
1994
  this.visibleEnd
1694
1995
  );
1695
- return html7`
1996
+ return html8`
1696
1997
  <div class="container" style="width: ${totalWidth}px; height: ${this.height}px;">
1697
1998
  ${indices.map((i) => {
1698
- const width = Math.min(MAX_CANVAS_WIDTH2, totalWidth - i * MAX_CANVAS_WIDTH2);
1699
- return html7`
1999
+ const width = Math.min(MAX_CANVAS_WIDTH3, totalWidth - i * MAX_CANVAS_WIDTH3);
2000
+ return html8`
1700
2001
  <canvas
1701
2002
  data-index=${i}
1702
2003
  width=${width * dpr}
1703
2004
  height=${this.height * dpr}
1704
- style="left: ${i * MAX_CANVAS_WIDTH2}px; width: ${width}px; height: ${this.height}px;"
2005
+ style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.height}px;"
1705
2006
  ></canvas>
1706
2007
  `;
1707
2008
  })}
@@ -1725,8 +2026,8 @@ var DawGridElement = class extends LitElement8 {
1725
2026
  const idx = Number(canvas.dataset.index);
1726
2027
  const ctx = canvas.getContext("2d");
1727
2028
  if (!ctx) continue;
1728
- const chunkLeft = idx * MAX_CANVAS_WIDTH2;
1729
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH2, this.length - chunkLeft);
2029
+ const chunkLeft = idx * MAX_CANVAS_WIDTH3;
2030
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, this.length - chunkLeft);
1730
2031
  ctx.resetTransform();
1731
2032
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1732
2033
  ctx.scale(dpr, dpr);
@@ -1757,7 +2058,7 @@ var DawGridElement = class extends LitElement8 {
1757
2058
  }
1758
2059
  }
1759
2060
  };
1760
- DawGridElement.styles = css6`
2061
+ DawGridElement.styles = css7`
1761
2062
  :host {
1762
2063
  display: block;
1763
2064
  position: absolute;
@@ -1775,33 +2076,33 @@ DawGridElement.styles = css6`
1775
2076
  }
1776
2077
  `;
1777
2078
  __decorateClass([
1778
- property6({ type: Number, attribute: false })
2079
+ property7({ type: Number, attribute: false })
1779
2080
  ], DawGridElement.prototype, "ticksPerPixel", 2);
1780
2081
  __decorateClass([
1781
- property6({ attribute: false })
2082
+ property7({ attribute: false })
1782
2083
  ], DawGridElement.prototype, "meterEntries", 2);
1783
2084
  __decorateClass([
1784
- property6({ type: Number, attribute: false })
2085
+ property7({ type: Number, attribute: false })
1785
2086
  ], DawGridElement.prototype, "ppqn", 2);
1786
2087
  __decorateClass([
1787
- property6({ type: Number, attribute: false })
2088
+ property7({ type: Number, attribute: false })
1788
2089
  ], DawGridElement.prototype, "visibleStart", 2);
1789
2090
  __decorateClass([
1790
- property6({ type: Number, attribute: false })
2091
+ property7({ type: Number, attribute: false })
1791
2092
  ], DawGridElement.prototype, "visibleEnd", 2);
1792
2093
  __decorateClass([
1793
- property6({ type: Number, attribute: false })
2094
+ property7({ type: Number, attribute: false })
1794
2095
  ], DawGridElement.prototype, "length", 2);
1795
2096
  __decorateClass([
1796
- property6({ type: Number, attribute: false })
2097
+ property7({ type: Number, attribute: false })
1797
2098
  ], DawGridElement.prototype, "height", 2);
1798
2099
  DawGridElement = __decorateClass([
1799
- customElement10("daw-grid")
2100
+ customElement11("daw-grid")
1800
2101
  ], DawGridElement);
1801
2102
 
1802
2103
  // src/styles/theme.ts
1803
- import { css as css7 } from "lit";
1804
- var hostStyles = css7`
2104
+ import { css as css8 } from "lit";
2105
+ var hostStyles = css8`
1805
2106
  :host {
1806
2107
  --daw-wave-color: #c49a6c;
1807
2108
  --daw-progress-color: #63c75f;
@@ -1817,7 +2118,7 @@ var hostStyles = css7`
1817
2118
  --daw-clip-header-text: #e0d4c8;
1818
2119
  }
1819
2120
  `;
1820
- var clipStyles = css7`
2121
+ var clipStyles = css8`
1821
2122
  .clip-container {
1822
2123
  position: absolute;
1823
2124
  overflow: hidden;
@@ -2062,6 +2363,9 @@ var RecordingController = class {
2062
2363
  constructor(host) {
2063
2364
  this._sessions = /* @__PURE__ */ new Map();
2064
2365
  this._workletLoadedCtx = null;
2366
+ /** Tracks worklet pause state explicitly so external consumers (editor,
2367
+ * pause button, spacebar) can share one source of truth. */
2368
+ this._isPaused = false;
2065
2369
  this._host = host;
2066
2370
  host.addController(this);
2067
2371
  }
@@ -2076,6 +2380,9 @@ var RecordingController = class {
2076
2380
  get isRecording() {
2077
2381
  return this._sessions.size > 0;
2078
2382
  }
2383
+ get isPaused() {
2384
+ return this._isPaused && this._sessions.size > 0;
2385
+ }
2079
2386
  getSession(trackId) {
2080
2387
  return this._sessions.get(trackId);
2081
2388
  }
@@ -2148,7 +2455,9 @@ var RecordingController = class {
2148
2455
  latencySamples,
2149
2456
  wasOverdub: options.overdub ?? false,
2150
2457
  _onTrackEnded: onTrackEnded,
2151
- _audioTrack: audioTrack
2458
+ _audioTrack: audioTrack,
2459
+ stopAckResolve: null,
2460
+ stopping: false
2152
2461
  };
2153
2462
  this._sessions.set(trackId, session);
2154
2463
  workletNode.port.onmessage = (e) => {
@@ -2186,25 +2495,69 @@ var RecordingController = class {
2186
2495
  const id = trackId ?? [...this._sessions.keys()][0];
2187
2496
  if (!id) return;
2188
2497
  const session = this._sessions.get(id);
2189
- if (!session) return;
2498
+ if (!session || session.stopping) return;
2190
2499
  session.workletNode.port.postMessage({ command: "pause" });
2500
+ this._isPaused = true;
2501
+ this._host.dispatchEvent(
2502
+ new CustomEvent("daw-recording-pause", {
2503
+ bubbles: true,
2504
+ composed: true,
2505
+ detail: { trackId: id }
2506
+ })
2507
+ );
2191
2508
  }
2192
2509
  resumeRecording(trackId) {
2193
2510
  const id = trackId ?? [...this._sessions.keys()][0];
2194
2511
  if (!id) return;
2195
2512
  const session = this._sessions.get(id);
2196
- if (!session) return;
2513
+ if (!session || session.stopping) return;
2197
2514
  session.workletNode.port.postMessage({ command: "resume" });
2515
+ this._isPaused = false;
2516
+ this._host.dispatchEvent(
2517
+ new CustomEvent("daw-recording-resume", {
2518
+ bubbles: true,
2519
+ composed: true,
2520
+ detail: { trackId: id }
2521
+ })
2522
+ );
2198
2523
  }
2199
- stopRecording(trackId) {
2524
+ async stopRecording(trackId) {
2200
2525
  const id = trackId ?? [...this._sessions.keys()][0];
2201
2526
  if (!id) return;
2202
2527
  const session = this._sessions.get(id);
2203
2528
  if (!session) return;
2529
+ const wasPaused = this._isPaused;
2530
+ this._isPaused = false;
2531
+ session.stopping = true;
2204
2532
  if (session.wasOverdub && typeof this._host.stop === "function") {
2205
2533
  this._host.stop();
2206
2534
  }
2207
- session.workletNode.port.postMessage({ command: "stop" });
2535
+ if (wasPaused) {
2536
+ session.workletNode.port.postMessage({ command: "stop" });
2537
+ } else {
2538
+ const stopAck = new Promise((resolve) => {
2539
+ session.stopAckResolve = resolve;
2540
+ });
2541
+ let timeoutId;
2542
+ const timeout = new Promise((resolve) => {
2543
+ timeoutId = setTimeout(resolve, 1e3);
2544
+ });
2545
+ session.workletNode.port.postMessage({ command: "stop" });
2546
+ await Promise.race([stopAck, timeout]);
2547
+ clearTimeout(timeoutId);
2548
+ session.stopAckResolve = null;
2549
+ let lastSamples = -1;
2550
+ let stable = 0;
2551
+ for (let i = 0; i < 50; i++) {
2552
+ if (session.totalSamples === lastSamples) {
2553
+ if (++stable >= 3) break;
2554
+ } else {
2555
+ stable = 0;
2556
+ lastSamples = session.totalSamples;
2557
+ }
2558
+ await new Promise((r) => setTimeout(r, 5));
2559
+ }
2560
+ }
2208
2561
  session.source.disconnect();
2209
2562
  session.workletNode.disconnect();
2210
2563
  this._removeTrackEndedListener(session);
@@ -2276,8 +2629,20 @@ var RecordingController = class {
2276
2629
  _onWorkletMessage(trackId, data) {
2277
2630
  const session = this._sessions.get(trackId);
2278
2631
  if (!session) return;
2279
- const { channels } = data;
2280
- if (!channels || channels.length === 0 || !channels[0]) return;
2632
+ const { channels, done } = data;
2633
+ try {
2634
+ const hasSamples = !!(channels && channels.length > 0 && channels[0] && channels[0].length > 0);
2635
+ if (!hasSamples) return;
2636
+ this._processWorkletSamples(trackId, session, channels);
2637
+ } finally {
2638
+ if (done && session.stopAckResolve) {
2639
+ const resolve = session.stopAckResolve;
2640
+ session.stopAckResolve = null;
2641
+ resolve();
2642
+ }
2643
+ }
2644
+ }
2645
+ _processWorkletSamples(trackId, session, channels) {
2281
2646
  const samplesProcessedBefore = session.totalSamples;
2282
2647
  for (let ch = 0; ch < session.channelCount; ch++) {
2283
2648
  if (channels[ch]) {
@@ -2285,6 +2650,7 @@ var RecordingController = class {
2285
2650
  }
2286
2651
  }
2287
2652
  session.totalSamples += channels[0].length;
2653
+ if (session.stopAckResolve !== null) return;
2288
2654
  for (let ch = 0; ch < session.channelCount; ch++) {
2289
2655
  if (!channels[ch]) continue;
2290
2656
  const oldPeakCount = Math.floor(session.peaks[ch].length / 2);
@@ -2355,6 +2721,123 @@ var RecordingController = class {
2355
2721
  }
2356
2722
  };
2357
2723
 
2724
+ // src/controllers/spectrogram-controller.ts
2725
+ import {
2726
+ SpectrogramOrchestrator
2727
+ } from "@dawcore/spectrogram";
2728
+ var LIBRARY_DEFAULTS = {
2729
+ fftSize: 2048,
2730
+ windowFunction: "hann",
2731
+ frequencyScale: "mel",
2732
+ minFrequency: 0,
2733
+ gainDb: 20,
2734
+ rangeDb: 80
2735
+ };
2736
+ var LIBRARY_DEFAULT_COLOR_MAP = "viridis";
2737
+ var SpectrogramController = class {
2738
+ constructor(host, workerFactory) {
2739
+ this.orchestrator = null;
2740
+ this.editorConfig = null;
2741
+ this.editorColorMap = null;
2742
+ this.trackConfigs = /* @__PURE__ */ new Map();
2743
+ this.trackColorMaps = /* @__PURE__ */ new Map();
2744
+ this.host = host;
2745
+ this.workerFactory = workerFactory;
2746
+ this.host.addController(this);
2747
+ }
2748
+ hostConnected() {
2749
+ }
2750
+ hostDisconnected() {
2751
+ this.dispose();
2752
+ }
2753
+ setEditorConfig(config) {
2754
+ this.editorConfig = config;
2755
+ this.reapply();
2756
+ }
2757
+ setEditorColorMap(colorMap) {
2758
+ this.editorColorMap = colorMap;
2759
+ this.reapply();
2760
+ }
2761
+ setTrackConfig(trackId, config) {
2762
+ if (config === null) {
2763
+ this.trackConfigs.delete(trackId);
2764
+ } else {
2765
+ this.trackConfigs.set(trackId, config);
2766
+ }
2767
+ this.reapply();
2768
+ }
2769
+ setTrackColorMap(trackId, colorMap) {
2770
+ if (colorMap === null) {
2771
+ this.trackColorMaps.delete(trackId);
2772
+ } else {
2773
+ this.trackColorMaps.set(trackId, colorMap);
2774
+ }
2775
+ this.reapply();
2776
+ }
2777
+ registerClipAudio(reg) {
2778
+ this.ensureOrchestrator().registerClip(reg);
2779
+ }
2780
+ unregisterClipAudio(clipId) {
2781
+ this.orchestrator?.unregisterClip(clipId);
2782
+ }
2783
+ registerCanvas(reg) {
2784
+ this.ensureOrchestrator().registerCanvas(reg);
2785
+ }
2786
+ unregisterCanvas(canvasId) {
2787
+ this.orchestrator?.unregisterCanvas(canvasId);
2788
+ }
2789
+ setViewport(state5) {
2790
+ this.orchestrator?.setViewport(state5);
2791
+ }
2792
+ dispose() {
2793
+ if (this.orchestrator) {
2794
+ this.orchestrator.dispose();
2795
+ this.orchestrator = null;
2796
+ }
2797
+ }
2798
+ ensureOrchestrator() {
2799
+ if (!this.orchestrator) {
2800
+ this.orchestrator = new SpectrogramOrchestrator({
2801
+ workerFactory: this.workerFactory,
2802
+ workerPoolSize: 2,
2803
+ config: this.mergedConfig(),
2804
+ colorMap: this.mergedColorMap()
2805
+ });
2806
+ this.orchestrator.addEventListener("viewport-ready", (e) => {
2807
+ const detail = e.detail;
2808
+ this.host.dispatchEvent(
2809
+ new CustomEvent("daw-spectrogram-ready", {
2810
+ detail,
2811
+ bubbles: true,
2812
+ composed: true
2813
+ })
2814
+ );
2815
+ });
2816
+ this.reapply();
2817
+ }
2818
+ return this.orchestrator;
2819
+ }
2820
+ reapply() {
2821
+ if (!this.orchestrator) return;
2822
+ this.orchestrator.setConfig(this.mergedConfig());
2823
+ this.orchestrator.setColorMap(this.mergedColorMap());
2824
+ }
2825
+ mergedConfig() {
2826
+ let track = null;
2827
+ for (const c of this.trackConfigs.values()) {
2828
+ track = c;
2829
+ break;
2830
+ }
2831
+ return { ...LIBRARY_DEFAULTS, ...this.editorConfig ?? {}, ...track ?? {} };
2832
+ }
2833
+ mergedColorMap() {
2834
+ for (const c of this.trackColorMaps.values()) {
2835
+ return c ?? LIBRARY_DEFAULT_COLOR_MAP;
2836
+ }
2837
+ return this.editorColorMap ?? LIBRARY_DEFAULT_COLOR_MAP;
2838
+ }
2839
+ };
2840
+
2358
2841
  // src/interactions/pointer-handler.ts
2359
2842
  import { pixelsToSeconds, snapTickToGrid } from "@waveform-playlist/core";
2360
2843
 
@@ -2619,6 +3102,7 @@ var ClipPointerHandler = class {
2619
3102
  const trackId = boundary.dataset.trackId;
2620
3103
  const edge = boundary.dataset.boundaryEdge;
2621
3104
  if (!clipId || !trackId || edge !== "left" && edge !== "right") return false;
3105
+ if (this._host.isMidiClip(trackId, clipId)) return true;
2622
3106
  this._beginDrag(edge === "left" ? "trim-left" : "trim-right", clipId, trackId, e);
2623
3107
  this._boundaryEl = boundary;
2624
3108
  return true;
@@ -2912,6 +3396,7 @@ async function loadFiles(host, files) {
2912
3396
  pan: 0,
2913
3397
  muted: false,
2914
3398
  soloed: false,
3399
+ renderMode: "waveform",
2915
3400
  clips: [
2916
3401
  {
2917
3402
  kind: "drop",
@@ -2924,7 +3409,10 @@ async function loadFiles(host, files) {
2924
3409
  name,
2925
3410
  fadeIn: 0,
2926
3411
  fadeOut: 0,
2927
- fadeType: "linear"
3412
+ fadeType: "linear",
3413
+ midiNotes: null,
3414
+ midiChannel: null,
3415
+ midiProgram: null
2928
3416
  }
2929
3417
  ]
2930
3418
  });
@@ -3011,7 +3499,10 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
3011
3499
  name: "Recording",
3012
3500
  fadeIn: 0,
3013
3501
  fadeOut: 0,
3014
- fadeType: "linear"
3502
+ fadeType: "linear",
3503
+ midiNotes: null,
3504
+ midiChannel: null,
3505
+ midiProgram: null
3015
3506
  };
3016
3507
  host._tracks = new Map(host._tracks).set(trackId, {
3017
3508
  ...desc,
@@ -3070,7 +3561,10 @@ function canSplitAtTime(host, time) {
3070
3561
  const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
3071
3562
  if (!track) return false;
3072
3563
  const atSample = Math.round(time * host.effectiveSampleRate);
3073
- return !!findClipAtSample(track.clips, atSample);
3564
+ const clip = findClipAtSample(track.clips, atSample);
3565
+ if (!clip) return false;
3566
+ if (clip.midiNotes != null) return false;
3567
+ return true;
3074
3568
  }
3075
3569
  function performSplit(host, time) {
3076
3570
  const { engine } = host;
@@ -3225,7 +3719,7 @@ async function loadWaveformDataFromUrl(src) {
3225
3719
 
3226
3720
  // src/elements/daw-editor.ts
3227
3721
  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();";
3228
- var DawEditorElement = class extends LitElement9 {
3722
+ var DawEditorElement = class extends LitElement10 {
3229
3723
  constructor() {
3230
3724
  super(...arguments);
3231
3725
  this._samplesPerPixel = 1024;
@@ -3239,6 +3733,8 @@ var DawEditorElement = class extends LitElement9 {
3239
3733
  this.clipHeaderHeight = 20;
3240
3734
  this.interactiveClips = false;
3241
3735
  this.indefinitePlayback = false;
3736
+ this._spectrogramConfig = null;
3737
+ this._spectrogramColorMap = null;
3242
3738
  this.scaleMode = "temporal";
3243
3739
  this._ticksPerPixel = 24;
3244
3740
  this._bpm = 120;
@@ -3274,6 +3770,7 @@ var DawEditorElement = class extends LitElement9 {
3274
3770
  this._childObserver = null;
3275
3771
  this._audioResume = new AudioResumeController(this);
3276
3772
  this._recordingController = new RecordingController(this);
3773
+ this._spectrogramController = null;
3277
3774
  this._clipPointer = new ClipPointerHandler(this);
3278
3775
  this._pointer = new PointerHandler(this);
3279
3776
  this._viewport = (() => {
@@ -3314,6 +3811,26 @@ var DawEditorElement = class extends LitElement9 {
3314
3811
  if (oldDescriptor?.src !== descriptor.src) {
3315
3812
  this._loadTrack(trackId, descriptor);
3316
3813
  }
3814
+ if (descriptor.renderMode === "spectrogram" && oldDescriptor?.renderMode !== "spectrogram") {
3815
+ const engineTrack = this._engineTracks.get(trackId);
3816
+ if (engineTrack) {
3817
+ for (const clip of engineTrack.clips) {
3818
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
3819
+ }
3820
+ }
3821
+ }
3822
+ if (descriptor.renderMode !== "spectrogram" && oldDescriptor?.renderMode === "spectrogram") {
3823
+ const engineTrack = this._engineTracks.get(trackId);
3824
+ if (engineTrack && this._spectrogramController) {
3825
+ for (const clip of engineTrack.clips) {
3826
+ this._spectrogramController.unregisterClipAudio(clip.id);
3827
+ }
3828
+ }
3829
+ this._disposeSpectrogramControllerIfEmpty();
3830
+ }
3831
+ if (descriptor.spectrogramConfig !== oldDescriptor?.spectrogramConfig) {
3832
+ this._spectrogramController?.setTrackConfig(trackId, descriptor.spectrogramConfig ?? null);
3833
+ }
3317
3834
  };
3318
3835
  this._onTrackControl = (e) => {
3319
3836
  const { trackId, prop, value } = e.detail ?? {};
@@ -3384,7 +3901,10 @@ var DawEditorElement = class extends LitElement9 {
3384
3901
  name: clipEl.name,
3385
3902
  fadeIn: clipEl.fadeIn,
3386
3903
  fadeOut: clipEl.fadeOut,
3387
- fadeType: clipEl.fadeType
3904
+ fadeType: clipEl.fadeType,
3905
+ midiNotes: clipEl.midiNotes,
3906
+ midiChannel: clipEl.midiChannel,
3907
+ midiProgram: clipEl.midiProgram
3388
3908
  };
3389
3909
  this._loadAndAppendClip(trackId, clipDesc);
3390
3910
  };
@@ -3435,6 +3955,9 @@ var DawEditorElement = class extends LitElement9 {
3435
3955
  };
3436
3956
  // --- Recording ---
3437
3957
  this.recordingStream = null;
3958
+ /** Set in togglePauseRecording when Transport is paused alongside the
3959
+ * worklet, so resume can restart it. Cleared on resume and on stop. */
3960
+ this._wasPlayingDuringRecording = false;
3438
3961
  }
3439
3962
  get samplesPerPixel() {
3440
3963
  return this._samplesPerPixel;
@@ -3451,6 +3974,72 @@ var DawEditorElement = class extends LitElement9 {
3451
3974
  this._samplesPerPixel = clamped;
3452
3975
  this.requestUpdate("samplesPerPixel", old);
3453
3976
  }
3977
+ get spectrogramConfig() {
3978
+ return this._spectrogramConfig;
3979
+ }
3980
+ set spectrogramConfig(value) {
3981
+ const old = this._spectrogramConfig;
3982
+ this._spectrogramConfig = value;
3983
+ this._spectrogramController?.setEditorConfig(value);
3984
+ this.requestUpdate("spectrogramConfig", old);
3985
+ }
3986
+ get spectrogramColorMap() {
3987
+ return this._spectrogramColorMap;
3988
+ }
3989
+ set spectrogramColorMap(value) {
3990
+ const old = this._spectrogramColorMap;
3991
+ this._spectrogramColorMap = value;
3992
+ this._spectrogramController?.setEditorColorMap(value);
3993
+ this.requestUpdate("spectrogramColorMap", old);
3994
+ }
3995
+ _ensureSpectrogramController() {
3996
+ if (!this._spectrogramController) {
3997
+ this._spectrogramController = new SpectrogramController(
3998
+ this,
3999
+ () => new Worker(new URL("@dawcore/spectrogram/worker/spectrogram.worker", import.meta.url), {
4000
+ type: "module"
4001
+ })
4002
+ );
4003
+ if (this._spectrogramConfig) {
4004
+ this._spectrogramController.setEditorConfig(this._spectrogramConfig);
4005
+ }
4006
+ if (this._spectrogramColorMap) {
4007
+ this._spectrogramController.setEditorColorMap(this._spectrogramColorMap);
4008
+ }
4009
+ }
4010
+ return this._spectrogramController;
4011
+ }
4012
+ /** Called by <daw-spectrogram> after transferControlToOffscreen. */
4013
+ _spectrogramRegisterCanvas(reg) {
4014
+ this._ensureSpectrogramController().registerCanvas(reg);
4015
+ }
4016
+ /** Called by <daw-spectrogram> on chunk unmount / element disconnect. */
4017
+ _spectrogramUnregisterCanvas(canvasId) {
4018
+ this._spectrogramController?.unregisterCanvas(canvasId);
4019
+ }
4020
+ /**
4021
+ * Push a clip's decoded audio into the spectrogram controller. No-op
4022
+ * unless the track is in spectrogram render-mode and the controller
4023
+ * already exists (it bootstraps from canvas registration).
4024
+ */
4025
+ _maybeRegisterSpectrogramClipAudio(trackId, clip) {
4026
+ const descriptor = this._tracks.get(trackId);
4027
+ if (descriptor?.renderMode !== "spectrogram") return;
4028
+ const buffer = clip.audioBuffer ?? this._clipBuffers.get(clip.id);
4029
+ if (!buffer) return;
4030
+ const channelData = [];
4031
+ for (let i = 0; i < buffer.numberOfChannels; i++) {
4032
+ channelData.push(buffer.getChannelData(i));
4033
+ }
4034
+ this._ensureSpectrogramController().registerClipAudio({
4035
+ clipId: clip.id,
4036
+ trackId,
4037
+ channelData,
4038
+ sampleRate: buffer.sampleRate,
4039
+ durationSamples: clip.durationSamples,
4040
+ offsetSamples: clip.offsetSamples
4041
+ });
4042
+ }
3454
4043
  get ticksPerPixel() {
3455
4044
  return this._ticksPerPixel;
3456
4045
  }
@@ -3534,6 +4123,17 @@ var DawEditorElement = class extends LitElement9 {
3534
4123
  );
3535
4124
  return result.get(clipId) ?? null;
3536
4125
  }
4126
+ /**
4127
+ * Returns true if the clip is a MIDI clip (has midiNotes).
4128
+ * Used by ClipPointerHandler to make trim handles inert for MIDI clips.
4129
+ * Returns false for unknown track/clip IDs (defensive).
4130
+ */
4131
+ isMidiClip(trackId, clipId) {
4132
+ const track = this._engineTracks.get(trackId);
4133
+ if (!track) return false;
4134
+ const clip = track.clips.find((c) => c.id === clipId);
4135
+ return clip?.midiNotes != null;
4136
+ }
3537
4137
  get effectiveSampleRate() {
3538
4138
  return this._resolvedSampleRate ?? this.sampleRate;
3539
4139
  }
@@ -3680,6 +4280,8 @@ var DawEditorElement = class extends LitElement9 {
3680
4280
  this._clipOffsets.clear();
3681
4281
  this._peakPipeline.terminate();
3682
4282
  this._minSamplesPerPixel = 0;
4283
+ this._spectrogramController?.dispose();
4284
+ this._spectrogramController = null;
3683
4285
  try {
3684
4286
  this._disposeEngine();
3685
4287
  } catch (err) {
@@ -3708,6 +4310,23 @@ var DawEditorElement = class extends LitElement9 {
3708
4310
  }
3709
4311
  }
3710
4312
  }
4313
+ updated(_changed) {
4314
+ if (this._spectrogramController) {
4315
+ const vs = this._viewport.visibleStart;
4316
+ const ve = this._viewport.visibleEnd;
4317
+ if (Number.isFinite(vs) && Number.isFinite(ve)) {
4318
+ const span = ve - vs;
4319
+ const bufferPad = span * 0.25;
4320
+ this._spectrogramController.setViewport({
4321
+ visibleStartPx: vs,
4322
+ visibleEndPx: ve,
4323
+ bufferStartPx: Math.max(0, vs - bufferPad),
4324
+ bufferEndPx: ve + bufferPad,
4325
+ samplesPerPixel: this._renderSpp
4326
+ });
4327
+ }
4328
+ }
4329
+ }
3711
4330
  _onTrackRemoved(trackId) {
3712
4331
  this._trackElements.delete(trackId);
3713
4332
  const removedTrack = this._engineTracks.get(trackId);
@@ -3717,6 +4336,7 @@ var DawEditorElement = class extends LitElement9 {
3717
4336
  this._clipBuffers.delete(clip.id);
3718
4337
  this._clipOffsets.delete(clip.id);
3719
4338
  nextPeaks.delete(clip.id);
4339
+ this._spectrogramController?.unregisterClipAudio(clip.id);
3720
4340
  }
3721
4341
  this._peaksData = nextPeaks;
3722
4342
  }
@@ -3731,11 +4351,23 @@ var DawEditorElement = class extends LitElement9 {
3731
4351
  this._engine.removeTrack(trackId);
3732
4352
  }
3733
4353
  this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
4354
+ this._disposeSpectrogramControllerIfEmpty();
3734
4355
  if (nextEngine.size === 0) {
3735
4356
  this._currentTime = 0;
3736
4357
  this._stopPlayhead();
3737
4358
  }
3738
4359
  }
4360
+ /** Drop the controller when no spectrogram tracks remain. */
4361
+ _disposeSpectrogramControllerIfEmpty() {
4362
+ if (!this._spectrogramController) return;
4363
+ const stillNeeded = Array.from(this._tracks.values()).some(
4364
+ (d) => d.renderMode === "spectrogram"
4365
+ );
4366
+ if (!stillNeeded) {
4367
+ this._spectrogramController.dispose();
4368
+ this._spectrogramController = null;
4369
+ }
4370
+ }
3739
4371
  _onClipRemovedFromDom(clipEl) {
3740
4372
  const clipId = clipEl.clipId;
3741
4373
  for (const [trackId, t] of this._engineTracks.entries()) {
@@ -3777,6 +4409,7 @@ var DawEditorElement = class extends LitElement9 {
3777
4409
  });
3778
4410
  }
3779
4411
  this._commitTrackChange(trackId, updatedTrack);
4412
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
3780
4413
  this.dispatchEvent(
3781
4414
  new CustomEvent("daw-clip-ready", {
3782
4415
  bubbles: true,
@@ -3883,6 +4516,65 @@ var DawEditorElement = class extends LitElement9 {
3883
4516
  }
3884
4517
  return clip;
3885
4518
  }
4519
+ /**
4520
+ * Filter MIDI notes to only those with finite, in-range fields. Logs a
4521
+ * warning for each dropped note. Used by _buildMidiClip and the
4522
+ * _applyClipUpdate MIDI branch to prevent NaN propagation through the
4523
+ * timeline.
4524
+ */
4525
+ _validMidiNotes(notes) {
4526
+ return notes.filter((n) => {
4527
+ 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;
4528
+ if (!ok) {
4529
+ console.warn("[dawcore] dropping malformed MIDI note: " + JSON.stringify(n));
4530
+ }
4531
+ return ok;
4532
+ });
4533
+ }
4534
+ /**
4535
+ * A clip descriptor is treated as MIDI when it has no audio src.
4536
+ * Includes placeholder MIDI clips (no notes, no duration yet — registered
4537
+ * with a 1s default span; notes upgrade via _applyClipUpdate). Warns when
4538
+ * a clip ambiguously has both src and midiNotes — the audio path runs
4539
+ * and notes would be silently ignored.
4540
+ */
4541
+ _isMidiDescriptor(clipDesc) {
4542
+ if (clipDesc.src) {
4543
+ if (clipDesc.midiNotes != null) {
4544
+ console.warn(
4545
+ '[dawcore] clip "' + (clipDesc.name || (isDomClip(clipDesc) ? clipDesc.clipId : "?")) + '" has both src and midiNotes \u2014 treating as audio (notes will be ignored)'
4546
+ );
4547
+ }
4548
+ return false;
4549
+ }
4550
+ return true;
4551
+ }
4552
+ /**
4553
+ * Build an engine clip from a MIDI clip descriptor. Always returns a clip
4554
+ * — empty notes / no declared duration get a 1-second placeholder span so
4555
+ * the clip is reachable via `engine.updateTrack` once notes arrive.
4556
+ */
4557
+ _buildMidiClip(clipDesc) {
4558
+ const sr = this.effectiveSampleRate;
4559
+ const notes = this._validMidiNotes(clipDesc.midiNotes ?? []);
4560
+ const noteSpanSeconds = notes.length ? notes.reduce((max, n) => Math.max(max, n.time + n.duration), 0) : 0;
4561
+ const sourceDurationSamples = Math.ceil(Math.max(noteSpanSeconds, clipDesc.duration, 1) * sr);
4562
+ const requestedDurationSamples = clipDesc.duration > 0 ? Math.round(clipDesc.duration * sr) : sourceDurationSamples;
4563
+ const clip = createClip3({
4564
+ startSample: Math.round(clipDesc.start * sr),
4565
+ durationSamples: requestedDurationSamples,
4566
+ offsetSamples: Math.round(clipDesc.offset * sr),
4567
+ sampleRate: sr,
4568
+ sourceDurationSamples,
4569
+ gain: clipDesc.gain,
4570
+ name: clipDesc.name,
4571
+ midiNotes: notes,
4572
+ midiChannel: clipDesc.midiChannel ?? void 0,
4573
+ midiProgram: clipDesc.midiProgram ?? void 0
4574
+ });
4575
+ if (isDomClip(clipDesc)) clip.id = clipDesc.clipId;
4576
+ return clip;
4577
+ }
3886
4578
  /** Remove a single clip from all per-clip caches. Used by error rollbacks. */
3887
4579
  _purgeClipCaches(clipId) {
3888
4580
  const nextBuffers = new Map(this._clipBuffers);
@@ -3892,6 +4584,7 @@ var DawEditorElement = class extends LitElement9 {
3892
4584
  nextPeaks.delete(clipId);
3893
4585
  this._peaksData = nextPeaks;
3894
4586
  this._clipOffsets.delete(clipId);
4587
+ this._spectrogramController?.unregisterClipAudio(clipId);
3895
4588
  }
3896
4589
  /**
3897
4590
  * Recompute duration and forward an updated track to the engine. Single
@@ -3920,6 +4613,34 @@ var DawEditorElement = class extends LitElement9 {
3920
4613
  }
3921
4614
  const oldClip = t.clips[idx];
3922
4615
  const sr = oldClip.sampleRate ?? this.effectiveSampleRate;
4616
+ const isMidiNow = clipEl.midiNotes != null;
4617
+ const wasMidi = oldClip.midiNotes != null;
4618
+ if (isMidiNow || wasMidi) {
4619
+ const notes = this._validMidiNotes(clipEl.midiNotes ?? []);
4620
+ const noteSpanSeconds = notes.length ? notes.reduce((max, n) => Math.max(max, n.time + n.duration), 0) : 0;
4621
+ const sourceDurationSamples = Math.ceil(Math.max(noteSpanSeconds, clipEl.duration, 1) * sr);
4622
+ const requestedDurationSamples = clipEl.duration > 0 ? Math.round(clipEl.duration * sr) : sourceDurationSamples;
4623
+ const updatedClip2 = {
4624
+ ...oldClip,
4625
+ audioBuffer: void 0,
4626
+ startSample: Math.round(clipEl.start * sr),
4627
+ offsetSamples: Math.round(clipEl.offset * sr),
4628
+ durationSamples: requestedDurationSamples,
4629
+ sourceDurationSamples,
4630
+ gain: clipEl.gain,
4631
+ name: clipEl.name || oldClip.name,
4632
+ midiNotes: notes,
4633
+ midiChannel: clipEl.midiChannel ?? void 0,
4634
+ midiProgram: clipEl.midiProgram ?? void 0
4635
+ };
4636
+ const updatedClips2 = [...t.clips];
4637
+ updatedClips2[idx] = updatedClip2;
4638
+ const updatedTrack2 = { ...t, clips: updatedClips2 };
4639
+ this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack2);
4640
+ this._purgeClipCaches(clipId);
4641
+ this._commitTrackChange(trackId, updatedTrack2);
4642
+ return;
4643
+ }
3923
4644
  const newStartSample = Math.round(clipEl.start * sr);
3924
4645
  const newDurationSamples = clipEl.duration > 0 ? Math.round(clipEl.duration * sr) : oldClip.durationSamples;
3925
4646
  const newOffsetSamples = Math.round(clipEl.offset * sr);
@@ -3996,7 +4717,10 @@ var DawEditorElement = class extends LitElement9 {
3996
4717
  name: trackEl.name || "",
3997
4718
  fadeIn: 0,
3998
4719
  fadeOut: 0,
3999
- fadeType: "linear"
4720
+ fadeType: "linear",
4721
+ midiNotes: null,
4722
+ midiChannel: null,
4723
+ midiProgram: null
4000
4724
  });
4001
4725
  } else {
4002
4726
  for (const clipEl of clipEls) {
@@ -4012,7 +4736,10 @@ var DawEditorElement = class extends LitElement9 {
4012
4736
  name: clipEl.name,
4013
4737
  fadeIn: clipEl.fadeIn,
4014
4738
  fadeOut: clipEl.fadeOut,
4015
- fadeType: clipEl.fadeType
4739
+ fadeType: clipEl.fadeType,
4740
+ midiNotes: clipEl.midiNotes,
4741
+ midiChannel: clipEl.midiChannel,
4742
+ midiProgram: clipEl.midiProgram
4016
4743
  });
4017
4744
  }
4018
4745
  }
@@ -4023,6 +4750,7 @@ var DawEditorElement = class extends LitElement9 {
4023
4750
  pan: trackEl.pan,
4024
4751
  muted: trackEl.muted,
4025
4752
  soloed: trackEl.soloed,
4753
+ renderMode: trackEl.renderMode,
4026
4754
  clips
4027
4755
  };
4028
4756
  }
@@ -4031,7 +4759,10 @@ var DawEditorElement = class extends LitElement9 {
4031
4759
  try {
4032
4760
  const clips = [];
4033
4761
  for (const clipDesc of descriptor.clips) {
4034
- if (!clipDesc.src) continue;
4762
+ if (this._isMidiDescriptor(clipDesc)) {
4763
+ clips.push(this._buildMidiClip(clipDesc));
4764
+ continue;
4765
+ }
4035
4766
  try {
4036
4767
  const waveformDataPromise = clipDesc.peaksSrc ? this._resolvePeaks(clipDesc.peaksSrc) : Promise.resolve(null);
4037
4768
  const audioPromise = this._fetchAndDecode(clipDesc.src);
@@ -4136,6 +4867,9 @@ var DawEditorElement = class extends LitElement9 {
4136
4867
  track.id = trackId;
4137
4868
  this._engineTracks = new Map(this._engineTracks).set(trackId, track);
4138
4869
  this._recomputeDuration();
4870
+ for (const c of clips) {
4871
+ this._maybeRegisterSpectrogramClipAudio(trackId, c);
4872
+ }
4139
4873
  const engine = await this._ensureEngine();
4140
4874
  engine.setTracks([...this._engineTracks.values()]);
4141
4875
  this.dispatchEvent(
@@ -4243,7 +4977,11 @@ var DawEditorElement = class extends LitElement9 {
4243
4977
  nextTracks.set(track.id, track);
4244
4978
  }
4245
4979
  this._engineTracks = nextTracks;
4246
- syncPeaksForChangedClips(this, engineState.tracks);
4980
+ const audioTracks = engineState.tracks.filter((t) => {
4981
+ const desc = this._tracks.get(t.id);
4982
+ return desc?.renderMode !== "piano-roll";
4983
+ });
4984
+ syncPeaksForChangedClips(this, audioTracks);
4247
4985
  }
4248
4986
  });
4249
4987
  engine.on("pause", () => {
@@ -4318,7 +5056,17 @@ var DawEditorElement = class extends LitElement9 {
4318
5056
  if (config.pan !== void 0) trackEl.pan = config.pan;
4319
5057
  if (config.muted) trackEl.setAttribute("muted", "");
4320
5058
  if (config.soloed) trackEl.setAttribute("soloed", "");
4321
- for (const clipConfig of config.clips ?? []) {
5059
+ const renderMode = config.renderMode ?? (config.midi ? "piano-roll" : void 0);
5060
+ if (renderMode !== void 0) trackEl.setAttribute("render-mode", renderMode);
5061
+ const clipConfigs = [...config.clips ?? []];
5062
+ if (config.midi) {
5063
+ clipConfigs.push({
5064
+ midiNotes: config.midi.notes,
5065
+ midiChannel: config.midi.channel,
5066
+ midiProgram: config.midi.program
5067
+ });
5068
+ }
5069
+ for (const clipConfig of clipConfigs) {
4322
5070
  trackEl.appendChild(this._buildClipElement(clipConfig));
4323
5071
  }
4324
5072
  return this._awaitId(
@@ -4364,6 +5112,7 @@ var DawEditorElement = class extends LitElement9 {
4364
5112
  if (partial.soloed) trackEl.setAttribute("soloed", "");
4365
5113
  else trackEl.removeAttribute("soloed");
4366
5114
  }
5115
+ if (partial.renderMode !== void 0) trackEl.setAttribute("render-mode", partial.renderMode);
4367
5116
  return;
4368
5117
  }
4369
5118
  const oldDesc = this._tracks.get(trackId);
@@ -4374,7 +5123,8 @@ var DawEditorElement = class extends LitElement9 {
4374
5123
  ...partial.volume !== void 0 && { volume: partial.volume },
4375
5124
  ...partial.pan !== void 0 && { pan: partial.pan },
4376
5125
  ...partial.muted !== void 0 && { muted: partial.muted },
4377
- ...partial.soloed !== void 0 && { soloed: partial.soloed }
5126
+ ...partial.soloed !== void 0 && { soloed: partial.soloed },
5127
+ ...partial.renderMode !== void 0 && { renderMode: partial.renderMode }
4378
5128
  };
4379
5129
  this._tracks = new Map(this._tracks).set(trackId, newDesc);
4380
5130
  if (this._engine) {
@@ -4509,6 +5259,11 @@ var DawEditorElement = class extends LitElement9 {
4509
5259
  if (config.fadeIn !== void 0) clipEl.fadeIn = config.fadeIn;
4510
5260
  if (config.fadeOut !== void 0) clipEl.fadeOut = config.fadeOut;
4511
5261
  if (config.fadeType !== void 0) clipEl.setAttribute("fade-type", config.fadeType);
5262
+ if (config.midiNotes !== void 0) clipEl.midiNotes = config.midiNotes;
5263
+ if (config.midiChannel !== void 0)
5264
+ clipEl.setAttribute("midi-channel", String(config.midiChannel));
5265
+ if (config.midiProgram !== void 0)
5266
+ clipEl.setAttribute("midi-program", String(config.midiProgram));
4512
5267
  return clipEl;
4513
5268
  }
4514
5269
  // --- Playback ---
@@ -4544,7 +5299,9 @@ var DawEditorElement = class extends LitElement9 {
4544
5299
  }
4545
5300
  /** Toggle between play and pause. */
4546
5301
  togglePlayPause() {
4547
- if (this._isPlaying) {
5302
+ if (this.isRecording) {
5303
+ this.togglePauseRecording();
5304
+ } else if (this._isPlaying) {
4548
5305
  this.pause();
4549
5306
  } else {
4550
5307
  this.play();
@@ -4618,14 +5375,41 @@ var DawEditorElement = class extends LitElement9 {
4618
5375
  get isRecording() {
4619
5376
  return this._recordingController.isRecording;
4620
5377
  }
5378
+ get isRecordingPaused() {
5379
+ return this._recordingController.isPaused;
5380
+ }
4621
5381
  pauseRecording() {
4622
5382
  this._recordingController.pauseRecording();
4623
5383
  }
4624
5384
  resumeRecording() {
4625
5385
  this._recordingController.resumeRecording();
5386
+ this._wasPlayingDuringRecording = false;
5387
+ }
5388
+ /**
5389
+ * Audacity-style pause toggle for active recordings: pauses both the
5390
+ * worklet capture and (if running) the playback Transport. On resume,
5391
+ * Transport restarts only if it was running before — non-overdub
5392
+ * recordings stay silent on resume.
5393
+ */
5394
+ togglePauseRecording() {
5395
+ if (!this.isRecording) return;
5396
+ if (this.isRecordingPaused) {
5397
+ const wasPlaying = this._wasPlayingDuringRecording;
5398
+ this.resumeRecording();
5399
+ if (wasPlaying) {
5400
+ void this.play(this.currentTime);
5401
+ }
5402
+ } else {
5403
+ this.pauseRecording();
5404
+ if (this._isPlaying) {
5405
+ this._wasPlayingDuringRecording = true;
5406
+ this.pause();
5407
+ }
5408
+ }
4626
5409
  }
4627
5410
  stopRecording() {
4628
- this._recordingController.stopRecording();
5411
+ this._wasPlayingDuringRecording = false;
5412
+ return this._recordingController.stopRecording();
4629
5413
  }
4630
5414
  _addRecordedClip(trackId, buf, startSample, durSamples, offsetSamples = 0) {
4631
5415
  addRecordedClip(this, trackId, buf, startSample, durSamples, offsetSamples);
@@ -4661,7 +5445,7 @@ var DawEditorElement = class extends LitElement9 {
4661
5445
  const w = Math.floor(audibleSamples / renderSpp);
4662
5446
  return rs.peaks.map((chPeaks, ch) => {
4663
5447
  const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
4664
- return html8`
5448
+ return html9`
4665
5449
  <daw-waveform
4666
5450
  data-recording-track=${trackId}
4667
5451
  data-recording-channel=${ch}
@@ -4758,11 +5542,11 @@ var DawEditorElement = class extends LitElement9 {
4758
5542
  trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
4759
5543
  };
4760
5544
  });
4761
- return html8`
4762
- ${orderedTracks.length > 0 || this.indefinitePlayback ? html8`<div class="controls-column">
4763
- ${this.timescale ? html8`<div style="height: 30px;"></div>` : ""}
5545
+ return html9`
5546
+ ${orderedTracks.length > 0 || this.indefinitePlayback ? html9`<div class="controls-column">
5547
+ ${this.timescale ? html9`<div style="height: 30px;"></div>` : ""}
4764
5548
  ${orderedTracks.map(
4765
- (t) => html8`
5549
+ (t) => html9`
4766
5550
  <daw-track-controls
4767
5551
  style="height: ${t.trackHeight}px;"
4768
5552
  .trackId=${t.trackId}
@@ -4785,7 +5569,7 @@ var DawEditorElement = class extends LitElement9 {
4785
5569
  @dragleave=${this._onDragLeave}
4786
5570
  @drop=${this._onDrop}
4787
5571
  >
4788
- ${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? html8`<daw-ruler
5572
+ ${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? html9`<daw-ruler
4789
5573
  .samplesPerPixel=${spp}
4790
5574
  .sampleRate=${this.effectiveSampleRate}
4791
5575
  .duration=${this._duration}
@@ -4795,7 +5579,7 @@ var DawEditorElement = class extends LitElement9 {
4795
5579
  .ppqn=${this.ppqn}
4796
5580
  .totalWidth=${this._totalWidth}
4797
5581
  ></daw-ruler>` : ""}
4798
- ${this.scaleMode === "beats" ? html8`<daw-grid
5582
+ ${this.scaleMode === "beats" ? html9`<daw-grid
4799
5583
  style="top: ${this.timescale ? 30 : 0}px;"
4800
5584
  .ticksPerPixel=${this.ticksPerPixel}
4801
5585
  .meterEntries=${this._meterEntries}
@@ -4805,11 +5589,11 @@ var DawEditorElement = class extends LitElement9 {
4805
5589
  .length=${this._totalWidth}
4806
5590
  .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
4807
5591
  ></daw-grid>` : ""}
4808
- ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? html8`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
5592
+ ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? html9`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
4809
5593
  <daw-playhead></daw-playhead>` : ""}
4810
5594
  ${orderedTracks.map((t) => {
4811
5595
  const channelHeight = this.waveHeight;
4812
- return html8`
5596
+ return html9`
4813
5597
  <div
4814
5598
  class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
4815
5599
  style="height: ${t.trackHeight}px;"
@@ -4872,12 +5656,12 @@ var DawEditorElement = class extends LitElement9 {
4872
5656
  const channels = segmentChannels ?? peakData?.data ?? [new Int16Array(0)];
4873
5657
  const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
4874
5658
  const chH = this.waveHeight;
4875
- return html8` <div
5659
+ return html9` <div
4876
5660
  class="clip-container"
4877
5661
  style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
4878
5662
  data-clip-id=${clip.id}
4879
5663
  >
4880
- ${hdrH > 0 ? html8`<div
5664
+ ${hdrH > 0 ? html9`<div
4881
5665
  class="clip-header"
4882
5666
  data-clip-id=${clip.id}
4883
5667
  data-track-id=${t.trackId}
@@ -4885,21 +5669,48 @@ var DawEditorElement = class extends LitElement9 {
4885
5669
  >
4886
5670
  <span>${clip.name || t.descriptor?.name || ""}</span>
4887
5671
  </div>` : ""}
4888
- ${channels.map(
4889
- (chPeaks, chIdx) => html8` <daw-waveform
4890
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
4891
- .peaks=${chPeaks}
5672
+ ${t.descriptor?.renderMode === "piano-roll" ? html9`<daw-piano-roll
5673
+ style="position:absolute;left:0;top:${hdrH}px;"
5674
+ .midiNotes=${clip.midiNotes ?? []}
4892
5675
  .length=${peakData?.length ?? width}
4893
- .waveHeight=${chH}
4894
- .barWidth=${this.barWidth}
4895
- .barGap=${this.barGap}
5676
+ .waveHeight=${chH * channels.length}
5677
+ .samplesPerPixel=${this._renderSpp}
5678
+ .sampleRate=${this.effectiveSampleRate}
5679
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
4896
5680
  .visibleStart=${this._viewport.visibleStart}
4897
5681
  .visibleEnd=${this._viewport.visibleEnd}
4898
5682
  .originX=${clipLeft}
4899
- .segments=${clipSegments}
4900
- ></daw-waveform>`
5683
+ ?selected=${t.trackId === this._selectedTrackId}
5684
+ ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
5685
+ (_chPeaks, chIdx) => html9`<daw-spectrogram
5686
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
5687
+ .clipId=${clip.id}
5688
+ .trackId=${t.trackId}
5689
+ .channelIndex=${chIdx}
5690
+ .length=${peakData?.length ?? width}
5691
+ .waveHeight=${chH}
5692
+ .samplesPerPixel=${this._renderSpp}
5693
+ .sampleRate=${this.effectiveSampleRate}
5694
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5695
+ .visibleStart=${this._viewport.visibleStart}
5696
+ .visibleEnd=${this._viewport.visibleEnd}
5697
+ .originX=${clipLeft}
5698
+ ></daw-spectrogram>`
5699
+ ) : channels.map(
5700
+ (chPeaks, chIdx) => html9` <daw-waveform
5701
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5702
+ .peaks=${chPeaks}
5703
+ .length=${peakData?.length ?? width}
5704
+ .waveHeight=${chH}
5705
+ .barWidth=${this.barWidth}
5706
+ .barGap=${this.barGap}
5707
+ .visibleStart=${this._viewport.visibleStart}
5708
+ .visibleEnd=${this._viewport.visibleEnd}
5709
+ .originX=${clipLeft}
5710
+ .segments=${clipSegments}
5711
+ ></daw-waveform>`
4901
5712
  )}
4902
- ${this.interactiveClips ? html8` <div
5713
+ ${this.interactiveClips ? html9` <div
4903
5714
  class="clip-boundary"
4904
5715
  data-boundary-edge="left"
4905
5716
  data-clip-id=${clip.id}
@@ -4925,7 +5736,7 @@ var DawEditorElement = class extends LitElement9 {
4925
5736
  };
4926
5737
  DawEditorElement.styles = [
4927
5738
  hostStyles,
4928
- css8`
5739
+ css9`
4929
5740
  :host {
4930
5741
  display: flex;
4931
5742
  position: relative;
@@ -4973,64 +5784,70 @@ DawEditorElement.styles = [
4973
5784
  ];
4974
5785
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
4975
5786
  __decorateClass([
4976
- property7({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
5787
+ property8({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
4977
5788
  ], DawEditorElement.prototype, "samplesPerPixel", 1);
4978
5789
  __decorateClass([
4979
- property7({ type: Number, attribute: "wave-height" })
5790
+ property8({ type: Number, attribute: "wave-height" })
4980
5791
  ], DawEditorElement.prototype, "waveHeight", 2);
4981
5792
  __decorateClass([
4982
- property7({ type: Boolean })
5793
+ property8({ type: Boolean })
4983
5794
  ], DawEditorElement.prototype, "timescale", 2);
4984
5795
  __decorateClass([
4985
- property7({ type: Boolean })
5796
+ property8({ type: Boolean })
4986
5797
  ], DawEditorElement.prototype, "mono", 2);
4987
5798
  __decorateClass([
4988
- property7({ type: Number, attribute: "bar-width" })
5799
+ property8({ type: Number, attribute: "bar-width" })
4989
5800
  ], DawEditorElement.prototype, "barWidth", 2);
4990
5801
  __decorateClass([
4991
- property7({ type: Number, attribute: "bar-gap" })
5802
+ property8({ type: Number, attribute: "bar-gap" })
4992
5803
  ], DawEditorElement.prototype, "barGap", 2);
4993
5804
  __decorateClass([
4994
- property7({ type: Boolean, attribute: "file-drop" })
5805
+ property8({ type: Boolean, attribute: "file-drop" })
4995
5806
  ], DawEditorElement.prototype, "fileDrop", 2);
4996
5807
  __decorateClass([
4997
- property7({ type: Boolean, attribute: "clip-headers" })
5808
+ property8({ type: Boolean, attribute: "clip-headers" })
4998
5809
  ], DawEditorElement.prototype, "clipHeaders", 2);
4999
5810
  __decorateClass([
5000
- property7({ type: Number, attribute: "clip-header-height" })
5811
+ property8({ type: Number, attribute: "clip-header-height" })
5001
5812
  ], DawEditorElement.prototype, "clipHeaderHeight", 2);
5002
5813
  __decorateClass([
5003
- property7({ type: Boolean, attribute: "interactive-clips" })
5814
+ property8({ type: Boolean, attribute: "interactive-clips" })
5004
5815
  ], DawEditorElement.prototype, "interactiveClips", 2);
5005
5816
  __decorateClass([
5006
- property7({ type: Boolean, attribute: "indefinite-playback" })
5817
+ property8({ type: Boolean, attribute: "indefinite-playback" })
5007
5818
  ], DawEditorElement.prototype, "indefinitePlayback", 2);
5008
5819
  __decorateClass([
5009
- property7({ type: String, attribute: "scale-mode" })
5820
+ property8({ attribute: false, noAccessor: true })
5821
+ ], DawEditorElement.prototype, "spectrogramConfig", 1);
5822
+ __decorateClass([
5823
+ property8({ attribute: false, noAccessor: true })
5824
+ ], DawEditorElement.prototype, "spectrogramColorMap", 1);
5825
+ __decorateClass([
5826
+ property8({ type: String, attribute: "scale-mode" })
5010
5827
  ], DawEditorElement.prototype, "scaleMode", 2);
5011
5828
  __decorateClass([
5012
- property7({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
5829
+ property8({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
5013
5830
  ], DawEditorElement.prototype, "ticksPerPixel", 1);
5014
5831
  __decorateClass([
5015
- property7({ type: Number, noAccessor: true })
5832
+ property8({ type: Number, noAccessor: true })
5016
5833
  ], DawEditorElement.prototype, "bpm", 1);
5017
5834
  __decorateClass([
5018
- property7({ attribute: false })
5835
+ property8({ attribute: false })
5019
5836
  ], DawEditorElement.prototype, "timeSignature", 2);
5020
5837
  __decorateClass([
5021
- property7({ attribute: false })
5838
+ property8({ attribute: false })
5022
5839
  ], DawEditorElement.prototype, "meterEntries", 2);
5023
5840
  __decorateClass([
5024
- property7({ type: Number, noAccessor: true })
5841
+ property8({ type: Number, noAccessor: true })
5025
5842
  ], DawEditorElement.prototype, "ppqn", 1);
5026
5843
  __decorateClass([
5027
- property7({ type: String, attribute: "snap-to" })
5844
+ property8({ type: String, attribute: "snap-to" })
5028
5845
  ], DawEditorElement.prototype, "snapTo", 2);
5029
5846
  __decorateClass([
5030
- property7({ attribute: false })
5847
+ property8({ attribute: false })
5031
5848
  ], DawEditorElement.prototype, "secondsToTicks", 2);
5032
5849
  __decorateClass([
5033
- property7({ attribute: false })
5850
+ property8({ attribute: false })
5034
5851
  ], DawEditorElement.prototype, "ticksToSeconds", 2);
5035
5852
  __decorateClass([
5036
5853
  state3()
@@ -5054,18 +5871,18 @@ __decorateClass([
5054
5871
  state3()
5055
5872
  ], DawEditorElement.prototype, "_dragOver", 2);
5056
5873
  __decorateClass([
5057
- property7({ attribute: false })
5874
+ property8({ attribute: false })
5058
5875
  ], DawEditorElement.prototype, "adapter", 1);
5059
5876
  __decorateClass([
5060
- property7({ attribute: "eager-resume" })
5877
+ property8({ attribute: "eager-resume" })
5061
5878
  ], DawEditorElement.prototype, "eagerResume", 2);
5062
5879
  DawEditorElement = __decorateClass([
5063
- customElement11("daw-editor")
5880
+ customElement12("daw-editor")
5064
5881
  ], DawEditorElement);
5065
5882
 
5066
5883
  // src/elements/daw-ruler.ts
5067
- import { LitElement as LitElement10, html as html9, css as css9 } from "lit";
5068
- import { customElement as customElement12, property as property8 } from "lit/decorators.js";
5884
+ import { LitElement as LitElement11, html as html10, css as css10 } from "lit";
5885
+ import { customElement as customElement13, property as property9 } from "lit/decorators.js";
5069
5886
 
5070
5887
  // src/utils/time-format.ts
5071
5888
  function formatTime(milliseconds) {
@@ -5116,8 +5933,8 @@ function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight
5116
5933
  }
5117
5934
 
5118
5935
  // src/elements/daw-ruler.ts
5119
- var MAX_CANVAS_WIDTH3 = 1e3;
5120
- var DawRulerElement = class extends LitElement10 {
5936
+ var MAX_CANVAS_WIDTH4 = 1e3;
5937
+ var DawRulerElement = class extends LitElement11 {
5121
5938
  constructor() {
5122
5939
  super(...arguments);
5123
5940
  this.samplesPerPixel = 1024;
@@ -5161,33 +5978,33 @@ var DawRulerElement = class extends LitElement10 {
5161
5978
  }
5162
5979
  render() {
5163
5980
  const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
5164
- if (widthX <= 0) return html9``;
5165
- const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH3);
5981
+ if (widthX <= 0) return html10``;
5982
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH4);
5166
5983
  const indices = Array.from({ length: totalChunks }, (_, i) => i);
5167
5984
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
5168
5985
  const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
5169
5986
  const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
5170
- return html9`
5987
+ return html10`
5171
5988
  <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
5172
5989
  ${indices.map((i) => {
5173
- const width = Math.min(MAX_CANVAS_WIDTH3, widthX - i * MAX_CANVAS_WIDTH3);
5174
- return html9`
5990
+ const width = Math.min(MAX_CANVAS_WIDTH4, widthX - i * MAX_CANVAS_WIDTH4);
5991
+ return html10`
5175
5992
  <canvas
5176
5993
  data-index=${i}
5177
5994
  width=${width * dpr}
5178
5995
  height=${this.rulerHeight * dpr}
5179
- style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.rulerHeight}px;"
5996
+ style="left: ${i * MAX_CANVAS_WIDTH4}px; width: ${width}px; height: ${this.rulerHeight}px;"
5180
5997
  ></canvas>
5181
5998
  `;
5182
5999
  })}
5183
6000
  ${this.scaleMode === "beats" ? beatsLabels.map(
5184
- (t) => html9`<span
6001
+ (t) => html10`<span
5185
6002
  class="label ${t.pixel > 0 ? "centered" : ""}"
5186
6003
  style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
5187
6004
  >${t.label}</span
5188
6005
  >`
5189
6006
  ) : temporalLabels.map(
5190
- ({ pix, text }) => html9`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
6007
+ ({ pix, text }) => html10`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
5191
6008
  )}
5192
6009
  </div>
5193
6010
  `;
@@ -5205,8 +6022,8 @@ var DawRulerElement = class extends LitElement10 {
5205
6022
  const idx = Number(canvas.dataset.index);
5206
6023
  const ctx = canvas.getContext("2d");
5207
6024
  if (!ctx) continue;
5208
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, widthX - idx * MAX_CANVAS_WIDTH3);
5209
- const globalOffset = idx * MAX_CANVAS_WIDTH3;
6025
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH4, widthX - idx * MAX_CANVAS_WIDTH4);
6026
+ const globalOffset = idx * MAX_CANVAS_WIDTH4;
5210
6027
  ctx.resetTransform();
5211
6028
  ctx.clearRect(0, 0, canvas.width, canvas.height);
5212
6029
  ctx.scale(dpr, dpr);
@@ -5238,7 +6055,7 @@ var DawRulerElement = class extends LitElement10 {
5238
6055
  }
5239
6056
  }
5240
6057
  };
5241
- DawRulerElement.styles = css9`
6058
+ DawRulerElement.styles = css10`
5242
6059
  :host {
5243
6060
  display: block;
5244
6061
  position: relative;
@@ -5264,40 +6081,40 @@ DawRulerElement.styles = css9`
5264
6081
  }
5265
6082
  `;
5266
6083
  __decorateClass([
5267
- property8({ type: Number, attribute: false })
6084
+ property9({ type: Number, attribute: false })
5268
6085
  ], DawRulerElement.prototype, "samplesPerPixel", 2);
5269
6086
  __decorateClass([
5270
- property8({ type: Number, attribute: false })
6087
+ property9({ type: Number, attribute: false })
5271
6088
  ], DawRulerElement.prototype, "sampleRate", 2);
5272
6089
  __decorateClass([
5273
- property8({ type: Number, attribute: false })
6090
+ property9({ type: Number, attribute: false })
5274
6091
  ], DawRulerElement.prototype, "duration", 2);
5275
6092
  __decorateClass([
5276
- property8({ type: Number, attribute: false })
6093
+ property9({ type: Number, attribute: false })
5277
6094
  ], DawRulerElement.prototype, "rulerHeight", 2);
5278
6095
  __decorateClass([
5279
- property8({ type: String, attribute: false })
6096
+ property9({ type: String, attribute: false })
5280
6097
  ], DawRulerElement.prototype, "scaleMode", 2);
5281
6098
  __decorateClass([
5282
- property8({ type: Number, attribute: false })
6099
+ property9({ type: Number, attribute: false })
5283
6100
  ], DawRulerElement.prototype, "ticksPerPixel", 2);
5284
6101
  __decorateClass([
5285
- property8({ attribute: false })
6102
+ property9({ attribute: false })
5286
6103
  ], DawRulerElement.prototype, "meterEntries", 2);
5287
6104
  __decorateClass([
5288
- property8({ type: Number, attribute: false })
6105
+ property9({ type: Number, attribute: false })
5289
6106
  ], DawRulerElement.prototype, "ppqn", 2);
5290
6107
  __decorateClass([
5291
- property8({ type: Number, attribute: false })
6108
+ property9({ type: Number, attribute: false })
5292
6109
  ], DawRulerElement.prototype, "totalWidth", 2);
5293
6110
  DawRulerElement = __decorateClass([
5294
- customElement12("daw-ruler")
6111
+ customElement13("daw-ruler")
5295
6112
  ], DawRulerElement);
5296
6113
 
5297
6114
  // src/elements/daw-selection.ts
5298
- import { LitElement as LitElement11, html as html10, css as css10 } from "lit";
5299
- import { customElement as customElement13, property as property9 } from "lit/decorators.js";
5300
- var DawSelectionElement = class extends LitElement11 {
6115
+ import { LitElement as LitElement12, html as html11, css as css11 } from "lit";
6116
+ import { customElement as customElement14, property as property10 } from "lit/decorators.js";
6117
+ var DawSelectionElement = class extends LitElement12 {
5301
6118
  constructor() {
5302
6119
  super(...arguments);
5303
6120
  this.startPx = 0;
@@ -5306,11 +6123,11 @@ var DawSelectionElement = class extends LitElement11 {
5306
6123
  render() {
5307
6124
  const left = Math.min(this.startPx, this.endPx);
5308
6125
  const width = Math.abs(this.endPx - this.startPx);
5309
- if (width === 0) return html10``;
5310
- return html10`<div style="left: ${left}px; width: ${width}px;"></div>`;
6126
+ if (width === 0) return html11``;
6127
+ return html11`<div style="left: ${left}px; width: ${width}px;"></div>`;
5311
6128
  }
5312
6129
  };
5313
- DawSelectionElement.styles = css10`
6130
+ DawSelectionElement.styles = css11`
5314
6131
  :host {
5315
6132
  position: absolute;
5316
6133
  top: 0;
@@ -5327,18 +6144,18 @@ DawSelectionElement.styles = css10`
5327
6144
  }
5328
6145
  `;
5329
6146
  __decorateClass([
5330
- property9({ type: Number, attribute: false })
6147
+ property10({ type: Number, attribute: false })
5331
6148
  ], DawSelectionElement.prototype, "startPx", 2);
5332
6149
  __decorateClass([
5333
- property9({ type: Number, attribute: false })
6150
+ property10({ type: Number, attribute: false })
5334
6151
  ], DawSelectionElement.prototype, "endPx", 2);
5335
6152
  DawSelectionElement = __decorateClass([
5336
- customElement13("daw-selection")
6153
+ customElement14("daw-selection")
5337
6154
  ], DawSelectionElement);
5338
6155
 
5339
6156
  // src/elements/daw-record-button.ts
5340
- import { html as html11, css as css11 } from "lit";
5341
- import { customElement as customElement14, state as state4 } from "lit/decorators.js";
6157
+ import { html as html12, css as css12 } from "lit";
6158
+ import { customElement as customElement15, state as state4 } from "lit/decorators.js";
5342
6159
  var DawRecordButtonElement = class extends DawTransportButton {
5343
6160
  constructor() {
5344
6161
  super(...arguments);
@@ -5379,7 +6196,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
5379
6196
  }
5380
6197
  }
5381
6198
  render() {
5382
- return html11`
6199
+ return html12`
5383
6200
  <button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
5384
6201
  <slot>Record</slot>
5385
6202
  </button>
@@ -5399,7 +6216,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
5399
6216
  };
5400
6217
  DawRecordButtonElement.styles = [
5401
6218
  DawTransportButton.styles,
5402
- css11`
6219
+ css12`
5403
6220
  button[data-recording] {
5404
6221
  color: #d08070;
5405
6222
  border-color: #d08070;
@@ -5411,14 +6228,14 @@ __decorateClass([
5411
6228
  state4()
5412
6229
  ], DawRecordButtonElement.prototype, "_isRecording", 2);
5413
6230
  DawRecordButtonElement = __decorateClass([
5414
- customElement14("daw-record-button")
6231
+ customElement15("daw-record-button")
5415
6232
  ], DawRecordButtonElement);
5416
6233
 
5417
6234
  // src/elements/daw-keyboard-shortcuts.ts
5418
- import { LitElement as LitElement12 } from "lit";
5419
- import { customElement as customElement15, property as property10 } from "lit/decorators.js";
6235
+ import { LitElement as LitElement13 } from "lit";
6236
+ import { customElement as customElement16, property as property11 } from "lit/decorators.js";
5420
6237
  import { handleKeyboardEvent } from "@waveform-playlist/core";
5421
- var DawKeyboardShortcutsElement = class extends LitElement12 {
6238
+ var DawKeyboardShortcutsElement = class extends LitElement13 {
5422
6239
  constructor() {
5423
6240
  super(...arguments);
5424
6241
  this.playback = false;
@@ -5572,17 +6389,198 @@ var DawKeyboardShortcutsElement = class extends LitElement12 {
5572
6389
  }
5573
6390
  };
5574
6391
  __decorateClass([
5575
- property10({ type: Boolean })
6392
+ property11({ type: Boolean })
5576
6393
  ], DawKeyboardShortcutsElement.prototype, "playback", 2);
5577
6394
  __decorateClass([
5578
- property10({ type: Boolean })
6395
+ property11({ type: Boolean })
5579
6396
  ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
5580
6397
  __decorateClass([
5581
- property10({ type: Boolean })
6398
+ property11({ type: Boolean })
5582
6399
  ], DawKeyboardShortcutsElement.prototype, "undo", 2);
5583
6400
  DawKeyboardShortcutsElement = __decorateClass([
5584
- customElement15("daw-keyboard-shortcuts")
6401
+ customElement16("daw-keyboard-shortcuts")
5585
6402
  ], DawKeyboardShortcutsElement);
6403
+
6404
+ // src/elements/daw-spectrogram.ts
6405
+ import { LitElement as LitElement14, html as html13, css as css13 } from "lit";
6406
+ import { customElement as customElement17, property as property12 } from "lit/decorators.js";
6407
+ var MAX_CANVAS_WIDTH5 = 1e3;
6408
+ var DawSpectrogramElement = class extends LitElement14 {
6409
+ constructor() {
6410
+ super(...arguments);
6411
+ this.clipId = "";
6412
+ this.trackId = "";
6413
+ this.channelIndex = 0;
6414
+ this.length = 0;
6415
+ this.waveHeight = 128;
6416
+ this._samplesPerPixel = 1024;
6417
+ this._sampleRate = 44100;
6418
+ this.clipOffsetSeconds = 0;
6419
+ this.visibleStart = -Infinity;
6420
+ this.visibleEnd = Infinity;
6421
+ this.originX = 0;
6422
+ this._canvases = [];
6423
+ this._registeredCanvasIds = [];
6424
+ }
6425
+ get samplesPerPixel() {
6426
+ return this._samplesPerPixel;
6427
+ }
6428
+ set samplesPerPixel(value) {
6429
+ if (!Number.isFinite(value) || value <= 0) {
6430
+ console.warn("[dawcore] daw-spectrogram samplesPerPixel " + value + " is invalid \u2014 ignored");
6431
+ return;
6432
+ }
6433
+ const old = this._samplesPerPixel;
6434
+ this._samplesPerPixel = value;
6435
+ this.requestUpdate("samplesPerPixel", old);
6436
+ }
6437
+ get sampleRate() {
6438
+ return this._sampleRate;
6439
+ }
6440
+ set sampleRate(value) {
6441
+ if (!Number.isFinite(value) || value <= 0) {
6442
+ console.warn("[dawcore] daw-spectrogram sampleRate " + value + " is invalid \u2014 ignored");
6443
+ return;
6444
+ }
6445
+ const old = this._sampleRate;
6446
+ this._sampleRate = value;
6447
+ this.requestUpdate("sampleRate", old);
6448
+ }
6449
+ /**
6450
+ * Walk up to the editor host. `closest('daw-editor')` does NOT cross
6451
+ * shadow boundaries — and this element lives inside the editor's shadow
6452
+ * DOM — so use getRootNode().host to step out.
6453
+ */
6454
+ _findHostEditor() {
6455
+ const root = this.getRootNode();
6456
+ const host = root instanceof ShadowRoot ? root.host : null;
6457
+ if (!host) return null;
6458
+ if (host.tagName === "DAW-EDITOR") return host;
6459
+ return host.closest("daw-editor");
6460
+ }
6461
+ willUpdate(changed) {
6462
+ const layoutChanged = changed.has("length") || changed.has("waveHeight") || changed.has("samplesPerPixel") || changed.has("clipId") || changed.has("channelIndex");
6463
+ if (layoutChanged) {
6464
+ this._rebuildChunks();
6465
+ }
6466
+ }
6467
+ _rebuildChunks() {
6468
+ this._unregisterAllCanvases();
6469
+ this._canvases = [];
6470
+ if (this.length <= 0) return;
6471
+ const chunkCount = Math.ceil(this.length / MAX_CANVAS_WIDTH5);
6472
+ for (let i = 0; i < chunkCount; i++) {
6473
+ const widthPx = Math.min(MAX_CANVAS_WIDTH5, this.length - i * MAX_CANVAS_WIDTH5);
6474
+ const canvas = document.createElement("canvas");
6475
+ canvas.style.left = i * MAX_CANVAS_WIDTH5 + "px";
6476
+ canvas.style.width = widthPx + "px";
6477
+ const dpr = window.devicePixelRatio || 1;
6478
+ canvas.width = widthPx * dpr;
6479
+ canvas.height = this.waveHeight * dpr;
6480
+ this._canvases.push(canvas);
6481
+ }
6482
+ }
6483
+ updated(_changed) {
6484
+ if (this._registeredCanvasIds.length === 0 && this._canvases.length > 0) {
6485
+ requestAnimationFrame(() => this._registerCanvases());
6486
+ }
6487
+ }
6488
+ _registerCanvases() {
6489
+ const editor = this._findHostEditor();
6490
+ if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") return;
6491
+ for (let i = 0; i < this._canvases.length; i++) {
6492
+ const canvas = this._canvases[i];
6493
+ const canvasId = this.clipId + "-ch" + this.channelIndex + "-chunk" + i;
6494
+ let offscreen;
6495
+ try {
6496
+ offscreen = canvas.transferControlToOffscreen();
6497
+ } catch (err) {
6498
+ console.warn(
6499
+ "[dawcore] daw-spectrogram transferControlToOffscreen failed for " + canvasId + ": " + (err instanceof Error ? err.message : String(err))
6500
+ );
6501
+ continue;
6502
+ }
6503
+ editor._spectrogramRegisterCanvas({
6504
+ canvasId,
6505
+ canvas: offscreen,
6506
+ clipId: this.clipId,
6507
+ trackId: this.trackId,
6508
+ channelIndex: this.channelIndex,
6509
+ chunkIndex: i,
6510
+ globalPixelOffset: this.originX + i * MAX_CANVAS_WIDTH5,
6511
+ widthPx: parseFloat(canvas.style.width),
6512
+ heightPx: this.waveHeight
6513
+ });
6514
+ this._registeredCanvasIds.push(canvasId);
6515
+ }
6516
+ }
6517
+ _unregisterAllCanvases() {
6518
+ const editor = this._findHostEditor();
6519
+ if (editor && typeof editor._spectrogramUnregisterCanvas === "function") {
6520
+ for (const id of this._registeredCanvasIds) {
6521
+ editor._spectrogramUnregisterCanvas(id);
6522
+ }
6523
+ }
6524
+ this._registeredCanvasIds = [];
6525
+ }
6526
+ disconnectedCallback() {
6527
+ super.disconnectedCallback();
6528
+ this._unregisterAllCanvases();
6529
+ }
6530
+ render() {
6531
+ return html13`${this._canvases.map((c) => c)}`;
6532
+ }
6533
+ };
6534
+ DawSpectrogramElement.styles = css13`
6535
+ :host {
6536
+ display: block;
6537
+ position: relative;
6538
+ background: var(--daw-spectrogram-background, #000);
6539
+ }
6540
+ canvas {
6541
+ position: absolute;
6542
+ top: 0;
6543
+ left: 0;
6544
+ height: 100%;
6545
+ pointer-events: none;
6546
+ }
6547
+ `;
6548
+ __decorateClass([
6549
+ property12({ attribute: false })
6550
+ ], DawSpectrogramElement.prototype, "clipId", 2);
6551
+ __decorateClass([
6552
+ property12({ attribute: false })
6553
+ ], DawSpectrogramElement.prototype, "trackId", 2);
6554
+ __decorateClass([
6555
+ property12({ type: Number, attribute: false })
6556
+ ], DawSpectrogramElement.prototype, "channelIndex", 2);
6557
+ __decorateClass([
6558
+ property12({ type: Number, attribute: false })
6559
+ ], DawSpectrogramElement.prototype, "length", 2);
6560
+ __decorateClass([
6561
+ property12({ type: Number, attribute: false })
6562
+ ], DawSpectrogramElement.prototype, "waveHeight", 2);
6563
+ __decorateClass([
6564
+ property12({ type: Number, attribute: false, noAccessor: true })
6565
+ ], DawSpectrogramElement.prototype, "samplesPerPixel", 1);
6566
+ __decorateClass([
6567
+ property12({ type: Number, attribute: false, noAccessor: true })
6568
+ ], DawSpectrogramElement.prototype, "sampleRate", 1);
6569
+ __decorateClass([
6570
+ property12({ type: Number, attribute: false })
6571
+ ], DawSpectrogramElement.prototype, "clipOffsetSeconds", 2);
6572
+ __decorateClass([
6573
+ property12({ type: Number, attribute: false })
6574
+ ], DawSpectrogramElement.prototype, "visibleStart", 2);
6575
+ __decorateClass([
6576
+ property12({ type: Number, attribute: false })
6577
+ ], DawSpectrogramElement.prototype, "visibleEnd", 2);
6578
+ __decorateClass([
6579
+ property12({ type: Number, attribute: false })
6580
+ ], DawSpectrogramElement.prototype, "originX", 2);
6581
+ DawSpectrogramElement = __decorateClass([
6582
+ customElement17("daw-spectrogram")
6583
+ ], DawSpectrogramElement);
5586
6584
  export {
5587
6585
  AudioResumeController,
5588
6586
  ClipPointerHandler,
@@ -5591,11 +6589,13 @@ export {
5591
6589
  DawGridElement,
5592
6590
  DawKeyboardShortcutsElement,
5593
6591
  DawPauseButtonElement,
6592
+ DawPianoRollElement,
5594
6593
  DawPlayButtonElement,
5595
6594
  DawPlayheadElement,
5596
6595
  DawRecordButtonElement,
5597
6596
  DawRulerElement,
5598
6597
  DawSelectionElement,
6598
+ DawSpectrogramElement,
5599
6599
  DawStopButtonElement,
5600
6600
  DawTrackControlsElement,
5601
6601
  DawTrackElement,
@@ -5603,6 +6603,7 @@ export {
5603
6603
  DawTransportElement,
5604
6604
  DawWaveformElement,
5605
6605
  RecordingController,
6606
+ SpectrogramController,
5606
6607
  isDomClip,
5607
6608
  splitAtPlayhead
5608
6609
  };