@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.mjs CHANGED
@@ -175,6 +175,7 @@ var DawTrackElement = class extends LitElement2 {
175
175
  this.muted = false;
176
176
  this.soloed = false;
177
177
  this.renderMode = "waveform";
178
+ this.spectrogramConfig = null;
178
179
  this.trackId = crypto.randomUUID();
179
180
  // Track removal is detected by the editor's MutationObserver,
180
181
  // not by dispatching from disconnectedCallback (detached elements
@@ -202,7 +203,16 @@ var DawTrackElement = class extends LitElement2 {
202
203
  this._hasRendered = true;
203
204
  return;
204
205
  }
205
- const trackProps = ["volume", "pan", "muted", "soloed", "src", "name", "renderMode"];
206
+ const trackProps = [
207
+ "volume",
208
+ "pan",
209
+ "muted",
210
+ "soloed",
211
+ "src",
212
+ "name",
213
+ "renderMode",
214
+ "spectrogramConfig"
215
+ ];
206
216
  const hasTrackChange = trackProps.some((p) => changed.has(p));
207
217
  if (hasTrackChange) {
208
218
  this.dispatchEvent(
@@ -236,6 +246,9 @@ __decorateClass([
236
246
  __decorateClass([
237
247
  property2({ attribute: "render-mode" })
238
248
  ], DawTrackElement.prototype, "renderMode", 2);
249
+ __decorateClass([
250
+ property2({ attribute: false })
251
+ ], DawTrackElement.prototype, "spectrogramConfig", 2);
239
252
  DawTrackElement = __decorateClass([
240
253
  customElement2("daw-track")
241
254
  ], DawTrackElement);
@@ -2708,6 +2721,123 @@ var RecordingController = class {
2708
2721
  }
2709
2722
  };
2710
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
+
2711
2841
  // src/interactions/pointer-handler.ts
2712
2842
  import { pixelsToSeconds, snapTickToGrid } from "@waveform-playlist/core";
2713
2843
 
@@ -3603,6 +3733,8 @@ var DawEditorElement = class extends LitElement10 {
3603
3733
  this.clipHeaderHeight = 20;
3604
3734
  this.interactiveClips = false;
3605
3735
  this.indefinitePlayback = false;
3736
+ this._spectrogramConfig = null;
3737
+ this._spectrogramColorMap = null;
3606
3738
  this.scaleMode = "temporal";
3607
3739
  this._ticksPerPixel = 24;
3608
3740
  this._bpm = 120;
@@ -3638,6 +3770,7 @@ var DawEditorElement = class extends LitElement10 {
3638
3770
  this._childObserver = null;
3639
3771
  this._audioResume = new AudioResumeController(this);
3640
3772
  this._recordingController = new RecordingController(this);
3773
+ this._spectrogramController = null;
3641
3774
  this._clipPointer = new ClipPointerHandler(this);
3642
3775
  this._pointer = new PointerHandler(this);
3643
3776
  this._viewport = (() => {
@@ -3645,6 +3778,15 @@ var DawEditorElement = class extends LitElement10 {
3645
3778
  v.scrollSelector = ".scroll-area";
3646
3779
  return v;
3647
3780
  })();
3781
+ /**
3782
+ * Cache of the last ViewportState forwarded to the spectrogram controller.
3783
+ * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
3784
+ * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
3785
+ * viewport. Skip the cross-controller call when nothing changed.
3786
+ *
3787
+ * The orchestrator dedupes too, but this avoids the call entirely.
3788
+ */
3789
+ this._lastSpectrogramViewport = null;
3648
3790
  // --- Track Events ---
3649
3791
  this._onTrackConnected = (e) => {
3650
3792
  const trackId = e.detail?.trackId;
@@ -3678,6 +3820,26 @@ var DawEditorElement = class extends LitElement10 {
3678
3820
  if (oldDescriptor?.src !== descriptor.src) {
3679
3821
  this._loadTrack(trackId, descriptor);
3680
3822
  }
3823
+ if (descriptor.renderMode === "spectrogram" && oldDescriptor?.renderMode !== "spectrogram") {
3824
+ const engineTrack = this._engineTracks.get(trackId);
3825
+ if (engineTrack) {
3826
+ for (const clip of engineTrack.clips) {
3827
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
3828
+ }
3829
+ }
3830
+ }
3831
+ if (descriptor.renderMode !== "spectrogram" && oldDescriptor?.renderMode === "spectrogram") {
3832
+ const engineTrack = this._engineTracks.get(trackId);
3833
+ if (engineTrack && this._spectrogramController) {
3834
+ for (const clip of engineTrack.clips) {
3835
+ this._spectrogramController.unregisterClipAudio(clip.id);
3836
+ }
3837
+ }
3838
+ this._disposeSpectrogramControllerIfEmpty();
3839
+ }
3840
+ if (descriptor.spectrogramConfig !== oldDescriptor?.spectrogramConfig) {
3841
+ this._spectrogramController?.setTrackConfig(trackId, descriptor.spectrogramConfig ?? null);
3842
+ }
3681
3843
  };
3682
3844
  this._onTrackControl = (e) => {
3683
3845
  const { trackId, prop, value } = e.detail ?? {};
@@ -3821,6 +3983,72 @@ var DawEditorElement = class extends LitElement10 {
3821
3983
  this._samplesPerPixel = clamped;
3822
3984
  this.requestUpdate("samplesPerPixel", old);
3823
3985
  }
3986
+ get spectrogramConfig() {
3987
+ return this._spectrogramConfig;
3988
+ }
3989
+ set spectrogramConfig(value) {
3990
+ const old = this._spectrogramConfig;
3991
+ this._spectrogramConfig = value;
3992
+ this._spectrogramController?.setEditorConfig(value);
3993
+ this.requestUpdate("spectrogramConfig", old);
3994
+ }
3995
+ get spectrogramColorMap() {
3996
+ return this._spectrogramColorMap;
3997
+ }
3998
+ set spectrogramColorMap(value) {
3999
+ const old = this._spectrogramColorMap;
4000
+ this._spectrogramColorMap = value;
4001
+ this._spectrogramController?.setEditorColorMap(value);
4002
+ this.requestUpdate("spectrogramColorMap", old);
4003
+ }
4004
+ _ensureSpectrogramController() {
4005
+ if (!this._spectrogramController) {
4006
+ this._spectrogramController = new SpectrogramController(
4007
+ this,
4008
+ () => new Worker(new URL("@dawcore/spectrogram/worker/spectrogram.worker", import.meta.url), {
4009
+ type: "module"
4010
+ })
4011
+ );
4012
+ if (this._spectrogramConfig) {
4013
+ this._spectrogramController.setEditorConfig(this._spectrogramConfig);
4014
+ }
4015
+ if (this._spectrogramColorMap) {
4016
+ this._spectrogramController.setEditorColorMap(this._spectrogramColorMap);
4017
+ }
4018
+ }
4019
+ return this._spectrogramController;
4020
+ }
4021
+ /** Called by <daw-spectrogram> after transferControlToOffscreen. */
4022
+ _spectrogramRegisterCanvas(reg) {
4023
+ this._ensureSpectrogramController().registerCanvas(reg);
4024
+ }
4025
+ /** Called by <daw-spectrogram> on chunk unmount / element disconnect. */
4026
+ _spectrogramUnregisterCanvas(canvasId) {
4027
+ this._spectrogramController?.unregisterCanvas(canvasId);
4028
+ }
4029
+ /**
4030
+ * Push a clip's decoded audio into the spectrogram controller. No-op
4031
+ * unless the track is in spectrogram render-mode and the controller
4032
+ * already exists (it bootstraps from canvas registration).
4033
+ */
4034
+ _maybeRegisterSpectrogramClipAudio(trackId, clip) {
4035
+ const descriptor = this._tracks.get(trackId);
4036
+ if (descriptor?.renderMode !== "spectrogram") return;
4037
+ const buffer = clip.audioBuffer ?? this._clipBuffers.get(clip.id);
4038
+ if (!buffer) return;
4039
+ const channelData = [];
4040
+ for (let i = 0; i < buffer.numberOfChannels; i++) {
4041
+ channelData.push(buffer.getChannelData(i));
4042
+ }
4043
+ this._ensureSpectrogramController().registerClipAudio({
4044
+ clipId: clip.id,
4045
+ trackId,
4046
+ channelData,
4047
+ sampleRate: buffer.sampleRate,
4048
+ durationSamples: clip.durationSamples,
4049
+ offsetSamples: clip.offsetSamples
4050
+ });
4051
+ }
3824
4052
  get ticksPerPixel() {
3825
4053
  return this._ticksPerPixel;
3826
4054
  }
@@ -4061,6 +4289,8 @@ var DawEditorElement = class extends LitElement10 {
4061
4289
  this._clipOffsets.clear();
4062
4290
  this._peakPipeline.terminate();
4063
4291
  this._minSamplesPerPixel = 0;
4292
+ this._spectrogramController?.dispose();
4293
+ this._spectrogramController = null;
4064
4294
  try {
4065
4295
  this._disposeEngine();
4066
4296
  } catch (err) {
@@ -4089,6 +4319,27 @@ var DawEditorElement = class extends LitElement10 {
4089
4319
  }
4090
4320
  }
4091
4321
  }
4322
+ updated(_changed) {
4323
+ if (this._spectrogramController) {
4324
+ const vs = this._viewport.visibleStart;
4325
+ const ve = this._viewport.visibleEnd;
4326
+ const spp = this._renderSpp;
4327
+ if (Number.isFinite(vs) && Number.isFinite(ve)) {
4328
+ const prev = this._lastSpectrogramViewport;
4329
+ if (prev && prev.vs === vs && prev.ve === ve && prev.spp === spp) return;
4330
+ this._lastSpectrogramViewport = { vs, ve, spp };
4331
+ const span = ve - vs;
4332
+ const bufferPad = span * 0.25;
4333
+ this._spectrogramController.setViewport({
4334
+ visibleStartPx: vs,
4335
+ visibleEndPx: ve,
4336
+ bufferStartPx: Math.max(0, vs - bufferPad),
4337
+ bufferEndPx: ve + bufferPad,
4338
+ samplesPerPixel: spp
4339
+ });
4340
+ }
4341
+ }
4342
+ }
4092
4343
  _onTrackRemoved(trackId) {
4093
4344
  this._trackElements.delete(trackId);
4094
4345
  const removedTrack = this._engineTracks.get(trackId);
@@ -4098,6 +4349,7 @@ var DawEditorElement = class extends LitElement10 {
4098
4349
  this._clipBuffers.delete(clip.id);
4099
4350
  this._clipOffsets.delete(clip.id);
4100
4351
  nextPeaks.delete(clip.id);
4352
+ this._spectrogramController?.unregisterClipAudio(clip.id);
4101
4353
  }
4102
4354
  this._peaksData = nextPeaks;
4103
4355
  }
@@ -4112,11 +4364,23 @@ var DawEditorElement = class extends LitElement10 {
4112
4364
  this._engine.removeTrack(trackId);
4113
4365
  }
4114
4366
  this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
4367
+ this._disposeSpectrogramControllerIfEmpty();
4115
4368
  if (nextEngine.size === 0) {
4116
4369
  this._currentTime = 0;
4117
4370
  this._stopPlayhead();
4118
4371
  }
4119
4372
  }
4373
+ /** Drop the controller when no spectrogram tracks remain. */
4374
+ _disposeSpectrogramControllerIfEmpty() {
4375
+ if (!this._spectrogramController) return;
4376
+ const stillNeeded = Array.from(this._tracks.values()).some(
4377
+ (d) => d.renderMode === "spectrogram"
4378
+ );
4379
+ if (!stillNeeded) {
4380
+ this._spectrogramController.dispose();
4381
+ this._spectrogramController = null;
4382
+ }
4383
+ }
4120
4384
  _onClipRemovedFromDom(clipEl) {
4121
4385
  const clipId = clipEl.clipId;
4122
4386
  for (const [trackId, t] of this._engineTracks.entries()) {
@@ -4158,6 +4422,7 @@ var DawEditorElement = class extends LitElement10 {
4158
4422
  });
4159
4423
  }
4160
4424
  this._commitTrackChange(trackId, updatedTrack);
4425
+ this._maybeRegisterSpectrogramClipAudio(trackId, clip);
4161
4426
  this.dispatchEvent(
4162
4427
  new CustomEvent("daw-clip-ready", {
4163
4428
  bubbles: true,
@@ -4332,6 +4597,7 @@ var DawEditorElement = class extends LitElement10 {
4332
4597
  nextPeaks.delete(clipId);
4333
4598
  this._peaksData = nextPeaks;
4334
4599
  this._clipOffsets.delete(clipId);
4600
+ this._spectrogramController?.unregisterClipAudio(clipId);
4335
4601
  }
4336
4602
  /**
4337
4603
  * Recompute duration and forward an updated track to the engine. Single
@@ -4614,6 +4880,9 @@ var DawEditorElement = class extends LitElement10 {
4614
4880
  track.id = trackId;
4615
4881
  this._engineTracks = new Map(this._engineTracks).set(trackId, track);
4616
4882
  this._recomputeDuration();
4883
+ for (const c of clips) {
4884
+ this._maybeRegisterSpectrogramClipAudio(trackId, c);
4885
+ }
4617
4886
  const engine = await this._ensureEngine();
4618
4887
  engine.setTracks([...this._engineTracks.values()]);
4619
4888
  this.dispatchEvent(
@@ -5425,19 +5694,34 @@ var DawEditorElement = class extends LitElement10 {
5425
5694
  .visibleEnd=${this._viewport.visibleEnd}
5426
5695
  .originX=${clipLeft}
5427
5696
  ?selected=${t.trackId === this._selectedTrackId}
5428
- ></daw-piano-roll>` : channels.map(
5697
+ ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
5698
+ (_chPeaks, chIdx) => html9`<daw-spectrogram
5699
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
5700
+ .clipId=${clip.id}
5701
+ .trackId=${t.trackId}
5702
+ .channelIndex=${chIdx}
5703
+ .length=${peakData?.length ?? width}
5704
+ .waveHeight=${chH}
5705
+ .samplesPerPixel=${this._renderSpp}
5706
+ .sampleRate=${this.effectiveSampleRate}
5707
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5708
+ .visibleStart=${this._viewport.visibleStart}
5709
+ .visibleEnd=${this._viewport.visibleEnd}
5710
+ .originX=${clipLeft}
5711
+ ></daw-spectrogram>`
5712
+ ) : channels.map(
5429
5713
  (chPeaks, chIdx) => html9` <daw-waveform
5430
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5431
- .peaks=${chPeaks}
5432
- .length=${peakData?.length ?? width}
5433
- .waveHeight=${chH}
5434
- .barWidth=${this.barWidth}
5435
- .barGap=${this.barGap}
5436
- .visibleStart=${this._viewport.visibleStart}
5437
- .visibleEnd=${this._viewport.visibleEnd}
5438
- .originX=${clipLeft}
5439
- .segments=${clipSegments}
5440
- ></daw-waveform>`
5714
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5715
+ .peaks=${chPeaks}
5716
+ .length=${peakData?.length ?? width}
5717
+ .waveHeight=${chH}
5718
+ .barWidth=${this.barWidth}
5719
+ .barGap=${this.barGap}
5720
+ .visibleStart=${this._viewport.visibleStart}
5721
+ .visibleEnd=${this._viewport.visibleEnd}
5722
+ .originX=${clipLeft}
5723
+ .segments=${clipSegments}
5724
+ ></daw-waveform>`
5441
5725
  )}
5442
5726
  ${this.interactiveClips ? html9` <div
5443
5727
  class="clip-boundary"
@@ -5545,6 +5829,12 @@ __decorateClass([
5545
5829
  __decorateClass([
5546
5830
  property8({ type: Boolean, attribute: "indefinite-playback" })
5547
5831
  ], DawEditorElement.prototype, "indefinitePlayback", 2);
5832
+ __decorateClass([
5833
+ property8({ attribute: false, noAccessor: true })
5834
+ ], DawEditorElement.prototype, "spectrogramConfig", 1);
5835
+ __decorateClass([
5836
+ property8({ attribute: false, noAccessor: true })
5837
+ ], DawEditorElement.prototype, "spectrogramColorMap", 1);
5548
5838
  __decorateClass([
5549
5839
  property8({ type: String, attribute: "scale-mode" })
5550
5840
  ], DawEditorElement.prototype, "scaleMode", 2);
@@ -6123,6 +6413,187 @@ __decorateClass([
6123
6413
  DawKeyboardShortcutsElement = __decorateClass([
6124
6414
  customElement16("daw-keyboard-shortcuts")
6125
6415
  ], DawKeyboardShortcutsElement);
6416
+
6417
+ // src/elements/daw-spectrogram.ts
6418
+ import { LitElement as LitElement14, html as html13, css as css13 } from "lit";
6419
+ import { customElement as customElement17, property as property12 } from "lit/decorators.js";
6420
+ var MAX_CANVAS_WIDTH5 = 1e3;
6421
+ var DawSpectrogramElement = class extends LitElement14 {
6422
+ constructor() {
6423
+ super(...arguments);
6424
+ this.clipId = "";
6425
+ this.trackId = "";
6426
+ this.channelIndex = 0;
6427
+ this.length = 0;
6428
+ this.waveHeight = 128;
6429
+ this._samplesPerPixel = 1024;
6430
+ this._sampleRate = 44100;
6431
+ this.clipOffsetSeconds = 0;
6432
+ this.visibleStart = -Infinity;
6433
+ this.visibleEnd = Infinity;
6434
+ this.originX = 0;
6435
+ this._canvases = [];
6436
+ this._registeredCanvasIds = [];
6437
+ }
6438
+ get samplesPerPixel() {
6439
+ return this._samplesPerPixel;
6440
+ }
6441
+ set samplesPerPixel(value) {
6442
+ if (!Number.isFinite(value) || value <= 0) {
6443
+ console.warn("[dawcore] daw-spectrogram samplesPerPixel " + value + " is invalid \u2014 ignored");
6444
+ return;
6445
+ }
6446
+ const old = this._samplesPerPixel;
6447
+ this._samplesPerPixel = value;
6448
+ this.requestUpdate("samplesPerPixel", old);
6449
+ }
6450
+ get sampleRate() {
6451
+ return this._sampleRate;
6452
+ }
6453
+ set sampleRate(value) {
6454
+ if (!Number.isFinite(value) || value <= 0) {
6455
+ console.warn("[dawcore] daw-spectrogram sampleRate " + value + " is invalid \u2014 ignored");
6456
+ return;
6457
+ }
6458
+ const old = this._sampleRate;
6459
+ this._sampleRate = value;
6460
+ this.requestUpdate("sampleRate", old);
6461
+ }
6462
+ /**
6463
+ * Walk up to the editor host. `closest('daw-editor')` does NOT cross
6464
+ * shadow boundaries — and this element lives inside the editor's shadow
6465
+ * DOM — so use getRootNode().host to step out.
6466
+ */
6467
+ _findHostEditor() {
6468
+ const root = this.getRootNode();
6469
+ const host = root instanceof ShadowRoot ? root.host : null;
6470
+ if (!host) return null;
6471
+ if (host.tagName === "DAW-EDITOR") return host;
6472
+ return host.closest("daw-editor");
6473
+ }
6474
+ willUpdate(changed) {
6475
+ const layoutChanged = changed.has("length") || changed.has("waveHeight") || changed.has("samplesPerPixel") || changed.has("clipId") || changed.has("channelIndex");
6476
+ if (layoutChanged) {
6477
+ this._rebuildChunks();
6478
+ }
6479
+ }
6480
+ _rebuildChunks() {
6481
+ this._unregisterAllCanvases();
6482
+ this._canvases = [];
6483
+ if (this.length <= 0) return;
6484
+ const chunkCount = Math.ceil(this.length / MAX_CANVAS_WIDTH5);
6485
+ for (let i = 0; i < chunkCount; i++) {
6486
+ const widthPx = Math.min(MAX_CANVAS_WIDTH5, this.length - i * MAX_CANVAS_WIDTH5);
6487
+ const canvas = document.createElement("canvas");
6488
+ canvas.style.left = i * MAX_CANVAS_WIDTH5 + "px";
6489
+ canvas.style.width = widthPx + "px";
6490
+ const dpr = window.devicePixelRatio || 1;
6491
+ canvas.width = widthPx * dpr;
6492
+ canvas.height = this.waveHeight * dpr;
6493
+ this._canvases.push(canvas);
6494
+ }
6495
+ }
6496
+ updated(_changed) {
6497
+ if (this._registeredCanvasIds.length === 0 && this._canvases.length > 0) {
6498
+ requestAnimationFrame(() => this._registerCanvases());
6499
+ }
6500
+ }
6501
+ _registerCanvases() {
6502
+ const editor = this._findHostEditor();
6503
+ if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") return;
6504
+ for (let i = 0; i < this._canvases.length; i++) {
6505
+ const canvas = this._canvases[i];
6506
+ const canvasId = this.clipId + "-ch" + this.channelIndex + "-chunk" + i;
6507
+ let offscreen;
6508
+ try {
6509
+ offscreen = canvas.transferControlToOffscreen();
6510
+ } catch (err) {
6511
+ console.warn(
6512
+ "[dawcore] daw-spectrogram transferControlToOffscreen failed for " + canvasId + ": " + (err instanceof Error ? err.message : String(err))
6513
+ );
6514
+ continue;
6515
+ }
6516
+ editor._spectrogramRegisterCanvas({
6517
+ canvasId,
6518
+ canvas: offscreen,
6519
+ clipId: this.clipId,
6520
+ trackId: this.trackId,
6521
+ channelIndex: this.channelIndex,
6522
+ chunkIndex: i,
6523
+ globalPixelOffset: this.originX + i * MAX_CANVAS_WIDTH5,
6524
+ widthPx: parseFloat(canvas.style.width),
6525
+ heightPx: this.waveHeight
6526
+ });
6527
+ this._registeredCanvasIds.push(canvasId);
6528
+ }
6529
+ }
6530
+ _unregisterAllCanvases() {
6531
+ const editor = this._findHostEditor();
6532
+ if (editor && typeof editor._spectrogramUnregisterCanvas === "function") {
6533
+ for (const id of this._registeredCanvasIds) {
6534
+ editor._spectrogramUnregisterCanvas(id);
6535
+ }
6536
+ }
6537
+ this._registeredCanvasIds = [];
6538
+ }
6539
+ disconnectedCallback() {
6540
+ super.disconnectedCallback();
6541
+ this._unregisterAllCanvases();
6542
+ }
6543
+ render() {
6544
+ return html13`${this._canvases.map((c) => c)}`;
6545
+ }
6546
+ };
6547
+ DawSpectrogramElement.styles = css13`
6548
+ :host {
6549
+ display: block;
6550
+ position: relative;
6551
+ background: var(--daw-spectrogram-background, #000);
6552
+ }
6553
+ canvas {
6554
+ position: absolute;
6555
+ top: 0;
6556
+ left: 0;
6557
+ height: 100%;
6558
+ pointer-events: none;
6559
+ }
6560
+ `;
6561
+ __decorateClass([
6562
+ property12({ attribute: false })
6563
+ ], DawSpectrogramElement.prototype, "clipId", 2);
6564
+ __decorateClass([
6565
+ property12({ attribute: false })
6566
+ ], DawSpectrogramElement.prototype, "trackId", 2);
6567
+ __decorateClass([
6568
+ property12({ type: Number, attribute: false })
6569
+ ], DawSpectrogramElement.prototype, "channelIndex", 2);
6570
+ __decorateClass([
6571
+ property12({ type: Number, attribute: false })
6572
+ ], DawSpectrogramElement.prototype, "length", 2);
6573
+ __decorateClass([
6574
+ property12({ type: Number, attribute: false })
6575
+ ], DawSpectrogramElement.prototype, "waveHeight", 2);
6576
+ __decorateClass([
6577
+ property12({ type: Number, attribute: false, noAccessor: true })
6578
+ ], DawSpectrogramElement.prototype, "samplesPerPixel", 1);
6579
+ __decorateClass([
6580
+ property12({ type: Number, attribute: false, noAccessor: true })
6581
+ ], DawSpectrogramElement.prototype, "sampleRate", 1);
6582
+ __decorateClass([
6583
+ property12({ type: Number, attribute: false })
6584
+ ], DawSpectrogramElement.prototype, "clipOffsetSeconds", 2);
6585
+ __decorateClass([
6586
+ property12({ type: Number, attribute: false })
6587
+ ], DawSpectrogramElement.prototype, "visibleStart", 2);
6588
+ __decorateClass([
6589
+ property12({ type: Number, attribute: false })
6590
+ ], DawSpectrogramElement.prototype, "visibleEnd", 2);
6591
+ __decorateClass([
6592
+ property12({ type: Number, attribute: false })
6593
+ ], DawSpectrogramElement.prototype, "originX", 2);
6594
+ DawSpectrogramElement = __decorateClass([
6595
+ customElement17("daw-spectrogram")
6596
+ ], DawSpectrogramElement);
6126
6597
  export {
6127
6598
  AudioResumeController,
6128
6599
  ClipPointerHandler,
@@ -6137,6 +6608,7 @@ export {
6137
6608
  DawRecordButtonElement,
6138
6609
  DawRulerElement,
6139
6610
  DawSelectionElement,
6611
+ DawSpectrogramElement,
6140
6612
  DawStopButtonElement,
6141
6613
  DawTrackControlsElement,
6142
6614
  DawTrackElement,
@@ -6144,6 +6616,7 @@ export {
6144
6616
  DawTransportElement,
6145
6617
  DawWaveformElement,
6146
6618
  RecordingController,
6619
+ SpectrogramController,
6147
6620
  isDomClip,
6148
6621
  splitAtPlayhead
6149
6622
  };