@dawcore/components 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  DawRecordButtonElement: () => DawRecordButtonElement,
52
52
  DawRulerElement: () => DawRulerElement,
53
53
  DawSelectionElement: () => DawSelectionElement,
54
+ DawSpectrogramElement: () => DawSpectrogramElement,
54
55
  DawStopButtonElement: () => DawStopButtonElement,
55
56
  DawTrackControlsElement: () => DawTrackControlsElement,
56
57
  DawTrackElement: () => DawTrackElement,
@@ -58,6 +59,7 @@ __export(index_exports, {
58
59
  DawTransportElement: () => DawTransportElement,
59
60
  DawWaveformElement: () => DawWaveformElement,
60
61
  RecordingController: () => RecordingController,
62
+ SpectrogramController: () => SpectrogramController,
61
63
  isDomClip: () => isDomClip,
62
64
  splitAtPlayhead: () => splitAtPlayhead
63
65
  });
@@ -229,6 +231,7 @@ var DawTrackElement = class extends import_lit2.LitElement {
229
231
  this.muted = false;
230
232
  this.soloed = false;
231
233
  this.renderMode = "waveform";
234
+ this.spectrogramConfig = null;
232
235
  this.trackId = crypto.randomUUID();
233
236
  // Track removal is detected by the editor's MutationObserver,
234
237
  // not by dispatching from disconnectedCallback (detached elements
@@ -256,7 +259,16 @@ var DawTrackElement = class extends import_lit2.LitElement {
256
259
  this._hasRendered = true;
257
260
  return;
258
261
  }
259
- const trackProps = ["volume", "pan", "muted", "soloed", "src", "name", "renderMode"];
262
+ const trackProps = [
263
+ "volume",
264
+ "pan",
265
+ "muted",
266
+ "soloed",
267
+ "src",
268
+ "name",
269
+ "renderMode",
270
+ "spectrogramConfig"
271
+ ];
260
272
  const hasTrackChange = trackProps.some((p) => changed.has(p));
261
273
  if (hasTrackChange) {
262
274
  this.dispatchEvent(
@@ -290,6 +302,9 @@ __decorateClass([
290
302
  __decorateClass([
291
303
  (0, import_decorators2.property)({ attribute: "render-mode" })
292
304
  ], DawTrackElement.prototype, "renderMode", 2);
305
+ __decorateClass([
306
+ (0, import_decorators2.property)({ attribute: false })
307
+ ], DawTrackElement.prototype, "spectrogramConfig", 2);
293
308
  DawTrackElement = __decorateClass([
294
309
  (0, import_decorators2.customElement)("daw-track")
295
310
  ], DawTrackElement);
@@ -2757,6 +2772,121 @@ var RecordingController = class {
2757
2772
  }
2758
2773
  };
2759
2774
 
2775
+ // src/controllers/spectrogram-controller.ts
2776
+ var import_spectrogram = require("@dawcore/spectrogram");
2777
+ var LIBRARY_DEFAULTS = {
2778
+ fftSize: 2048,
2779
+ windowFunction: "hann",
2780
+ frequencyScale: "mel",
2781
+ minFrequency: 0,
2782
+ gainDb: 20,
2783
+ rangeDb: 80
2784
+ };
2785
+ var LIBRARY_DEFAULT_COLOR_MAP = "viridis";
2786
+ var SpectrogramController = class {
2787
+ constructor(host, workerFactory) {
2788
+ this.orchestrator = null;
2789
+ this.editorConfig = null;
2790
+ this.editorColorMap = null;
2791
+ this.trackConfigs = /* @__PURE__ */ new Map();
2792
+ this.trackColorMaps = /* @__PURE__ */ new Map();
2793
+ this.host = host;
2794
+ this.workerFactory = workerFactory;
2795
+ this.host.addController(this);
2796
+ }
2797
+ hostConnected() {
2798
+ }
2799
+ hostDisconnected() {
2800
+ this.dispose();
2801
+ }
2802
+ setEditorConfig(config) {
2803
+ this.editorConfig = config;
2804
+ this.reapply();
2805
+ }
2806
+ setEditorColorMap(colorMap) {
2807
+ this.editorColorMap = colorMap;
2808
+ this.reapply();
2809
+ }
2810
+ setTrackConfig(trackId, config) {
2811
+ if (config === null) {
2812
+ this.trackConfigs.delete(trackId);
2813
+ } else {
2814
+ this.trackConfigs.set(trackId, config);
2815
+ }
2816
+ this.reapply();
2817
+ }
2818
+ setTrackColorMap(trackId, colorMap) {
2819
+ if (colorMap === null) {
2820
+ this.trackColorMaps.delete(trackId);
2821
+ } else {
2822
+ this.trackColorMaps.set(trackId, colorMap);
2823
+ }
2824
+ this.reapply();
2825
+ }
2826
+ registerClipAudio(reg) {
2827
+ this.ensureOrchestrator().registerClip(reg);
2828
+ }
2829
+ unregisterClipAudio(clipId) {
2830
+ this.orchestrator?.unregisterClip(clipId);
2831
+ }
2832
+ registerCanvas(reg) {
2833
+ this.ensureOrchestrator().registerCanvas(reg);
2834
+ }
2835
+ unregisterCanvas(canvasId) {
2836
+ this.orchestrator?.unregisterCanvas(canvasId);
2837
+ }
2838
+ setViewport(state5) {
2839
+ this.orchestrator?.setViewport(state5);
2840
+ }
2841
+ dispose() {
2842
+ if (this.orchestrator) {
2843
+ this.orchestrator.dispose();
2844
+ this.orchestrator = null;
2845
+ }
2846
+ }
2847
+ ensureOrchestrator() {
2848
+ if (!this.orchestrator) {
2849
+ this.orchestrator = new import_spectrogram.SpectrogramOrchestrator({
2850
+ workerFactory: this.workerFactory,
2851
+ workerPoolSize: 2,
2852
+ config: this.mergedConfig(),
2853
+ colorMap: this.mergedColorMap()
2854
+ });
2855
+ this.orchestrator.addEventListener("viewport-ready", (e) => {
2856
+ const detail = e.detail;
2857
+ this.host.dispatchEvent(
2858
+ new CustomEvent("daw-spectrogram-ready", {
2859
+ detail,
2860
+ bubbles: true,
2861
+ composed: true
2862
+ })
2863
+ );
2864
+ });
2865
+ this.reapply();
2866
+ }
2867
+ return this.orchestrator;
2868
+ }
2869
+ reapply() {
2870
+ if (!this.orchestrator) return;
2871
+ this.orchestrator.setConfig(this.mergedConfig());
2872
+ this.orchestrator.setColorMap(this.mergedColorMap());
2873
+ }
2874
+ mergedConfig() {
2875
+ let track = null;
2876
+ for (const c of this.trackConfigs.values()) {
2877
+ track = c;
2878
+ break;
2879
+ }
2880
+ return { ...LIBRARY_DEFAULTS, ...this.editorConfig ?? {}, ...track ?? {} };
2881
+ }
2882
+ mergedColorMap() {
2883
+ for (const c of this.trackColorMaps.values()) {
2884
+ return c ?? LIBRARY_DEFAULT_COLOR_MAP;
2885
+ }
2886
+ return this.editorColorMap ?? LIBRARY_DEFAULT_COLOR_MAP;
2887
+ }
2888
+ };
2889
+
2760
2890
  // src/interactions/pointer-handler.ts
2761
2891
  var import_core4 = require("@waveform-playlist/core");
2762
2892
 
@@ -3637,6 +3767,7 @@ async function loadWaveformDataFromUrl(src) {
3637
3767
  }
3638
3768
 
3639
3769
  // src/elements/daw-editor.ts
3770
+ var import_meta = {};
3640
3771
  var NO_ADAPTER_ERROR = "No PlayoutAdapter set on <daw-editor>. Set editor.adapter before use.\n\n // Option 1: Native Web Audio (no Tone.js)\n npm install @dawcore/transport\n import { NativePlayoutAdapter } from '@dawcore/transport';\n editor.adapter = new NativePlayoutAdapter(new AudioContext());\n\n // Option 2: Tone.js (effects, MIDI synths)\n npm install @waveform-playlist/playout\n import { createToneAdapter } from '@waveform-playlist/playout';\n editor.adapter = createToneAdapter();";
3641
3772
  var DawEditorElement = class extends import_lit14.LitElement {
3642
3773
  constructor() {
@@ -3652,6 +3783,8 @@ var DawEditorElement = class extends import_lit14.LitElement {
3652
3783
  this.clipHeaderHeight = 20;
3653
3784
  this.interactiveClips = false;
3654
3785
  this.indefinitePlayback = false;
3786
+ this._spectrogramConfig = null;
3787
+ this._spectrogramColorMap = null;
3655
3788
  this.scaleMode = "temporal";
3656
3789
  this._ticksPerPixel = 24;
3657
3790
  this._bpm = 120;
@@ -3687,6 +3820,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
3687
3820
  this._childObserver = null;
3688
3821
  this._audioResume = new AudioResumeController(this);
3689
3822
  this._recordingController = new RecordingController(this);
3823
+ this._spectrogramController = null;
3690
3824
  this._clipPointer = new ClipPointerHandler(this);
3691
3825
  this._pointer = new PointerHandler(this);
3692
3826
  this._viewport = (() => {
@@ -3694,6 +3828,15 @@ var DawEditorElement = class extends import_lit14.LitElement {
3694
3828
  v.scrollSelector = ".scroll-area";
3695
3829
  return v;
3696
3830
  })();
3831
+ /**
3832
+ * Cache of the last ViewportState forwarded to the spectrogram controller.
3833
+ * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
3834
+ * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
3835
+ * viewport. Skip the cross-controller call when nothing changed.
3836
+ *
3837
+ * The orchestrator dedupes too, but this avoids the call entirely.
3838
+ */
3839
+ this._lastSpectrogramViewport = null;
3697
3840
  // --- Track Events ---
3698
3841
  this._onTrackConnected = (e) => {
3699
3842
  const trackId = e.detail?.trackId;
@@ -3727,6 +3870,26 @@ var DawEditorElement = class extends import_lit14.LitElement {
3727
3870
  if (oldDescriptor?.src !== descriptor.src) {
3728
3871
  this._loadTrack(trackId, descriptor);
3729
3872
  }
3873
+ if (descriptor.renderMode === "spectrogram" && oldDescriptor?.renderMode !== "spectrogram") {
3874
+ const engineTrack = this._engineTracks.get(trackId);
3875
+ if (engineTrack) {
3876
+ for (const clip of engineTrack.clips) {
3877
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
3878
+ }
3879
+ }
3880
+ }
3881
+ if (descriptor.renderMode !== "spectrogram" && oldDescriptor?.renderMode === "spectrogram") {
3882
+ const engineTrack = this._engineTracks.get(trackId);
3883
+ if (engineTrack && this._spectrogramController) {
3884
+ for (const clip of engineTrack.clips) {
3885
+ this._spectrogramController.unregisterClipAudio(clip.id);
3886
+ }
3887
+ }
3888
+ this._disposeSpectrogramControllerIfEmpty();
3889
+ }
3890
+ if (descriptor.spectrogramConfig !== oldDescriptor?.spectrogramConfig) {
3891
+ this._spectrogramController?.setTrackConfig(trackId, descriptor.spectrogramConfig ?? null);
3892
+ }
3730
3893
  };
3731
3894
  this._onTrackControl = (e) => {
3732
3895
  const { trackId, prop, value } = e.detail ?? {};
@@ -3870,6 +4033,72 @@ var DawEditorElement = class extends import_lit14.LitElement {
3870
4033
  this._samplesPerPixel = clamped;
3871
4034
  this.requestUpdate("samplesPerPixel", old);
3872
4035
  }
4036
+ get spectrogramConfig() {
4037
+ return this._spectrogramConfig;
4038
+ }
4039
+ set spectrogramConfig(value) {
4040
+ const old = this._spectrogramConfig;
4041
+ this._spectrogramConfig = value;
4042
+ this._spectrogramController?.setEditorConfig(value);
4043
+ this.requestUpdate("spectrogramConfig", old);
4044
+ }
4045
+ get spectrogramColorMap() {
4046
+ return this._spectrogramColorMap;
4047
+ }
4048
+ set spectrogramColorMap(value) {
4049
+ const old = this._spectrogramColorMap;
4050
+ this._spectrogramColorMap = value;
4051
+ this._spectrogramController?.setEditorColorMap(value);
4052
+ this.requestUpdate("spectrogramColorMap", old);
4053
+ }
4054
+ _ensureSpectrogramController() {
4055
+ if (!this._spectrogramController) {
4056
+ this._spectrogramController = new SpectrogramController(
4057
+ this,
4058
+ () => new Worker(new URL("@dawcore/spectrogram/worker/spectrogram.worker", import_meta.url), {
4059
+ type: "module"
4060
+ })
4061
+ );
4062
+ if (this._spectrogramConfig) {
4063
+ this._spectrogramController.setEditorConfig(this._spectrogramConfig);
4064
+ }
4065
+ if (this._spectrogramColorMap) {
4066
+ this._spectrogramController.setEditorColorMap(this._spectrogramColorMap);
4067
+ }
4068
+ }
4069
+ return this._spectrogramController;
4070
+ }
4071
+ /** Called by <daw-spectrogram> after transferControlToOffscreen. */
4072
+ _spectrogramRegisterCanvas(reg) {
4073
+ this._ensureSpectrogramController().registerCanvas(reg);
4074
+ }
4075
+ /** Called by <daw-spectrogram> on chunk unmount / element disconnect. */
4076
+ _spectrogramUnregisterCanvas(canvasId) {
4077
+ this._spectrogramController?.unregisterCanvas(canvasId);
4078
+ }
4079
+ /**
4080
+ * Push a clip's decoded audio into the spectrogram controller. No-op
4081
+ * unless the track is in spectrogram render-mode and the controller
4082
+ * already exists (it bootstraps from canvas registration).
4083
+ */
4084
+ _maybeRegisterSpectrogramClipAudio(trackId, clip) {
4085
+ const descriptor = this._tracks.get(trackId);
4086
+ if (descriptor?.renderMode !== "spectrogram") return;
4087
+ const buffer = clip.audioBuffer ?? this._clipBuffers.get(clip.id);
4088
+ if (!buffer) return;
4089
+ const channelData = [];
4090
+ for (let i = 0; i < buffer.numberOfChannels; i++) {
4091
+ channelData.push(buffer.getChannelData(i));
4092
+ }
4093
+ this._ensureSpectrogramController().registerClipAudio({
4094
+ clipId: clip.id,
4095
+ trackId,
4096
+ channelData,
4097
+ sampleRate: buffer.sampleRate,
4098
+ durationSamples: clip.durationSamples,
4099
+ offsetSamples: clip.offsetSamples
4100
+ });
4101
+ }
3873
4102
  get ticksPerPixel() {
3874
4103
  return this._ticksPerPixel;
3875
4104
  }
@@ -4110,6 +4339,8 @@ var DawEditorElement = class extends import_lit14.LitElement {
4110
4339
  this._clipOffsets.clear();
4111
4340
  this._peakPipeline.terminate();
4112
4341
  this._minSamplesPerPixel = 0;
4342
+ this._spectrogramController?.dispose();
4343
+ this._spectrogramController = null;
4113
4344
  try {
4114
4345
  this._disposeEngine();
4115
4346
  } catch (err) {
@@ -4138,6 +4369,27 @@ var DawEditorElement = class extends import_lit14.LitElement {
4138
4369
  }
4139
4370
  }
4140
4371
  }
4372
+ updated(_changed) {
4373
+ if (this._spectrogramController) {
4374
+ const vs = this._viewport.visibleStart;
4375
+ const ve = this._viewport.visibleEnd;
4376
+ const spp = this._renderSpp;
4377
+ if (Number.isFinite(vs) && Number.isFinite(ve)) {
4378
+ const prev = this._lastSpectrogramViewport;
4379
+ if (prev && prev.vs === vs && prev.ve === ve && prev.spp === spp) return;
4380
+ this._lastSpectrogramViewport = { vs, ve, spp };
4381
+ const span = ve - vs;
4382
+ const bufferPad = span * 0.25;
4383
+ this._spectrogramController.setViewport({
4384
+ visibleStartPx: vs,
4385
+ visibleEndPx: ve,
4386
+ bufferStartPx: Math.max(0, vs - bufferPad),
4387
+ bufferEndPx: ve + bufferPad,
4388
+ samplesPerPixel: spp
4389
+ });
4390
+ }
4391
+ }
4392
+ }
4141
4393
  _onTrackRemoved(trackId) {
4142
4394
  this._trackElements.delete(trackId);
4143
4395
  const removedTrack = this._engineTracks.get(trackId);
@@ -4147,6 +4399,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4147
4399
  this._clipBuffers.delete(clip.id);
4148
4400
  this._clipOffsets.delete(clip.id);
4149
4401
  nextPeaks.delete(clip.id);
4402
+ this._spectrogramController?.unregisterClipAudio(clip.id);
4150
4403
  }
4151
4404
  this._peaksData = nextPeaks;
4152
4405
  }
@@ -4161,11 +4414,23 @@ var DawEditorElement = class extends import_lit14.LitElement {
4161
4414
  this._engine.removeTrack(trackId);
4162
4415
  }
4163
4416
  this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
4417
+ this._disposeSpectrogramControllerIfEmpty();
4164
4418
  if (nextEngine.size === 0) {
4165
4419
  this._currentTime = 0;
4166
4420
  this._stopPlayhead();
4167
4421
  }
4168
4422
  }
4423
+ /** Drop the controller when no spectrogram tracks remain. */
4424
+ _disposeSpectrogramControllerIfEmpty() {
4425
+ if (!this._spectrogramController) return;
4426
+ const stillNeeded = Array.from(this._tracks.values()).some(
4427
+ (d) => d.renderMode === "spectrogram"
4428
+ );
4429
+ if (!stillNeeded) {
4430
+ this._spectrogramController.dispose();
4431
+ this._spectrogramController = null;
4432
+ }
4433
+ }
4169
4434
  _onClipRemovedFromDom(clipEl) {
4170
4435
  const clipId = clipEl.clipId;
4171
4436
  for (const [trackId, t] of this._engineTracks.entries()) {
@@ -4207,6 +4472,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4207
4472
  });
4208
4473
  }
4209
4474
  this._commitTrackChange(trackId, updatedTrack);
4475
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
4210
4476
  this.dispatchEvent(
4211
4477
  new CustomEvent("daw-clip-ready", {
4212
4478
  bubbles: true,
@@ -4381,6 +4647,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4381
4647
  nextPeaks.delete(clipId);
4382
4648
  this._peaksData = nextPeaks;
4383
4649
  this._clipOffsets.delete(clipId);
4650
+ this._spectrogramController?.unregisterClipAudio(clipId);
4384
4651
  }
4385
4652
  /**
4386
4653
  * Recompute duration and forward an updated track to the engine. Single
@@ -4663,6 +4930,9 @@ var DawEditorElement = class extends import_lit14.LitElement {
4663
4930
  track.id = trackId;
4664
4931
  this._engineTracks = new Map(this._engineTracks).set(trackId, track);
4665
4932
  this._recomputeDuration();
4933
+ for (const c of clips) {
4934
+ this._maybeRegisterSpectrogramClipAudio(trackId, c);
4935
+ }
4666
4936
  const engine = await this._ensureEngine();
4667
4937
  engine.setTracks([...this._engineTracks.values()]);
4668
4938
  this.dispatchEvent(
@@ -5474,19 +5744,34 @@ var DawEditorElement = class extends import_lit14.LitElement {
5474
5744
  .visibleEnd=${this._viewport.visibleEnd}
5475
5745
  .originX=${clipLeft}
5476
5746
  ?selected=${t.trackId === this._selectedTrackId}
5477
- ></daw-piano-roll>` : channels.map(
5747
+ ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
5748
+ (_chPeaks, chIdx) => import_lit14.html`<daw-spectrogram
5749
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
5750
+ .clipId=${clip.id}
5751
+ .trackId=${t.trackId}
5752
+ .channelIndex=${chIdx}
5753
+ .length=${peakData?.length ?? width}
5754
+ .waveHeight=${chH}
5755
+ .samplesPerPixel=${this._renderSpp}
5756
+ .sampleRate=${this.effectiveSampleRate}
5757
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5758
+ .visibleStart=${this._viewport.visibleStart}
5759
+ .visibleEnd=${this._viewport.visibleEnd}
5760
+ .originX=${clipLeft}
5761
+ ></daw-spectrogram>`
5762
+ ) : channels.map(
5478
5763
  (chPeaks, chIdx) => import_lit14.html` <daw-waveform
5479
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5480
- .peaks=${chPeaks}
5481
- .length=${peakData?.length ?? width}
5482
- .waveHeight=${chH}
5483
- .barWidth=${this.barWidth}
5484
- .barGap=${this.barGap}
5485
- .visibleStart=${this._viewport.visibleStart}
5486
- .visibleEnd=${this._viewport.visibleEnd}
5487
- .originX=${clipLeft}
5488
- .segments=${clipSegments}
5489
- ></daw-waveform>`
5764
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5765
+ .peaks=${chPeaks}
5766
+ .length=${peakData?.length ?? width}
5767
+ .waveHeight=${chH}
5768
+ .barWidth=${this.barWidth}
5769
+ .barGap=${this.barGap}
5770
+ .visibleStart=${this._viewport.visibleStart}
5771
+ .visibleEnd=${this._viewport.visibleEnd}
5772
+ .originX=${clipLeft}
5773
+ .segments=${clipSegments}
5774
+ ></daw-waveform>`
5490
5775
  )}
5491
5776
  ${this.interactiveClips ? import_lit14.html` <div
5492
5777
  class="clip-boundary"
@@ -5594,6 +5879,12 @@ __decorateClass([
5594
5879
  __decorateClass([
5595
5880
  (0, import_decorators12.property)({ type: Boolean, attribute: "indefinite-playback" })
5596
5881
  ], DawEditorElement.prototype, "indefinitePlayback", 2);
5882
+ __decorateClass([
5883
+ (0, import_decorators12.property)({ attribute: false, noAccessor: true })
5884
+ ], DawEditorElement.prototype, "spectrogramConfig", 1);
5885
+ __decorateClass([
5886
+ (0, import_decorators12.property)({ attribute: false, noAccessor: true })
5887
+ ], DawEditorElement.prototype, "spectrogramColorMap", 1);
5597
5888
  __decorateClass([
5598
5889
  (0, import_decorators12.property)({ type: String, attribute: "scale-mode" })
5599
5890
  ], DawEditorElement.prototype, "scaleMode", 2);
@@ -6172,6 +6463,187 @@ __decorateClass([
6172
6463
  DawKeyboardShortcutsElement = __decorateClass([
6173
6464
  (0, import_decorators16.customElement)("daw-keyboard-shortcuts")
6174
6465
  ], DawKeyboardShortcutsElement);
6466
+
6467
+ // src/elements/daw-spectrogram.ts
6468
+ var import_lit19 = require("lit");
6469
+ var import_decorators17 = require("lit/decorators.js");
6470
+ var MAX_CANVAS_WIDTH5 = 1e3;
6471
+ var DawSpectrogramElement = class extends import_lit19.LitElement {
6472
+ constructor() {
6473
+ super(...arguments);
6474
+ this.clipId = "";
6475
+ this.trackId = "";
6476
+ this.channelIndex = 0;
6477
+ this.length = 0;
6478
+ this.waveHeight = 128;
6479
+ this._samplesPerPixel = 1024;
6480
+ this._sampleRate = 44100;
6481
+ this.clipOffsetSeconds = 0;
6482
+ this.visibleStart = -Infinity;
6483
+ this.visibleEnd = Infinity;
6484
+ this.originX = 0;
6485
+ this._canvases = [];
6486
+ this._registeredCanvasIds = [];
6487
+ }
6488
+ get samplesPerPixel() {
6489
+ return this._samplesPerPixel;
6490
+ }
6491
+ set samplesPerPixel(value) {
6492
+ if (!Number.isFinite(value) || value <= 0) {
6493
+ console.warn("[dawcore] daw-spectrogram samplesPerPixel " + value + " is invalid \u2014 ignored");
6494
+ return;
6495
+ }
6496
+ const old = this._samplesPerPixel;
6497
+ this._samplesPerPixel = value;
6498
+ this.requestUpdate("samplesPerPixel", old);
6499
+ }
6500
+ get sampleRate() {
6501
+ return this._sampleRate;
6502
+ }
6503
+ set sampleRate(value) {
6504
+ if (!Number.isFinite(value) || value <= 0) {
6505
+ console.warn("[dawcore] daw-spectrogram sampleRate " + value + " is invalid \u2014 ignored");
6506
+ return;
6507
+ }
6508
+ const old = this._sampleRate;
6509
+ this._sampleRate = value;
6510
+ this.requestUpdate("sampleRate", old);
6511
+ }
6512
+ /**
6513
+ * Walk up to the editor host. `closest('daw-editor')` does NOT cross
6514
+ * shadow boundaries — and this element lives inside the editor's shadow
6515
+ * DOM — so use getRootNode().host to step out.
6516
+ */
6517
+ _findHostEditor() {
6518
+ const root = this.getRootNode();
6519
+ const host = root instanceof ShadowRoot ? root.host : null;
6520
+ if (!host) return null;
6521
+ if (host.tagName === "DAW-EDITOR") return host;
6522
+ return host.closest("daw-editor");
6523
+ }
6524
+ willUpdate(changed) {
6525
+ const layoutChanged = changed.has("length") || changed.has("waveHeight") || changed.has("samplesPerPixel") || changed.has("clipId") || changed.has("channelIndex");
6526
+ if (layoutChanged) {
6527
+ this._rebuildChunks();
6528
+ }
6529
+ }
6530
+ _rebuildChunks() {
6531
+ this._unregisterAllCanvases();
6532
+ this._canvases = [];
6533
+ if (this.length <= 0) return;
6534
+ const chunkCount = Math.ceil(this.length / MAX_CANVAS_WIDTH5);
6535
+ for (let i = 0; i < chunkCount; i++) {
6536
+ const widthPx = Math.min(MAX_CANVAS_WIDTH5, this.length - i * MAX_CANVAS_WIDTH5);
6537
+ const canvas = document.createElement("canvas");
6538
+ canvas.style.left = i * MAX_CANVAS_WIDTH5 + "px";
6539
+ canvas.style.width = widthPx + "px";
6540
+ const dpr = window.devicePixelRatio || 1;
6541
+ canvas.width = widthPx * dpr;
6542
+ canvas.height = this.waveHeight * dpr;
6543
+ this._canvases.push(canvas);
6544
+ }
6545
+ }
6546
+ updated(_changed) {
6547
+ if (this._registeredCanvasIds.length === 0 && this._canvases.length > 0) {
6548
+ requestAnimationFrame(() => this._registerCanvases());
6549
+ }
6550
+ }
6551
+ _registerCanvases() {
6552
+ const editor = this._findHostEditor();
6553
+ if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") return;
6554
+ for (let i = 0; i < this._canvases.length; i++) {
6555
+ const canvas = this._canvases[i];
6556
+ const canvasId = this.clipId + "-ch" + this.channelIndex + "-chunk" + i;
6557
+ let offscreen;
6558
+ try {
6559
+ offscreen = canvas.transferControlToOffscreen();
6560
+ } catch (err) {
6561
+ console.warn(
6562
+ "[dawcore] daw-spectrogram transferControlToOffscreen failed for " + canvasId + ": " + (err instanceof Error ? err.message : String(err))
6563
+ );
6564
+ continue;
6565
+ }
6566
+ editor._spectrogramRegisterCanvas({
6567
+ canvasId,
6568
+ canvas: offscreen,
6569
+ clipId: this.clipId,
6570
+ trackId: this.trackId,
6571
+ channelIndex: this.channelIndex,
6572
+ chunkIndex: i,
6573
+ globalPixelOffset: this.originX + i * MAX_CANVAS_WIDTH5,
6574
+ widthPx: parseFloat(canvas.style.width),
6575
+ heightPx: this.waveHeight
6576
+ });
6577
+ this._registeredCanvasIds.push(canvasId);
6578
+ }
6579
+ }
6580
+ _unregisterAllCanvases() {
6581
+ const editor = this._findHostEditor();
6582
+ if (editor && typeof editor._spectrogramUnregisterCanvas === "function") {
6583
+ for (const id of this._registeredCanvasIds) {
6584
+ editor._spectrogramUnregisterCanvas(id);
6585
+ }
6586
+ }
6587
+ this._registeredCanvasIds = [];
6588
+ }
6589
+ disconnectedCallback() {
6590
+ super.disconnectedCallback();
6591
+ this._unregisterAllCanvases();
6592
+ }
6593
+ render() {
6594
+ return import_lit19.html`${this._canvases.map((c) => c)}`;
6595
+ }
6596
+ };
6597
+ DawSpectrogramElement.styles = import_lit19.css`
6598
+ :host {
6599
+ display: block;
6600
+ position: relative;
6601
+ background: var(--daw-spectrogram-background, #000);
6602
+ }
6603
+ canvas {
6604
+ position: absolute;
6605
+ top: 0;
6606
+ left: 0;
6607
+ height: 100%;
6608
+ pointer-events: none;
6609
+ }
6610
+ `;
6611
+ __decorateClass([
6612
+ (0, import_decorators17.property)({ attribute: false })
6613
+ ], DawSpectrogramElement.prototype, "clipId", 2);
6614
+ __decorateClass([
6615
+ (0, import_decorators17.property)({ attribute: false })
6616
+ ], DawSpectrogramElement.prototype, "trackId", 2);
6617
+ __decorateClass([
6618
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6619
+ ], DawSpectrogramElement.prototype, "channelIndex", 2);
6620
+ __decorateClass([
6621
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6622
+ ], DawSpectrogramElement.prototype, "length", 2);
6623
+ __decorateClass([
6624
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6625
+ ], DawSpectrogramElement.prototype, "waveHeight", 2);
6626
+ __decorateClass([
6627
+ (0, import_decorators17.property)({ type: Number, attribute: false, noAccessor: true })
6628
+ ], DawSpectrogramElement.prototype, "samplesPerPixel", 1);
6629
+ __decorateClass([
6630
+ (0, import_decorators17.property)({ type: Number, attribute: false, noAccessor: true })
6631
+ ], DawSpectrogramElement.prototype, "sampleRate", 1);
6632
+ __decorateClass([
6633
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6634
+ ], DawSpectrogramElement.prototype, "clipOffsetSeconds", 2);
6635
+ __decorateClass([
6636
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6637
+ ], DawSpectrogramElement.prototype, "visibleStart", 2);
6638
+ __decorateClass([
6639
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6640
+ ], DawSpectrogramElement.prototype, "visibleEnd", 2);
6641
+ __decorateClass([
6642
+ (0, import_decorators17.property)({ type: Number, attribute: false })
6643
+ ], DawSpectrogramElement.prototype, "originX", 2);
6644
+ DawSpectrogramElement = __decorateClass([
6645
+ (0, import_decorators17.customElement)("daw-spectrogram")
6646
+ ], DawSpectrogramElement);
6175
6647
  // Annotate the CommonJS export names for ESM import in node:
6176
6648
  0 && (module.exports = {
6177
6649
  AudioResumeController,
@@ -6187,6 +6659,7 @@ DawKeyboardShortcutsElement = __decorateClass([
6187
6659
  DawRecordButtonElement,
6188
6660
  DawRulerElement,
6189
6661
  DawSelectionElement,
6662
+ DawSpectrogramElement,
6190
6663
  DawStopButtonElement,
6191
6664
  DawTrackControlsElement,
6192
6665
  DawTrackElement,
@@ -6194,6 +6667,7 @@ DawKeyboardShortcutsElement = __decorateClass([
6194
6667
  DawTransportElement,
6195
6668
  DawWaveformElement,
6196
6669
  RecordingController,
6670
+ SpectrogramController,
6197
6671
  isDomClip,
6198
6672
  splitAtPlayhead
6199
6673
  });