@dawcore/components 0.0.2 → 0.0.4
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 +107 -9
- package/dist/index.d.ts +107 -9
- package/dist/index.js +471 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +470 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
ClipPointerHandler: () => ClipPointerHandler,
|
|
43
43
|
DawClipElement: () => DawClipElement,
|
|
44
44
|
DawEditorElement: () => DawEditorElement,
|
|
45
|
+
DawKeyboardShortcutsElement: () => DawKeyboardShortcutsElement,
|
|
45
46
|
DawPauseButtonElement: () => DawPauseButtonElement,
|
|
46
47
|
DawPlayButtonElement: () => DawPlayButtonElement,
|
|
47
48
|
DawPlayheadElement: () => DawPlayheadElement,
|
|
@@ -1133,6 +1134,13 @@ var PeakPipeline = class {
|
|
|
1133
1134
|
this._baseScale = baseScale;
|
|
1134
1135
|
this._bits = bits;
|
|
1135
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
|
+
}
|
|
1136
1144
|
/**
|
|
1137
1145
|
* Generate PeakData for a clip from its AudioBuffer.
|
|
1138
1146
|
* Uses cached WaveformData when available; otherwise generates via worker.
|
|
@@ -1140,8 +1148,9 @@ var PeakPipeline = class {
|
|
|
1140
1148
|
*/
|
|
1141
1149
|
async generatePeaks(audioBuffer, samplesPerPixel, isMono, offsetSamples, durationSamples) {
|
|
1142
1150
|
const waveformData = await this._getWaveformData(audioBuffer);
|
|
1151
|
+
const effectiveScale = this._clampScale(waveformData, samplesPerPixel);
|
|
1143
1152
|
try {
|
|
1144
|
-
return extractPeaks(waveformData,
|
|
1153
|
+
return extractPeaks(waveformData, effectiveScale, isMono, offsetSamples, durationSamples);
|
|
1145
1154
|
} catch (err) {
|
|
1146
1155
|
console.warn("[dawcore] extractPeaks failed: " + String(err));
|
|
1147
1156
|
throw err;
|
|
@@ -1149,23 +1158,29 @@ var PeakPipeline = class {
|
|
|
1149
1158
|
}
|
|
1150
1159
|
/**
|
|
1151
1160
|
* Re-extract peaks for all clips at a new zoom level using cached WaveformData.
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
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.
|
|
1155
1164
|
*/
|
|
1156
1165
|
reextractPeaks(clipBuffers, samplesPerPixel, isMono, clipOffsets) {
|
|
1157
1166
|
const result = /* @__PURE__ */ new Map();
|
|
1167
|
+
let clampedCount = 0;
|
|
1168
|
+
let clampedScale = 0;
|
|
1158
1169
|
for (const [clipId, audioBuffer] of clipBuffers) {
|
|
1159
1170
|
const cached = this._cache.get(audioBuffer);
|
|
1160
1171
|
if (cached) {
|
|
1161
|
-
|
|
1172
|
+
const effectiveScale = this._clampScale(cached, samplesPerPixel, false);
|
|
1173
|
+
if (effectiveScale !== samplesPerPixel) {
|
|
1174
|
+
clampedCount++;
|
|
1175
|
+
clampedScale = effectiveScale;
|
|
1176
|
+
}
|
|
1162
1177
|
try {
|
|
1163
1178
|
const offsets = clipOffsets?.get(clipId);
|
|
1164
1179
|
result.set(
|
|
1165
1180
|
clipId,
|
|
1166
1181
|
extractPeaks(
|
|
1167
1182
|
cached,
|
|
1168
|
-
|
|
1183
|
+
effectiveScale,
|
|
1169
1184
|
isMono,
|
|
1170
1185
|
offsets?.offsetSamples,
|
|
1171
1186
|
offsets?.durationSamples
|
|
@@ -1176,8 +1191,42 @@ var PeakPipeline = class {
|
|
|
1176
1191
|
}
|
|
1177
1192
|
}
|
|
1178
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
|
+
}
|
|
1179
1199
|
return result;
|
|
1180
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
|
+
}
|
|
1181
1230
|
terminate() {
|
|
1182
1231
|
this._worker?.terminate();
|
|
1183
1232
|
this._worker = null;
|
|
@@ -2229,6 +2278,13 @@ var ClipPointerHandler = class {
|
|
|
2229
2278
|
this._isDragging = false;
|
|
2230
2279
|
this._lastDeltaPx = 0;
|
|
2231
2280
|
this._cumulativeDeltaSamples = 0;
|
|
2281
|
+
if (this._host.engine) {
|
|
2282
|
+
this._host.engine.beginTransaction();
|
|
2283
|
+
} else {
|
|
2284
|
+
console.warn(
|
|
2285
|
+
"[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2232
2288
|
if (mode === "trim-left" || mode === "trim-right") {
|
|
2233
2289
|
const container = this._host.shadowRoot?.querySelector(
|
|
2234
2290
|
`.clip-container[data-clip-id="${clipId}"]`
|
|
@@ -2267,8 +2323,8 @@ var ClipPointerHandler = class {
|
|
|
2267
2323
|
const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
|
|
2268
2324
|
this._lastDeltaPx = totalDeltaPx;
|
|
2269
2325
|
const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
|
|
2270
|
-
this.
|
|
2271
|
-
|
|
2326
|
+
const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
|
|
2327
|
+
this._cumulativeDeltaSamples += applied;
|
|
2272
2328
|
} else {
|
|
2273
2329
|
const boundary = this._mode === "trim-left" ? "left" : "right";
|
|
2274
2330
|
const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
|
|
@@ -2357,9 +2413,18 @@ var ClipPointerHandler = class {
|
|
|
2357
2413
|
}
|
|
2358
2414
|
})
|
|
2359
2415
|
);
|
|
2416
|
+
} else {
|
|
2417
|
+
console.warn(
|
|
2418
|
+
"[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
|
|
2419
|
+
);
|
|
2360
2420
|
}
|
|
2361
2421
|
}
|
|
2362
2422
|
} finally {
|
|
2423
|
+
if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
|
|
2424
|
+
this._host.engine?.commitTransaction();
|
|
2425
|
+
} else {
|
|
2426
|
+
this._host.engine?.abortTransaction();
|
|
2427
|
+
}
|
|
2363
2428
|
this._reset();
|
|
2364
2429
|
}
|
|
2365
2430
|
}
|
|
@@ -2470,6 +2535,7 @@ async function loadFiles(host, files) {
|
|
|
2470
2535
|
clips: [
|
|
2471
2536
|
{
|
|
2472
2537
|
src: "",
|
|
2538
|
+
peaksSrc: "",
|
|
2473
2539
|
start: 0,
|
|
2474
2540
|
duration: audioBuffer.duration,
|
|
2475
2541
|
offset: 0,
|
|
@@ -2555,6 +2621,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
|
|
|
2555
2621
|
const sr = host.effectiveSampleRate;
|
|
2556
2622
|
const clipDesc = {
|
|
2557
2623
|
src: "",
|
|
2624
|
+
peaksSrc: "",
|
|
2558
2625
|
start: startSample / sr,
|
|
2559
2626
|
duration: durSamples / sr,
|
|
2560
2627
|
offset: 0,
|
|
@@ -2595,6 +2662,35 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
|
|
|
2595
2662
|
|
|
2596
2663
|
// src/interactions/split-handler.ts
|
|
2597
2664
|
function splitAtPlayhead(host) {
|
|
2665
|
+
const wasPlaying = host.isPlaying;
|
|
2666
|
+
const time = host.currentTime;
|
|
2667
|
+
if (!canSplitAtTime(host, time)) return false;
|
|
2668
|
+
if (wasPlaying) {
|
|
2669
|
+
host.stop();
|
|
2670
|
+
}
|
|
2671
|
+
let result;
|
|
2672
|
+
try {
|
|
2673
|
+
result = performSplit(host, time);
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
|
|
2676
|
+
result = false;
|
|
2677
|
+
}
|
|
2678
|
+
if (wasPlaying) {
|
|
2679
|
+
host.play(time);
|
|
2680
|
+
}
|
|
2681
|
+
return result;
|
|
2682
|
+
}
|
|
2683
|
+
function canSplitAtTime(host, time) {
|
|
2684
|
+
const { engine } = host;
|
|
2685
|
+
if (!engine) return false;
|
|
2686
|
+
const state5 = engine.getState();
|
|
2687
|
+
if (!state5.selectedTrackId) return false;
|
|
2688
|
+
const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
|
|
2689
|
+
if (!track) return false;
|
|
2690
|
+
const atSample = Math.round(time * host.effectiveSampleRate);
|
|
2691
|
+
return !!findClipAtSample(track.clips, atSample);
|
|
2692
|
+
}
|
|
2693
|
+
function performSplit(host, time) {
|
|
2598
2694
|
const { engine } = host;
|
|
2599
2695
|
if (!engine) return false;
|
|
2600
2696
|
const stateBefore = engine.getState();
|
|
@@ -2602,7 +2698,7 @@ function splitAtPlayhead(host) {
|
|
|
2602
2698
|
if (!selectedTrackId) return false;
|
|
2603
2699
|
const track = tracks.find((t) => t.id === selectedTrackId);
|
|
2604
2700
|
if (!track) return false;
|
|
2605
|
-
const atSample = Math.round(
|
|
2701
|
+
const atSample = Math.round(time * host.effectiveSampleRate);
|
|
2606
2702
|
const clip = findClipAtSample(track.clips, atSample);
|
|
2607
2703
|
if (!clip) return false;
|
|
2608
2704
|
const originalClipId = clip.id;
|
|
@@ -2727,11 +2823,29 @@ function findAudioBufferForClip(host, clip, track) {
|
|
|
2727
2823
|
return null;
|
|
2728
2824
|
}
|
|
2729
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
|
+
|
|
2730
2844
|
// src/elements/daw-editor.ts
|
|
2731
2845
|
var DawEditorElement = class extends import_lit12.LitElement {
|
|
2732
2846
|
constructor() {
|
|
2733
2847
|
super(...arguments);
|
|
2734
|
-
this.
|
|
2848
|
+
this._samplesPerPixel = 1024;
|
|
2735
2849
|
this.waveHeight = 128;
|
|
2736
2850
|
this.timescale = false;
|
|
2737
2851
|
this.mono = false;
|
|
@@ -2758,9 +2872,12 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
2758
2872
|
this._engine = null;
|
|
2759
2873
|
this._enginePromise = null;
|
|
2760
2874
|
this._audioCache = /* @__PURE__ */ new Map();
|
|
2875
|
+
this._peaksCache = /* @__PURE__ */ new Map();
|
|
2761
2876
|
this._clipBuffers = /* @__PURE__ */ new Map();
|
|
2762
2877
|
this._clipOffsets = /* @__PURE__ */ new Map();
|
|
2763
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;
|
|
2764
2881
|
this._trackElements = /* @__PURE__ */ new Map();
|
|
2765
2882
|
this._childObserver = null;
|
|
2766
2883
|
this._audioResume = new AudioResumeController(this);
|
|
@@ -2809,6 +2926,19 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
2809
2926
|
this._onTrackControl = (e) => {
|
|
2810
2927
|
const { trackId, prop, value } = e.detail ?? {};
|
|
2811
2928
|
if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
|
|
2929
|
+
if (this._selectedTrackId !== trackId) {
|
|
2930
|
+
this._setSelectedTrackId(trackId);
|
|
2931
|
+
if (this._engine) {
|
|
2932
|
+
this._engine.selectTrack(trackId);
|
|
2933
|
+
}
|
|
2934
|
+
this.dispatchEvent(
|
|
2935
|
+
new CustomEvent("daw-track-select", {
|
|
2936
|
+
bubbles: true,
|
|
2937
|
+
composed: true,
|
|
2938
|
+
detail: { trackId }
|
|
2939
|
+
})
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2812
2942
|
const oldDescriptor = this._tracks.get(trackId);
|
|
2813
2943
|
if (oldDescriptor) {
|
|
2814
2944
|
const descriptor = { ...oldDescriptor, [prop]: value };
|
|
@@ -2866,20 +2996,24 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
2866
2996
|
);
|
|
2867
2997
|
}
|
|
2868
2998
|
};
|
|
2869
|
-
this._onKeyDown = (e) => {
|
|
2870
|
-
if (!this.interactiveClips) return;
|
|
2871
|
-
if (e.key === "s" || e.key === "S") {
|
|
2872
|
-
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
2873
|
-
const tag = e.target?.tagName;
|
|
2874
|
-
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
2875
|
-
if (e.target?.isContentEditable) return;
|
|
2876
|
-
e.preventDefault();
|
|
2877
|
-
this.splitAtPlayhead();
|
|
2878
|
-
}
|
|
2879
|
-
};
|
|
2880
2999
|
// --- Recording ---
|
|
2881
3000
|
this.recordingStream = null;
|
|
2882
3001
|
}
|
|
3002
|
+
get samplesPerPixel() {
|
|
3003
|
+
return this._samplesPerPixel;
|
|
3004
|
+
}
|
|
3005
|
+
set samplesPerPixel(value) {
|
|
3006
|
+
const old = this._samplesPerPixel;
|
|
3007
|
+
if (!Number.isFinite(value) || value <= 0) return;
|
|
3008
|
+
const clamped = this._minSamplesPerPixel > 0 && value < this._minSamplesPerPixel ? this._minSamplesPerPixel : value;
|
|
3009
|
+
if (clamped !== value) {
|
|
3010
|
+
console.warn(
|
|
3011
|
+
"[dawcore] Zoom " + value + " spp rejected \u2014 pre-computed peaks limit is " + this._minSamplesPerPixel + " spp"
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
this._samplesPerPixel = clamped;
|
|
3015
|
+
this.requestUpdate("samplesPerPixel", old);
|
|
3016
|
+
}
|
|
2883
3017
|
get _clipHandler() {
|
|
2884
3018
|
return this.interactiveClips ? this._clipPointer : null;
|
|
2885
3019
|
}
|
|
@@ -2942,10 +3076,6 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
2942
3076
|
// --- Lifecycle ---
|
|
2943
3077
|
connectedCallback() {
|
|
2944
3078
|
super.connectedCallback();
|
|
2945
|
-
if (!this.hasAttribute("tabindex")) {
|
|
2946
|
-
this.setAttribute("tabindex", "0");
|
|
2947
|
-
}
|
|
2948
|
-
this.addEventListener("keydown", this._onKeyDown);
|
|
2949
3079
|
this.addEventListener("daw-track-connected", this._onTrackConnected);
|
|
2950
3080
|
this.addEventListener("daw-track-update", this._onTrackUpdate);
|
|
2951
3081
|
this.addEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -2971,7 +3101,6 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
2971
3101
|
}
|
|
2972
3102
|
disconnectedCallback() {
|
|
2973
3103
|
super.disconnectedCallback();
|
|
2974
|
-
this.removeEventListener("keydown", this._onKeyDown);
|
|
2975
3104
|
this.removeEventListener("daw-track-connected", this._onTrackConnected);
|
|
2976
3105
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
2977
3106
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -2980,9 +3109,11 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
2980
3109
|
this._childObserver = null;
|
|
2981
3110
|
this._trackElements.clear();
|
|
2982
3111
|
this._audioCache.clear();
|
|
3112
|
+
this._peaksCache.clear();
|
|
2983
3113
|
this._clipBuffers.clear();
|
|
2984
3114
|
this._clipOffsets.clear();
|
|
2985
3115
|
this._peakPipeline.terminate();
|
|
3116
|
+
this._minSamplesPerPixel = 0;
|
|
2986
3117
|
try {
|
|
2987
3118
|
this._disposeEngine();
|
|
2988
3119
|
} catch (err) {
|
|
@@ -3032,6 +3163,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
3032
3163
|
if (this._engine) {
|
|
3033
3164
|
this._engine.removeTrack(trackId);
|
|
3034
3165
|
}
|
|
3166
|
+
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
3035
3167
|
if (nextEngine.size === 0) {
|
|
3036
3168
|
this._currentTime = 0;
|
|
3037
3169
|
this._stopPlayhead();
|
|
@@ -3043,6 +3175,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
3043
3175
|
if (clipEls.length === 0 && trackEl.src) {
|
|
3044
3176
|
clips.push({
|
|
3045
3177
|
src: trackEl.src,
|
|
3178
|
+
peaksSrc: "",
|
|
3046
3179
|
start: 0,
|
|
3047
3180
|
duration: 0,
|
|
3048
3181
|
offset: 0,
|
|
@@ -3056,6 +3189,7 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
3056
3189
|
for (const clipEl of clipEls) {
|
|
3057
3190
|
clips.push({
|
|
3058
3191
|
src: clipEl.src,
|
|
3192
|
+
peaksSrc: clipEl.peaksSrc,
|
|
3059
3193
|
start: clipEl.start,
|
|
3060
3194
|
duration: clipEl.duration,
|
|
3061
3195
|
offset: clipEl.offset,
|
|
@@ -3083,7 +3217,77 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
3083
3217
|
const clips = [];
|
|
3084
3218
|
for (const clipDesc of descriptor.clips) {
|
|
3085
3219
|
if (!clipDesc.src) continue;
|
|
3086
|
-
const
|
|
3220
|
+
const waveformDataPromise = clipDesc.peaksSrc ? this._fetchPeaks(clipDesc.peaksSrc) : null;
|
|
3221
|
+
const audioPromise = this._fetchAndDecode(clipDesc.src);
|
|
3222
|
+
let waveformData = null;
|
|
3223
|
+
if (waveformDataPromise) {
|
|
3224
|
+
try {
|
|
3225
|
+
waveformData = await waveformDataPromise;
|
|
3226
|
+
} catch (err) {
|
|
3227
|
+
console.warn(
|
|
3228
|
+
"[dawcore] Failed to load peaks from " + clipDesc.peaksSrc + ": " + String(err) + " \u2014 falling back to AudioBuffer generation"
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (waveformData) {
|
|
3233
|
+
const clip2 = (0, import_core4.createClipFromSeconds)({
|
|
3234
|
+
waveformData,
|
|
3235
|
+
startTime: clipDesc.start,
|
|
3236
|
+
duration: clipDesc.duration || waveformData.duration,
|
|
3237
|
+
offset: clipDesc.offset,
|
|
3238
|
+
gain: clipDesc.gain,
|
|
3239
|
+
name: clipDesc.name,
|
|
3240
|
+
sampleRate: waveformData.sample_rate,
|
|
3241
|
+
sourceDuration: waveformData.duration
|
|
3242
|
+
});
|
|
3243
|
+
const effectiveScale = Math.max(this.samplesPerPixel, waveformData.scale);
|
|
3244
|
+
const peakData2 = extractPeaks(
|
|
3245
|
+
waveformData,
|
|
3246
|
+
effectiveScale,
|
|
3247
|
+
this.mono,
|
|
3248
|
+
clip2.offsetSamples,
|
|
3249
|
+
clip2.durationSamples
|
|
3250
|
+
);
|
|
3251
|
+
this._clipOffsets.set(clip2.id, {
|
|
3252
|
+
offsetSamples: clip2.offsetSamples,
|
|
3253
|
+
durationSamples: clip2.durationSamples
|
|
3254
|
+
});
|
|
3255
|
+
this._peaksData = new Map(this._peaksData).set(clip2.id, peakData2);
|
|
3256
|
+
this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
|
|
3257
|
+
const previewTrack = (0, import_core4.createTrack)({
|
|
3258
|
+
name: descriptor.name,
|
|
3259
|
+
clips: [clip2],
|
|
3260
|
+
volume: descriptor.volume,
|
|
3261
|
+
pan: descriptor.pan,
|
|
3262
|
+
muted: descriptor.muted,
|
|
3263
|
+
soloed: descriptor.soloed
|
|
3264
|
+
});
|
|
3265
|
+
previewTrack.id = trackId;
|
|
3266
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
|
|
3267
|
+
this._recomputeDuration();
|
|
3268
|
+
let audioBuffer2;
|
|
3269
|
+
try {
|
|
3270
|
+
audioBuffer2 = await audioPromise;
|
|
3271
|
+
} catch (audioErr) {
|
|
3272
|
+
const nextPeaks = new Map(this._peaksData);
|
|
3273
|
+
nextPeaks.delete(clip2.id);
|
|
3274
|
+
this._peaksData = nextPeaks;
|
|
3275
|
+
this._clipOffsets.delete(clip2.id);
|
|
3276
|
+
const nextEngine = new Map(this._engineTracks);
|
|
3277
|
+
nextEngine.delete(trackId);
|
|
3278
|
+
this._engineTracks = nextEngine;
|
|
3279
|
+
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
3280
|
+
this._recomputeDuration();
|
|
3281
|
+
throw audioErr;
|
|
3282
|
+
}
|
|
3283
|
+
this._resolvedSampleRate = audioBuffer2.sampleRate;
|
|
3284
|
+
const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
|
|
3285
|
+
this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
|
|
3286
|
+
this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
|
|
3287
|
+
clips.push(updatedClip);
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
const audioBuffer = await audioPromise;
|
|
3087
3291
|
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
3088
3292
|
const clip = (0, import_core4.createClipFromSeconds)({
|
|
3089
3293
|
audioBuffer,
|
|
@@ -3165,6 +3369,16 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
3165
3369
|
throw err;
|
|
3166
3370
|
}
|
|
3167
3371
|
}
|
|
3372
|
+
_fetchPeaks(src) {
|
|
3373
|
+
const cached = this._peaksCache.get(src);
|
|
3374
|
+
if (cached) return cached;
|
|
3375
|
+
const promise = loadWaveformDataFromUrl(src).catch((err) => {
|
|
3376
|
+
this._peaksCache.delete(src);
|
|
3377
|
+
throw err;
|
|
3378
|
+
});
|
|
3379
|
+
this._peaksCache.set(src, promise);
|
|
3380
|
+
return promise;
|
|
3381
|
+
}
|
|
3168
3382
|
_recomputeDuration() {
|
|
3169
3383
|
let maxSample = 0;
|
|
3170
3384
|
for (const track of this._engineTracks.values()) {
|
|
@@ -3263,18 +3477,71 @@ var DawEditorElement = class extends import_lit12.LitElement {
|
|
|
3263
3477
|
this._stopPlayhead();
|
|
3264
3478
|
this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
|
|
3265
3479
|
}
|
|
3480
|
+
/** Toggle between play and pause. */
|
|
3481
|
+
togglePlayPause() {
|
|
3482
|
+
if (this._isPlaying) {
|
|
3483
|
+
this.pause();
|
|
3484
|
+
} else {
|
|
3485
|
+
this.play();
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3266
3488
|
seekTo(time) {
|
|
3267
|
-
if (!this._engine)
|
|
3268
|
-
|
|
3269
|
-
|
|
3489
|
+
if (!this._engine) {
|
|
3490
|
+
console.warn("[dawcore] seekTo: engine not ready, call ignored");
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
if (this._isPlaying) {
|
|
3494
|
+
this.stop();
|
|
3495
|
+
this.play(time);
|
|
3496
|
+
} else {
|
|
3497
|
+
this._engine.seek(time);
|
|
3498
|
+
this._currentTime = time;
|
|
3499
|
+
this._stopPlayhead();
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
/** Undo the last structural edit. */
|
|
3503
|
+
undo() {
|
|
3504
|
+
if (!this._engine) {
|
|
3505
|
+
console.warn("[dawcore] undo: engine not ready, call ignored");
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
this._engine.undo();
|
|
3509
|
+
}
|
|
3510
|
+
/** Redo the last undone edit. */
|
|
3511
|
+
redo() {
|
|
3512
|
+
if (!this._engine) {
|
|
3513
|
+
console.warn("[dawcore] redo: engine not ready, call ignored");
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
this._engine.redo();
|
|
3517
|
+
}
|
|
3518
|
+
/** Whether undo is available. */
|
|
3519
|
+
get canUndo() {
|
|
3520
|
+
return this._engine?.canUndo ?? false;
|
|
3521
|
+
}
|
|
3522
|
+
/** Whether redo is available. */
|
|
3523
|
+
get canRedo() {
|
|
3524
|
+
return this._engine?.canRedo ?? false;
|
|
3270
3525
|
}
|
|
3271
3526
|
/** Split the clip under the playhead on the selected track. */
|
|
3272
3527
|
splitAtPlayhead() {
|
|
3273
3528
|
return splitAtPlayhead({
|
|
3274
3529
|
effectiveSampleRate: this.effectiveSampleRate,
|
|
3275
3530
|
currentTime: this._currentTime,
|
|
3531
|
+
isPlaying: this._isPlaying,
|
|
3276
3532
|
engine: this._engine,
|
|
3277
|
-
dispatchEvent: (e) => this.dispatchEvent(e)
|
|
3533
|
+
dispatchEvent: (e) => this.dispatchEvent(e),
|
|
3534
|
+
stop: () => {
|
|
3535
|
+
this._engine?.stop();
|
|
3536
|
+
this._stopPlayhead();
|
|
3537
|
+
},
|
|
3538
|
+
// Call engine.play directly (synchronous) — not the async editor play()
|
|
3539
|
+
// which yields to microtask queue via await engine.init(). Engine is
|
|
3540
|
+
// already initialized at split time; the async gap causes audio desync.
|
|
3541
|
+
play: (time) => {
|
|
3542
|
+
this._engine?.play(time);
|
|
3543
|
+
this._startPlayhead();
|
|
3544
|
+
}
|
|
3278
3545
|
});
|
|
3279
3546
|
}
|
|
3280
3547
|
get currentTime() {
|
|
@@ -3524,8 +3791,8 @@ DawEditorElement.styles = [
|
|
|
3524
3791
|
];
|
|
3525
3792
|
DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
|
|
3526
3793
|
__decorateClass([
|
|
3527
|
-
(0, import_decorators10.property)({ type: Number, attribute: "samples-per-pixel" })
|
|
3528
|
-
], DawEditorElement.prototype, "samplesPerPixel",
|
|
3794
|
+
(0, import_decorators10.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
|
|
3795
|
+
], DawEditorElement.prototype, "samplesPerPixel", 1);
|
|
3529
3796
|
__decorateClass([
|
|
3530
3797
|
(0, import_decorators10.property)({ type: Number, attribute: "wave-height" })
|
|
3531
3798
|
], DawEditorElement.prototype, "waveHeight", 2);
|
|
@@ -3874,12 +4141,183 @@ __decorateClass([
|
|
|
3874
4141
|
DawRecordButtonElement = __decorateClass([
|
|
3875
4142
|
(0, import_decorators13.customElement)("daw-record-button")
|
|
3876
4143
|
], DawRecordButtonElement);
|
|
4144
|
+
|
|
4145
|
+
// src/elements/daw-keyboard-shortcuts.ts
|
|
4146
|
+
var import_lit16 = require("lit");
|
|
4147
|
+
var import_decorators14 = require("lit/decorators.js");
|
|
4148
|
+
var import_core5 = require("@waveform-playlist/core");
|
|
4149
|
+
var DawKeyboardShortcutsElement = class extends import_lit16.LitElement {
|
|
4150
|
+
constructor() {
|
|
4151
|
+
super(...arguments);
|
|
4152
|
+
this.playback = false;
|
|
4153
|
+
this.splitting = false;
|
|
4154
|
+
this.undo = false;
|
|
4155
|
+
// --- JS properties for remapping ---
|
|
4156
|
+
this.playbackShortcuts = null;
|
|
4157
|
+
this.splittingShortcuts = null;
|
|
4158
|
+
this.undoShortcuts = null;
|
|
4159
|
+
/** Additional custom shortcuts. */
|
|
4160
|
+
this.customShortcuts = [];
|
|
4161
|
+
this._editor = null;
|
|
4162
|
+
this._cachedShortcuts = null;
|
|
4163
|
+
// --- Event handler ---
|
|
4164
|
+
this._onKeyDown = (e) => {
|
|
4165
|
+
const shortcuts = this.shortcuts;
|
|
4166
|
+
if (shortcuts.length === 0) return;
|
|
4167
|
+
try {
|
|
4168
|
+
(0, import_core5.handleKeyboardEvent)(e, shortcuts, true);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
|
|
4171
|
+
const target = this._editor ?? this;
|
|
4172
|
+
target.dispatchEvent(
|
|
4173
|
+
new CustomEvent("daw-error", {
|
|
4174
|
+
bubbles: true,
|
|
4175
|
+
composed: true,
|
|
4176
|
+
detail: { operation: "keyboard-shortcut", key: e.key, error: err }
|
|
4177
|
+
})
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
/** All active shortcuts (read-only, cached). */
|
|
4183
|
+
get shortcuts() {
|
|
4184
|
+
if (!this._cachedShortcuts) {
|
|
4185
|
+
this._cachedShortcuts = this._buildShortcuts();
|
|
4186
|
+
}
|
|
4187
|
+
return this._cachedShortcuts;
|
|
4188
|
+
}
|
|
4189
|
+
/** Invalidate cached shortcuts when Lit properties change. */
|
|
4190
|
+
updated() {
|
|
4191
|
+
this._cachedShortcuts = null;
|
|
4192
|
+
}
|
|
4193
|
+
// --- Lifecycle ---
|
|
4194
|
+
connectedCallback() {
|
|
4195
|
+
super.connectedCallback();
|
|
4196
|
+
this._editor = this.closest("daw-editor");
|
|
4197
|
+
if (!this._editor) {
|
|
4198
|
+
console.warn(
|
|
4199
|
+
"[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
|
|
4200
|
+
);
|
|
4201
|
+
}
|
|
4202
|
+
document.addEventListener("keydown", this._onKeyDown);
|
|
4203
|
+
}
|
|
4204
|
+
disconnectedCallback() {
|
|
4205
|
+
super.disconnectedCallback();
|
|
4206
|
+
document.removeEventListener("keydown", this._onKeyDown);
|
|
4207
|
+
this._editor = null;
|
|
4208
|
+
}
|
|
4209
|
+
// No shadow DOM — render-less element
|
|
4210
|
+
createRenderRoot() {
|
|
4211
|
+
return this;
|
|
4212
|
+
}
|
|
4213
|
+
// --- Shortcut building ---
|
|
4214
|
+
_buildShortcuts() {
|
|
4215
|
+
const editor = this._editor;
|
|
4216
|
+
if (!editor) return this.customShortcuts;
|
|
4217
|
+
const result = [];
|
|
4218
|
+
if (this.playback) {
|
|
4219
|
+
const map = this.playbackShortcuts;
|
|
4220
|
+
result.push(
|
|
4221
|
+
this._makeShortcut(
|
|
4222
|
+
map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
|
|
4223
|
+
() => editor.togglePlayPause(),
|
|
4224
|
+
"Play/Pause"
|
|
4225
|
+
),
|
|
4226
|
+
this._makeShortcut(
|
|
4227
|
+
map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
|
|
4228
|
+
() => editor.stop(),
|
|
4229
|
+
"Stop"
|
|
4230
|
+
),
|
|
4231
|
+
this._makeShortcut(
|
|
4232
|
+
map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
|
|
4233
|
+
() => editor.seekTo(0),
|
|
4234
|
+
"Rewind to start"
|
|
4235
|
+
)
|
|
4236
|
+
);
|
|
4237
|
+
}
|
|
4238
|
+
if (this.splitting) {
|
|
4239
|
+
const map = this.splittingShortcuts;
|
|
4240
|
+
const binding = map?.splitAtPlayhead ?? {
|
|
4241
|
+
key: "s",
|
|
4242
|
+
ctrlKey: false,
|
|
4243
|
+
metaKey: false,
|
|
4244
|
+
altKey: false
|
|
4245
|
+
};
|
|
4246
|
+
result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
|
|
4247
|
+
}
|
|
4248
|
+
if (this.undo) {
|
|
4249
|
+
const map = this.undoShortcuts;
|
|
4250
|
+
const undoBinding = map?.undo ?? { key: "z" };
|
|
4251
|
+
const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
|
|
4252
|
+
if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
|
|
4253
|
+
const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
|
|
4254
|
+
result.push(
|
|
4255
|
+
this._makeShortcut(
|
|
4256
|
+
{ ...undoBinding, ctrlKey: true, ...undoShift },
|
|
4257
|
+
() => editor.undo(),
|
|
4258
|
+
"Undo"
|
|
4259
|
+
),
|
|
4260
|
+
this._makeShortcut(
|
|
4261
|
+
{ ...undoBinding, metaKey: true, ...undoShift },
|
|
4262
|
+
() => editor.undo(),
|
|
4263
|
+
"Undo"
|
|
4264
|
+
)
|
|
4265
|
+
);
|
|
4266
|
+
} else {
|
|
4267
|
+
result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
|
|
4268
|
+
}
|
|
4269
|
+
if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
|
|
4270
|
+
const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
|
|
4271
|
+
result.push(
|
|
4272
|
+
this._makeShortcut(
|
|
4273
|
+
{ ...redoBinding, ctrlKey: true, ...redoShift },
|
|
4274
|
+
() => editor.redo(),
|
|
4275
|
+
"Redo"
|
|
4276
|
+
),
|
|
4277
|
+
this._makeShortcut(
|
|
4278
|
+
{ ...redoBinding, metaKey: true, ...redoShift },
|
|
4279
|
+
() => editor.redo(),
|
|
4280
|
+
"Redo"
|
|
4281
|
+
)
|
|
4282
|
+
);
|
|
4283
|
+
} else {
|
|
4284
|
+
result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
result.push(...this.customShortcuts);
|
|
4288
|
+
return result;
|
|
4289
|
+
}
|
|
4290
|
+
_makeShortcut(binding, action, description) {
|
|
4291
|
+
return {
|
|
4292
|
+
key: binding.key,
|
|
4293
|
+
...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
|
|
4294
|
+
...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
|
|
4295
|
+
...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
|
|
4296
|
+
...binding.altKey !== void 0 && { altKey: binding.altKey },
|
|
4297
|
+
action,
|
|
4298
|
+
description
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
};
|
|
4302
|
+
__decorateClass([
|
|
4303
|
+
(0, import_decorators14.property)({ type: Boolean })
|
|
4304
|
+
], DawKeyboardShortcutsElement.prototype, "playback", 2);
|
|
4305
|
+
__decorateClass([
|
|
4306
|
+
(0, import_decorators14.property)({ type: Boolean })
|
|
4307
|
+
], DawKeyboardShortcutsElement.prototype, "splitting", 2);
|
|
4308
|
+
__decorateClass([
|
|
4309
|
+
(0, import_decorators14.property)({ type: Boolean })
|
|
4310
|
+
], DawKeyboardShortcutsElement.prototype, "undo", 2);
|
|
4311
|
+
DawKeyboardShortcutsElement = __decorateClass([
|
|
4312
|
+
(0, import_decorators14.customElement)("daw-keyboard-shortcuts")
|
|
4313
|
+
], DawKeyboardShortcutsElement);
|
|
3877
4314
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3878
4315
|
0 && (module.exports = {
|
|
3879
4316
|
AudioResumeController,
|
|
3880
4317
|
ClipPointerHandler,
|
|
3881
4318
|
DawClipElement,
|
|
3882
4319
|
DawEditorElement,
|
|
4320
|
+
DawKeyboardShortcutsElement,
|
|
3883
4321
|
DawPauseButtonElement,
|
|
3884
4322
|
DawPlayButtonElement,
|
|
3885
4323
|
DawPlayheadElement,
|