@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 +38 -5
- package/dist/index.d.ts +38 -5
- package/dist/index.js +219 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
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
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
-
/**
|
|
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,
|
|
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
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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",
|
|
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);
|