@dawcore/components 0.0.3 → 0.0.5

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.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as lit from 'lit';
2
2
  import { LitElement, PropertyValues, ReactiveController, ReactiveControllerHost } from 'lit';
3
3
  import { Peaks, Bits, FadeType, PeakData, ClipTrack, KeyboardShortcut } from '@waveform-playlist/core';
4
+ import WaveformData from 'waveform-data';
4
5
  import { PlaylistEngine } from '@waveform-playlist/engine';
5
6
 
6
7
  declare class DawClipElement extends LitElement {
@@ -184,6 +185,7 @@ interface TrackDescriptor {
184
185
  }
185
186
  interface ClipDescriptor {
186
187
  src: string;
188
+ peaksSrc: string;
187
189
  start: number;
188
190
  duration: number;
189
191
  offset: number;
@@ -213,6 +215,11 @@ declare class PeakPipeline {
213
215
  private _baseScale;
214
216
  private _bits;
215
217
  constructor(baseScale?: number, bits?: 8 | 16);
218
+ /**
219
+ * Inject externally-loaded WaveformData (e.g., from a .dat file) into the cache.
220
+ * Prevents worker generation for this AudioBuffer on all subsequent calls.
221
+ */
222
+ cacheWaveformData(audioBuffer: AudioBuffer, waveformData: WaveformData): void;
216
223
  /**
217
224
  * Generate PeakData for a clip from its AudioBuffer.
218
225
  * Uses cached WaveformData when available; otherwise generates via worker.
@@ -221,14 +228,26 @@ declare class PeakPipeline {
221
228
  generatePeaks(audioBuffer: AudioBuffer, samplesPerPixel: number, isMono: boolean, offsetSamples?: number, durationSamples?: number): Promise<PeakData>;
222
229
  /**
223
230
  * Re-extract peaks for all clips at a new zoom level using cached WaveformData.
224
- * Only works for zoom levels coarser than (or equal to) the cached base scale.
225
- * Returns a new Map of clipId PeakData. Clips without cached data or where
226
- * the target scale is finer than the cached base are skipped.
231
+ * Returns a new Map of clipId PeakData. Clips without cached data are skipped.
232
+ * When the requested scale is finer than cached data, peaks are clamped to the
233
+ * cached scale and a single summary warning is logged.
227
234
  */
228
235
  reextractPeaks(clipBuffers: ReadonlyMap<string, AudioBuffer>, samplesPerPixel: number, isMono: boolean, clipOffsets?: ReadonlyMap<string, {
229
236
  offsetSamples: number;
230
237
  durationSamples: number;
231
238
  }>): Map<string, PeakData>;
239
+ /**
240
+ * Clamp requested scale to cached WaveformData scale.
241
+ * WaveformData.resample() can only go coarser — if the requested zoom is
242
+ * finer than the cached data, use the cached scale. Set warn=true to log
243
+ * (default); reextractPeaks passes false and logs a single summary instead.
244
+ */
245
+ private _clampScale;
246
+ /**
247
+ * Return the coarsest (largest) scale among cached WaveformData entries
248
+ * that correspond to the given clip buffers. Returns 0 if none are cached.
249
+ */
250
+ getMaxCachedScale(clipBuffers: ReadonlyMap<string, AudioBuffer>): number;
232
251
  terminate(): void;
233
252
  private _getWaveformData;
234
253
  }
@@ -516,7 +535,9 @@ interface LoadFilesResult {
516
535
  }
517
536
 
518
537
  declare class DawEditorElement extends LitElement {
519
- samplesPerPixel: number;
538
+ get samplesPerPixel(): number;
539
+ set samplesPerPixel(value: number);
540
+ private _samplesPerPixel;
520
541
  waveHeight: number;
521
542
  timescale: boolean;
522
543
  mono: boolean;
@@ -526,7 +547,8 @@ declare class DawEditorElement extends LitElement {
526
547
  clipHeaders: boolean;
527
548
  clipHeaderHeight: number;
528
549
  interactiveClips: boolean;
529
- /** Initial sample rate hint. Overridden by decoded audio buffer's actual rate. */
550
+ /** Desired sample rate. Creates a cross-browser AudioContext at this rate.
551
+ * Pre-computed .dat peaks render instantly when they match. */
530
552
  sampleRate: number;
531
553
  /** Resolved sample rate — falls back to sampleRate property until first audio decode. */
532
554
  _resolvedSampleRate: number | null;
@@ -543,12 +565,15 @@ declare class DawEditorElement extends LitElement {
543
565
  _engine: PlaylistEngine | null;
544
566
  private _enginePromise;
545
567
  _audioCache: Map<string, Promise<AudioBuffer>>;
568
+ private _peaksCache;
546
569
  _clipBuffers: Map<string, AudioBuffer>;
547
570
  _clipOffsets: Map<string, {
548
571
  offsetSamples: number;
549
572
  durationSamples: number;
550
573
  }>;
551
574
  _peakPipeline: PeakPipeline;
575
+ /** Coarsest scale from pre-computed peaks — zoom cannot go finer than this. 0 = no limit. */
576
+ private _minSamplesPerPixel;
552
577
  private _trackElements;
553
578
  private _childObserver;
554
579
  private _audioResume;
@@ -587,7 +612,15 @@ declare class DawEditorElement extends LitElement {
587
612
  private _onTrackRemoveRequest;
588
613
  private _readTrackDescriptor;
589
614
  private _loadTrack;
615
+ private _contextConfigurePromise;
616
+ /**
617
+ * Ensure the global AudioContext is configured with the editor's sample-rate hint
618
+ * before the first audio operation. Idempotent — concurrent callers await the
619
+ * same promise so no one proceeds to getGlobalAudioContext() before configuration.
620
+ */
621
+ private _ensureContextConfigured;
590
622
  _fetchAndDecode(src: string): Promise<AudioBuffer>;
623
+ private _fetchPeaks;
591
624
  _recomputeDuration(): void;
592
625
  _ensureEngine(): Promise<PlaylistEngine>;
593
626
  private _buildEngine;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as lit from 'lit';
2
2
  import { LitElement, PropertyValues, ReactiveController, ReactiveControllerHost } from 'lit';
3
3
  import { Peaks, Bits, FadeType, PeakData, ClipTrack, KeyboardShortcut } from '@waveform-playlist/core';
4
+ import WaveformData from 'waveform-data';
4
5
  import { PlaylistEngine } from '@waveform-playlist/engine';
5
6
 
6
7
  declare class DawClipElement extends LitElement {
@@ -184,6 +185,7 @@ interface TrackDescriptor {
184
185
  }
185
186
  interface ClipDescriptor {
186
187
  src: string;
188
+ peaksSrc: string;
187
189
  start: number;
188
190
  duration: number;
189
191
  offset: number;
@@ -213,6 +215,11 @@ declare class PeakPipeline {
213
215
  private _baseScale;
214
216
  private _bits;
215
217
  constructor(baseScale?: number, bits?: 8 | 16);
218
+ /**
219
+ * Inject externally-loaded WaveformData (e.g., from a .dat file) into the cache.
220
+ * Prevents worker generation for this AudioBuffer on all subsequent calls.
221
+ */
222
+ cacheWaveformData(audioBuffer: AudioBuffer, waveformData: WaveformData): void;
216
223
  /**
217
224
  * Generate PeakData for a clip from its AudioBuffer.
218
225
  * Uses cached WaveformData when available; otherwise generates via worker.
@@ -221,14 +228,26 @@ declare class PeakPipeline {
221
228
  generatePeaks(audioBuffer: AudioBuffer, samplesPerPixel: number, isMono: boolean, offsetSamples?: number, durationSamples?: number): Promise<PeakData>;
222
229
  /**
223
230
  * Re-extract peaks for all clips at a new zoom level using cached WaveformData.
224
- * Only works for zoom levels coarser than (or equal to) the cached base scale.
225
- * Returns a new Map of clipId PeakData. Clips without cached data or where
226
- * the target scale is finer than the cached base are skipped.
231
+ * Returns a new Map of clipId PeakData. Clips without cached data are skipped.
232
+ * When the requested scale is finer than cached data, peaks are clamped to the
233
+ * cached scale and a single summary warning is logged.
227
234
  */
228
235
  reextractPeaks(clipBuffers: ReadonlyMap<string, AudioBuffer>, samplesPerPixel: number, isMono: boolean, clipOffsets?: ReadonlyMap<string, {
229
236
  offsetSamples: number;
230
237
  durationSamples: number;
231
238
  }>): Map<string, PeakData>;
239
+ /**
240
+ * Clamp requested scale to cached WaveformData scale.
241
+ * WaveformData.resample() can only go coarser — if the requested zoom is
242
+ * finer than the cached data, use the cached scale. Set warn=true to log
243
+ * (default); reextractPeaks passes false and logs a single summary instead.
244
+ */
245
+ private _clampScale;
246
+ /**
247
+ * Return the coarsest (largest) scale among cached WaveformData entries
248
+ * that correspond to the given clip buffers. Returns 0 if none are cached.
249
+ */
250
+ getMaxCachedScale(clipBuffers: ReadonlyMap<string, AudioBuffer>): number;
232
251
  terminate(): void;
233
252
  private _getWaveformData;
234
253
  }
@@ -516,7 +535,9 @@ interface LoadFilesResult {
516
535
  }
517
536
 
518
537
  declare class DawEditorElement extends LitElement {
519
- samplesPerPixel: number;
538
+ get samplesPerPixel(): number;
539
+ set samplesPerPixel(value: number);
540
+ private _samplesPerPixel;
520
541
  waveHeight: number;
521
542
  timescale: boolean;
522
543
  mono: boolean;
@@ -526,7 +547,8 @@ declare class DawEditorElement extends LitElement {
526
547
  clipHeaders: boolean;
527
548
  clipHeaderHeight: number;
528
549
  interactiveClips: boolean;
529
- /** Initial sample rate hint. Overridden by decoded audio buffer's actual rate. */
550
+ /** Desired sample rate. Creates a cross-browser AudioContext at this rate.
551
+ * Pre-computed .dat peaks render instantly when they match. */
530
552
  sampleRate: number;
531
553
  /** Resolved sample rate — falls back to sampleRate property until first audio decode. */
532
554
  _resolvedSampleRate: number | null;
@@ -543,12 +565,15 @@ declare class DawEditorElement extends LitElement {
543
565
  _engine: PlaylistEngine | null;
544
566
  private _enginePromise;
545
567
  _audioCache: Map<string, Promise<AudioBuffer>>;
568
+ private _peaksCache;
546
569
  _clipBuffers: Map<string, AudioBuffer>;
547
570
  _clipOffsets: Map<string, {
548
571
  offsetSamples: number;
549
572
  durationSamples: number;
550
573
  }>;
551
574
  _peakPipeline: PeakPipeline;
575
+ /** Coarsest scale from pre-computed peaks — zoom cannot go finer than this. 0 = no limit. */
576
+ private _minSamplesPerPixel;
552
577
  private _trackElements;
553
578
  private _childObserver;
554
579
  private _audioResume;
@@ -587,7 +612,15 @@ declare class DawEditorElement extends LitElement {
587
612
  private _onTrackRemoveRequest;
588
613
  private _readTrackDescriptor;
589
614
  private _loadTrack;
615
+ private _contextConfigurePromise;
616
+ /**
617
+ * Ensure the global AudioContext is configured with the editor's sample-rate hint
618
+ * before the first audio operation. Idempotent — concurrent callers await the
619
+ * same promise so no one proceeds to getGlobalAudioContext() before configuration.
620
+ */
621
+ private _ensureContextConfigured;
590
622
  _fetchAndDecode(src: string): Promise<AudioBuffer>;
623
+ private _fetchPeaks;
591
624
  _recomputeDuration(): void;
592
625
  _ensureEngine(): Promise<PlaylistEngine>;
593
626
  private _buildEngine;
package/dist/index.js CHANGED
@@ -1134,6 +1134,13 @@ var PeakPipeline = class {
1134
1134
  this._baseScale = baseScale;
1135
1135
  this._bits = bits;
1136
1136
  }
1137
+ /**
1138
+ * Inject externally-loaded WaveformData (e.g., from a .dat file) into the cache.
1139
+ * Prevents worker generation for this AudioBuffer on all subsequent calls.
1140
+ */
1141
+ cacheWaveformData(audioBuffer, waveformData) {
1142
+ this._cache.set(audioBuffer, waveformData);
1143
+ }
1137
1144
  /**
1138
1145
  * Generate PeakData for a clip from its AudioBuffer.
1139
1146
  * Uses cached WaveformData when available; otherwise generates via worker.
@@ -1141,8 +1148,9 @@ var PeakPipeline = class {
1141
1148
  */
1142
1149
  async generatePeaks(audioBuffer, samplesPerPixel, isMono, offsetSamples, durationSamples) {
1143
1150
  const waveformData = await this._getWaveformData(audioBuffer);
1151
+ const effectiveScale = this._clampScale(waveformData, samplesPerPixel);
1144
1152
  try {
1145
- return extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, durationSamples);
1153
+ return extractPeaks(waveformData, effectiveScale, isMono, offsetSamples, durationSamples);
1146
1154
  } catch (err) {
1147
1155
  console.warn("[dawcore] extractPeaks failed: " + String(err));
1148
1156
  throw err;
@@ -1150,23 +1158,29 @@ var PeakPipeline = class {
1150
1158
  }
1151
1159
  /**
1152
1160
  * Re-extract peaks for all clips at a new zoom level using cached WaveformData.
1153
- * Only works for zoom levels coarser than (or equal to) the cached base scale.
1154
- * Returns a new Map of clipId PeakData. Clips without cached data or where
1155
- * the target scale is finer than the cached base are skipped.
1161
+ * Returns a new Map of clipId PeakData. Clips without cached data are skipped.
1162
+ * When the requested scale is finer than cached data, peaks are clamped to the
1163
+ * cached scale and a single summary warning is logged.
1156
1164
  */
1157
1165
  reextractPeaks(clipBuffers, samplesPerPixel, isMono, clipOffsets) {
1158
1166
  const result = /* @__PURE__ */ new Map();
1167
+ let clampedCount = 0;
1168
+ let clampedScale = 0;
1159
1169
  for (const [clipId, audioBuffer] of clipBuffers) {
1160
1170
  const cached = this._cache.get(audioBuffer);
1161
1171
  if (cached) {
1162
- if (samplesPerPixel < cached.scale) continue;
1172
+ const effectiveScale = this._clampScale(cached, samplesPerPixel, false);
1173
+ if (effectiveScale !== samplesPerPixel) {
1174
+ clampedCount++;
1175
+ clampedScale = effectiveScale;
1176
+ }
1163
1177
  try {
1164
1178
  const offsets = clipOffsets?.get(clipId);
1165
1179
  result.set(
1166
1180
  clipId,
1167
1181
  extractPeaks(
1168
1182
  cached,
1169
- samplesPerPixel,
1183
+ effectiveScale,
1170
1184
  isMono,
1171
1185
  offsets?.offsetSamples,
1172
1186
  offsets?.durationSamples
@@ -1177,8 +1191,42 @@ var PeakPipeline = class {
1177
1191
  }
1178
1192
  }
1179
1193
  }
1194
+ if (clampedCount > 0) {
1195
+ console.warn(
1196
+ "[dawcore] Requested zoom " + samplesPerPixel + " spp is finer than pre-computed peaks (" + clampedScale + " spp) \u2014 " + clampedCount + " clip(s) using available resolution"
1197
+ );
1198
+ }
1180
1199
  return result;
1181
1200
  }
1201
+ /**
1202
+ * Clamp requested scale to cached WaveformData scale.
1203
+ * WaveformData.resample() can only go coarser — if the requested zoom is
1204
+ * finer than the cached data, use the cached scale. Set warn=true to log
1205
+ * (default); reextractPeaks passes false and logs a single summary instead.
1206
+ */
1207
+ _clampScale(waveformData, requestedScale, warn = true) {
1208
+ if (requestedScale < waveformData.scale) {
1209
+ if (warn) {
1210
+ console.warn(
1211
+ "[dawcore] Requested zoom " + requestedScale + " spp is finer than pre-computed peaks (" + waveformData.scale + " spp) \u2014 using available resolution"
1212
+ );
1213
+ }
1214
+ return waveformData.scale;
1215
+ }
1216
+ return requestedScale;
1217
+ }
1218
+ /**
1219
+ * Return the coarsest (largest) scale among cached WaveformData entries
1220
+ * that correspond to the given clip buffers. Returns 0 if none are cached.
1221
+ */
1222
+ getMaxCachedScale(clipBuffers) {
1223
+ let max = 0;
1224
+ for (const audioBuffer of clipBuffers.values()) {
1225
+ const cached = this._cache.get(audioBuffer);
1226
+ if (cached && cached.scale > max) max = cached.scale;
1227
+ }
1228
+ return max;
1229
+ }
1182
1230
  terminate() {
1183
1231
  this._worker?.terminate();
1184
1232
  this._worker = null;
@@ -2487,6 +2535,7 @@ async function loadFiles(host, files) {
2487
2535
  clips: [
2488
2536
  {
2489
2537
  src: "",
2538
+ peaksSrc: "",
2490
2539
  start: 0,
2491
2540
  duration: audioBuffer.duration,
2492
2541
  offset: 0,
@@ -2572,6 +2621,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
2572
2621
  const sr = host.effectiveSampleRate;
2573
2622
  const clipDesc = {
2574
2623
  src: "",
2624
+ peaksSrc: "",
2575
2625
  start: startSample / sr,
2576
2626
  duration: durSamples / sr,
2577
2627
  offset: 0,
@@ -2773,11 +2823,29 @@ function findAudioBufferForClip(host, clip, track) {
2773
2823
  return null;
2774
2824
  }
2775
2825
 
2826
+ // src/interactions/peaks-loader.ts
2827
+ var import_waveform_data2 = __toESM(require("waveform-data"));
2828
+ async function loadWaveformDataFromUrl(src) {
2829
+ const response = await fetch(src);
2830
+ if (!response.ok) {
2831
+ throw new Error("[dawcore] Failed to fetch peaks data: " + response.statusText);
2832
+ }
2833
+ const { pathname } = new URL(src, globalThis.location?.href ?? "http://localhost");
2834
+ const isBinary = pathname.toLowerCase().endsWith(".dat");
2835
+ if (isBinary) {
2836
+ const arrayBuffer = await response.arrayBuffer();
2837
+ return import_waveform_data2.default.create(arrayBuffer);
2838
+ } else {
2839
+ const json = await response.json();
2840
+ return import_waveform_data2.default.create(json);
2841
+ }
2842
+ }
2843
+
2776
2844
  // src/elements/daw-editor.ts
2777
2845
  var DawEditorElement = class extends import_lit12.LitElement {
2778
2846
  constructor() {
2779
2847
  super(...arguments);
2780
- this.samplesPerPixel = 1024;
2848
+ this._samplesPerPixel = 1024;
2781
2849
  this.waveHeight = 128;
2782
2850
  this.timescale = false;
2783
2851
  this.mono = false;
@@ -2804,9 +2872,12 @@ var DawEditorElement = class extends import_lit12.LitElement {
2804
2872
  this._engine = null;
2805
2873
  this._enginePromise = null;
2806
2874
  this._audioCache = /* @__PURE__ */ new Map();
2875
+ this._peaksCache = /* @__PURE__ */ new Map();
2807
2876
  this._clipBuffers = /* @__PURE__ */ new Map();
2808
2877
  this._clipOffsets = /* @__PURE__ */ new Map();
2809
2878
  this._peakPipeline = new PeakPipeline();
2879
+ /** Coarsest scale from pre-computed peaks — zoom cannot go finer than this. 0 = no limit. */
2880
+ this._minSamplesPerPixel = 0;
2810
2881
  this._trackElements = /* @__PURE__ */ new Map();
2811
2882
  this._childObserver = null;
2812
2883
  this._audioResume = new AudioResumeController(this);
@@ -2892,6 +2963,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
2892
2963
  this._onTrackRemoved(trackId);
2893
2964
  }
2894
2965
  };
2966
+ this._contextConfigurePromise = null;
2895
2967
  // --- File Drop ---
2896
2968
  this._onDragOver = (e) => {
2897
2969
  if (!this.fileDrop) return;
@@ -2928,6 +3000,21 @@ var DawEditorElement = class extends import_lit12.LitElement {
2928
3000
  // --- Recording ---
2929
3001
  this.recordingStream = null;
2930
3002
  }
3003
+ get samplesPerPixel() {
3004
+ return this._samplesPerPixel;
3005
+ }
3006
+ set samplesPerPixel(value) {
3007
+ const old = this._samplesPerPixel;
3008
+ if (!Number.isFinite(value) || value <= 0) return;
3009
+ const clamped = this._minSamplesPerPixel > 0 && value < this._minSamplesPerPixel ? this._minSamplesPerPixel : value;
3010
+ if (clamped !== value) {
3011
+ console.warn(
3012
+ "[dawcore] Zoom " + value + " spp rejected \u2014 pre-computed peaks limit is " + this._minSamplesPerPixel + " spp"
3013
+ );
3014
+ }
3015
+ this._samplesPerPixel = clamped;
3016
+ this.requestUpdate("samplesPerPixel", old);
3017
+ }
2931
3018
  get _clipHandler() {
2932
3019
  return this.interactiveClips ? this._clipPointer : null;
2933
3020
  }
@@ -3023,9 +3110,12 @@ var DawEditorElement = class extends import_lit12.LitElement {
3023
3110
  this._childObserver = null;
3024
3111
  this._trackElements.clear();
3025
3112
  this._audioCache.clear();
3113
+ this._peaksCache.clear();
3026
3114
  this._clipBuffers.clear();
3027
3115
  this._clipOffsets.clear();
3028
3116
  this._peakPipeline.terminate();
3117
+ this._minSamplesPerPixel = 0;
3118
+ this._contextConfigurePromise = null;
3029
3119
  try {
3030
3120
  this._disposeEngine();
3031
3121
  } catch (err) {
@@ -3075,6 +3165,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3075
3165
  if (this._engine) {
3076
3166
  this._engine.removeTrack(trackId);
3077
3167
  }
3168
+ this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
3078
3169
  if (nextEngine.size === 0) {
3079
3170
  this._currentTime = 0;
3080
3171
  this._stopPlayhead();
@@ -3086,6 +3177,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3086
3177
  if (clipEls.length === 0 && trackEl.src) {
3087
3178
  clips.push({
3088
3179
  src: trackEl.src,
3180
+ peaksSrc: "",
3089
3181
  start: 0,
3090
3182
  duration: 0,
3091
3183
  offset: 0,
@@ -3099,6 +3191,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3099
3191
  for (const clipEl of clipEls) {
3100
3192
  clips.push({
3101
3193
  src: clipEl.src,
3194
+ peaksSrc: clipEl.peaksSrc,
3102
3195
  start: clipEl.start,
3103
3196
  duration: clipEl.duration,
3104
3197
  offset: clipEl.offset,
@@ -3126,7 +3219,88 @@ var DawEditorElement = class extends import_lit12.LitElement {
3126
3219
  const clips = [];
3127
3220
  for (const clipDesc of descriptor.clips) {
3128
3221
  if (!clipDesc.src) continue;
3129
- const audioBuffer = await this._fetchAndDecode(clipDesc.src);
3222
+ const waveformDataPromise = clipDesc.peaksSrc ? this._fetchPeaks(clipDesc.peaksSrc) : null;
3223
+ const audioPromise = this._fetchAndDecode(clipDesc.src);
3224
+ let waveformData = null;
3225
+ if (waveformDataPromise) {
3226
+ try {
3227
+ const wd = await waveformDataPromise;
3228
+ await this._ensureContextConfigured();
3229
+ const { getGlobalAudioContext } = await import("@waveform-playlist/playout");
3230
+ const contextRate = getGlobalAudioContext().sampleRate;
3231
+ if (wd.sample_rate === contextRate) {
3232
+ waveformData = wd;
3233
+ } else {
3234
+ console.warn(
3235
+ "[dawcore] Pre-computed peaks at " + wd.sample_rate + " Hz do not match AudioContext at " + contextRate + " Hz \u2014 ignoring " + clipDesc.peaksSrc + ", generating from audio"
3236
+ );
3237
+ }
3238
+ } catch (err) {
3239
+ console.warn(
3240
+ "[dawcore] Failed to load peaks from " + clipDesc.peaksSrc + ": " + String(err) + " \u2014 falling back to AudioBuffer generation"
3241
+ );
3242
+ }
3243
+ }
3244
+ if (waveformData) {
3245
+ const wdRate = waveformData.sample_rate;
3246
+ const clip2 = (0, import_core4.createClip)({
3247
+ waveformData,
3248
+ startSample: Math.round(clipDesc.start * wdRate),
3249
+ durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
3250
+ offsetSamples: Math.round(clipDesc.offset * wdRate),
3251
+ gain: clipDesc.gain,
3252
+ name: clipDesc.name,
3253
+ sampleRate: wdRate,
3254
+ sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
3255
+ });
3256
+ const effectiveScale = Math.max(this.samplesPerPixel, waveformData.scale);
3257
+ const peakData2 = extractPeaks(
3258
+ waveformData,
3259
+ effectiveScale,
3260
+ this.mono,
3261
+ clip2.offsetSamples,
3262
+ clip2.durationSamples
3263
+ );
3264
+ this._clipOffsets.set(clip2.id, {
3265
+ offsetSamples: clip2.offsetSamples,
3266
+ durationSamples: clip2.durationSamples
3267
+ });
3268
+ this._peaksData = new Map(this._peaksData).set(clip2.id, peakData2);
3269
+ this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
3270
+ const previewTrack = (0, import_core4.createTrack)({
3271
+ name: descriptor.name,
3272
+ clips: [clip2],
3273
+ volume: descriptor.volume,
3274
+ pan: descriptor.pan,
3275
+ muted: descriptor.muted,
3276
+ soloed: descriptor.soloed
3277
+ });
3278
+ previewTrack.id = trackId;
3279
+ this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
3280
+ this._recomputeDuration();
3281
+ let audioBuffer2;
3282
+ try {
3283
+ audioBuffer2 = await audioPromise;
3284
+ } catch (audioErr) {
3285
+ const nextPeaks = new Map(this._peaksData);
3286
+ nextPeaks.delete(clip2.id);
3287
+ this._peaksData = nextPeaks;
3288
+ this._clipOffsets.delete(clip2.id);
3289
+ const nextEngine = new Map(this._engineTracks);
3290
+ nextEngine.delete(trackId);
3291
+ this._engineTracks = nextEngine;
3292
+ this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
3293
+ this._recomputeDuration();
3294
+ throw audioErr;
3295
+ }
3296
+ this._resolvedSampleRate = audioBuffer2.sampleRate;
3297
+ const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
3298
+ this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
3299
+ this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
3300
+ clips.push(updatedClip);
3301
+ continue;
3302
+ }
3303
+ const audioBuffer = await audioPromise;
3130
3304
  this._resolvedSampleRate = audioBuffer.sampleRate;
3131
3305
  const clip = (0, import_core4.createClipFromSeconds)({
3132
3306
  audioBuffer,
@@ -3185,6 +3359,30 @@ var DawEditorElement = class extends import_lit12.LitElement {
3185
3359
  );
3186
3360
  }
3187
3361
  }
3362
+ /**
3363
+ * Ensure the global AudioContext is configured with the editor's sample-rate hint
3364
+ * before the first audio operation. Idempotent — concurrent callers await the
3365
+ * same promise so no one proceeds to getGlobalAudioContext() before configuration.
3366
+ */
3367
+ _ensureContextConfigured() {
3368
+ if (!this._contextConfigurePromise) {
3369
+ this._contextConfigurePromise = (async () => {
3370
+ const { configureGlobalContext } = await import("@waveform-playlist/playout");
3371
+ const actualRate = configureGlobalContext({
3372
+ sampleRate: this.sampleRate
3373
+ });
3374
+ if (actualRate !== this.sampleRate) {
3375
+ console.warn(
3376
+ "[dawcore] Requested sampleRate " + this.sampleRate + " but AudioContext is running at " + actualRate
3377
+ );
3378
+ }
3379
+ })().catch((err) => {
3380
+ this._contextConfigurePromise = null;
3381
+ throw err;
3382
+ });
3383
+ }
3384
+ return this._contextConfigurePromise;
3385
+ }
3188
3386
  async _fetchAndDecode(src) {
3189
3387
  if (this._audioCache.has(src)) {
3190
3388
  return this._audioCache.get(src);
@@ -3197,6 +3395,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
3197
3395
  );
3198
3396
  }
3199
3397
  const arrayBuffer = await response.arrayBuffer();
3398
+ await this._ensureContextConfigured();
3200
3399
  const { getGlobalAudioContext } = await import("@waveform-playlist/playout");
3201
3400
  return getGlobalAudioContext().decodeAudioData(arrayBuffer);
3202
3401
  })();
@@ -3208,6 +3407,16 @@ var DawEditorElement = class extends import_lit12.LitElement {
3208
3407
  throw err;
3209
3408
  }
3210
3409
  }
3410
+ _fetchPeaks(src) {
3411
+ const cached = this._peaksCache.get(src);
3412
+ if (cached) return cached;
3413
+ const promise = loadWaveformDataFromUrl(src).catch((err) => {
3414
+ this._peaksCache.delete(src);
3415
+ throw err;
3416
+ });
3417
+ this._peaksCache.set(src, promise);
3418
+ return promise;
3419
+ }
3211
3420
  _recomputeDuration() {
3212
3421
  let maxSample = 0;
3213
3422
  for (const track of this._engineTracks.values()) {
@@ -3620,8 +3829,8 @@ DawEditorElement.styles = [
3620
3829
  ];
3621
3830
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
3622
3831
  __decorateClass([
3623
- (0, import_decorators10.property)({ type: Number, attribute: "samples-per-pixel" })
3624
- ], DawEditorElement.prototype, "samplesPerPixel", 2);
3832
+ (0, import_decorators10.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
3833
+ ], DawEditorElement.prototype, "samplesPerPixel", 1);
3625
3834
  __decorateClass([
3626
3835
  (0, import_decorators10.property)({ type: Number, attribute: "wave-height" })
3627
3836
  ], DawEditorElement.prototype, "waveHeight", 2);