@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.mjs
CHANGED
|
@@ -1083,6 +1083,13 @@ var PeakPipeline = class {
|
|
|
1083
1083
|
this._baseScale = baseScale;
|
|
1084
1084
|
this._bits = bits;
|
|
1085
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Inject externally-loaded WaveformData (e.g., from a .dat file) into the cache.
|
|
1088
|
+
* Prevents worker generation for this AudioBuffer on all subsequent calls.
|
|
1089
|
+
*/
|
|
1090
|
+
cacheWaveformData(audioBuffer, waveformData) {
|
|
1091
|
+
this._cache.set(audioBuffer, waveformData);
|
|
1092
|
+
}
|
|
1086
1093
|
/**
|
|
1087
1094
|
* Generate PeakData for a clip from its AudioBuffer.
|
|
1088
1095
|
* Uses cached WaveformData when available; otherwise generates via worker.
|
|
@@ -1090,8 +1097,9 @@ var PeakPipeline = class {
|
|
|
1090
1097
|
*/
|
|
1091
1098
|
async generatePeaks(audioBuffer, samplesPerPixel, isMono, offsetSamples, durationSamples) {
|
|
1092
1099
|
const waveformData = await this._getWaveformData(audioBuffer);
|
|
1100
|
+
const effectiveScale = this._clampScale(waveformData, samplesPerPixel);
|
|
1093
1101
|
try {
|
|
1094
|
-
return extractPeaks(waveformData,
|
|
1102
|
+
return extractPeaks(waveformData, effectiveScale, isMono, offsetSamples, durationSamples);
|
|
1095
1103
|
} catch (err) {
|
|
1096
1104
|
console.warn("[dawcore] extractPeaks failed: " + String(err));
|
|
1097
1105
|
throw err;
|
|
@@ -1099,23 +1107,29 @@ var PeakPipeline = class {
|
|
|
1099
1107
|
}
|
|
1100
1108
|
/**
|
|
1101
1109
|
* Re-extract peaks for all clips at a new zoom level using cached WaveformData.
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1110
|
+
* Returns a new Map of clipId → PeakData. Clips without cached data are skipped.
|
|
1111
|
+
* When the requested scale is finer than cached data, peaks are clamped to the
|
|
1112
|
+
* cached scale and a single summary warning is logged.
|
|
1105
1113
|
*/
|
|
1106
1114
|
reextractPeaks(clipBuffers, samplesPerPixel, isMono, clipOffsets) {
|
|
1107
1115
|
const result = /* @__PURE__ */ new Map();
|
|
1116
|
+
let clampedCount = 0;
|
|
1117
|
+
let clampedScale = 0;
|
|
1108
1118
|
for (const [clipId, audioBuffer] of clipBuffers) {
|
|
1109
1119
|
const cached = this._cache.get(audioBuffer);
|
|
1110
1120
|
if (cached) {
|
|
1111
|
-
|
|
1121
|
+
const effectiveScale = this._clampScale(cached, samplesPerPixel, false);
|
|
1122
|
+
if (effectiveScale !== samplesPerPixel) {
|
|
1123
|
+
clampedCount++;
|
|
1124
|
+
clampedScale = effectiveScale;
|
|
1125
|
+
}
|
|
1112
1126
|
try {
|
|
1113
1127
|
const offsets = clipOffsets?.get(clipId);
|
|
1114
1128
|
result.set(
|
|
1115
1129
|
clipId,
|
|
1116
1130
|
extractPeaks(
|
|
1117
1131
|
cached,
|
|
1118
|
-
|
|
1132
|
+
effectiveScale,
|
|
1119
1133
|
isMono,
|
|
1120
1134
|
offsets?.offsetSamples,
|
|
1121
1135
|
offsets?.durationSamples
|
|
@@ -1126,8 +1140,42 @@ var PeakPipeline = class {
|
|
|
1126
1140
|
}
|
|
1127
1141
|
}
|
|
1128
1142
|
}
|
|
1143
|
+
if (clampedCount > 0) {
|
|
1144
|
+
console.warn(
|
|
1145
|
+
"[dawcore] Requested zoom " + samplesPerPixel + " spp is finer than pre-computed peaks (" + clampedScale + " spp) \u2014 " + clampedCount + " clip(s) using available resolution"
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1129
1148
|
return result;
|
|
1130
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Clamp requested scale to cached WaveformData scale.
|
|
1152
|
+
* WaveformData.resample() can only go coarser — if the requested zoom is
|
|
1153
|
+
* finer than the cached data, use the cached scale. Set warn=true to log
|
|
1154
|
+
* (default); reextractPeaks passes false and logs a single summary instead.
|
|
1155
|
+
*/
|
|
1156
|
+
_clampScale(waveformData, requestedScale, warn = true) {
|
|
1157
|
+
if (requestedScale < waveformData.scale) {
|
|
1158
|
+
if (warn) {
|
|
1159
|
+
console.warn(
|
|
1160
|
+
"[dawcore] Requested zoom " + requestedScale + " spp is finer than pre-computed peaks (" + waveformData.scale + " spp) \u2014 using available resolution"
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
return waveformData.scale;
|
|
1164
|
+
}
|
|
1165
|
+
return requestedScale;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Return the coarsest (largest) scale among cached WaveformData entries
|
|
1169
|
+
* that correspond to the given clip buffers. Returns 0 if none are cached.
|
|
1170
|
+
*/
|
|
1171
|
+
getMaxCachedScale(clipBuffers) {
|
|
1172
|
+
let max = 0;
|
|
1173
|
+
for (const audioBuffer of clipBuffers.values()) {
|
|
1174
|
+
const cached = this._cache.get(audioBuffer);
|
|
1175
|
+
if (cached && cached.scale > max) max = cached.scale;
|
|
1176
|
+
}
|
|
1177
|
+
return max;
|
|
1178
|
+
}
|
|
1131
1179
|
terminate() {
|
|
1132
1180
|
this._worker?.terminate();
|
|
1133
1181
|
this._worker = null;
|
|
@@ -2179,6 +2227,13 @@ var ClipPointerHandler = class {
|
|
|
2179
2227
|
this._isDragging = false;
|
|
2180
2228
|
this._lastDeltaPx = 0;
|
|
2181
2229
|
this._cumulativeDeltaSamples = 0;
|
|
2230
|
+
if (this._host.engine) {
|
|
2231
|
+
this._host.engine.beginTransaction();
|
|
2232
|
+
} else {
|
|
2233
|
+
console.warn(
|
|
2234
|
+
"[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2182
2237
|
if (mode === "trim-left" || mode === "trim-right") {
|
|
2183
2238
|
const container = this._host.shadowRoot?.querySelector(
|
|
2184
2239
|
`.clip-container[data-clip-id="${clipId}"]`
|
|
@@ -2217,8 +2272,8 @@ var ClipPointerHandler = class {
|
|
|
2217
2272
|
const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
|
|
2218
2273
|
this._lastDeltaPx = totalDeltaPx;
|
|
2219
2274
|
const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
|
|
2220
|
-
this.
|
|
2221
|
-
|
|
2275
|
+
const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
|
|
2276
|
+
this._cumulativeDeltaSamples += applied;
|
|
2222
2277
|
} else {
|
|
2223
2278
|
const boundary = this._mode === "trim-left" ? "left" : "right";
|
|
2224
2279
|
const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
|
|
@@ -2307,9 +2362,18 @@ var ClipPointerHandler = class {
|
|
|
2307
2362
|
}
|
|
2308
2363
|
})
|
|
2309
2364
|
);
|
|
2365
|
+
} else {
|
|
2366
|
+
console.warn(
|
|
2367
|
+
"[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
|
|
2368
|
+
);
|
|
2310
2369
|
}
|
|
2311
2370
|
}
|
|
2312
2371
|
} finally {
|
|
2372
|
+
if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
|
|
2373
|
+
this._host.engine?.commitTransaction();
|
|
2374
|
+
} else {
|
|
2375
|
+
this._host.engine?.abortTransaction();
|
|
2376
|
+
}
|
|
2313
2377
|
this._reset();
|
|
2314
2378
|
}
|
|
2315
2379
|
}
|
|
@@ -2420,6 +2484,7 @@ async function loadFiles(host, files) {
|
|
|
2420
2484
|
clips: [
|
|
2421
2485
|
{
|
|
2422
2486
|
src: "",
|
|
2487
|
+
peaksSrc: "",
|
|
2423
2488
|
start: 0,
|
|
2424
2489
|
duration: audioBuffer.duration,
|
|
2425
2490
|
offset: 0,
|
|
@@ -2505,6 +2570,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
|
|
|
2505
2570
|
const sr = host.effectiveSampleRate;
|
|
2506
2571
|
const clipDesc = {
|
|
2507
2572
|
src: "",
|
|
2573
|
+
peaksSrc: "",
|
|
2508
2574
|
start: startSample / sr,
|
|
2509
2575
|
duration: durSamples / sr,
|
|
2510
2576
|
offset: 0,
|
|
@@ -2545,6 +2611,35 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
|
|
|
2545
2611
|
|
|
2546
2612
|
// src/interactions/split-handler.ts
|
|
2547
2613
|
function splitAtPlayhead(host) {
|
|
2614
|
+
const wasPlaying = host.isPlaying;
|
|
2615
|
+
const time = host.currentTime;
|
|
2616
|
+
if (!canSplitAtTime(host, time)) return false;
|
|
2617
|
+
if (wasPlaying) {
|
|
2618
|
+
host.stop();
|
|
2619
|
+
}
|
|
2620
|
+
let result;
|
|
2621
|
+
try {
|
|
2622
|
+
result = performSplit(host, time);
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
|
|
2625
|
+
result = false;
|
|
2626
|
+
}
|
|
2627
|
+
if (wasPlaying) {
|
|
2628
|
+
host.play(time);
|
|
2629
|
+
}
|
|
2630
|
+
return result;
|
|
2631
|
+
}
|
|
2632
|
+
function canSplitAtTime(host, time) {
|
|
2633
|
+
const { engine } = host;
|
|
2634
|
+
if (!engine) return false;
|
|
2635
|
+
const state5 = engine.getState();
|
|
2636
|
+
if (!state5.selectedTrackId) return false;
|
|
2637
|
+
const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
|
|
2638
|
+
if (!track) return false;
|
|
2639
|
+
const atSample = Math.round(time * host.effectiveSampleRate);
|
|
2640
|
+
return !!findClipAtSample(track.clips, atSample);
|
|
2641
|
+
}
|
|
2642
|
+
function performSplit(host, time) {
|
|
2548
2643
|
const { engine } = host;
|
|
2549
2644
|
if (!engine) return false;
|
|
2550
2645
|
const stateBefore = engine.getState();
|
|
@@ -2552,7 +2647,7 @@ function splitAtPlayhead(host) {
|
|
|
2552
2647
|
if (!selectedTrackId) return false;
|
|
2553
2648
|
const track = tracks.find((t) => t.id === selectedTrackId);
|
|
2554
2649
|
if (!track) return false;
|
|
2555
|
-
const atSample = Math.round(
|
|
2650
|
+
const atSample = Math.round(time * host.effectiveSampleRate);
|
|
2556
2651
|
const clip = findClipAtSample(track.clips, atSample);
|
|
2557
2652
|
if (!clip) return false;
|
|
2558
2653
|
const originalClipId = clip.id;
|
|
@@ -2677,11 +2772,29 @@ function findAudioBufferForClip(host, clip, track) {
|
|
|
2677
2772
|
return null;
|
|
2678
2773
|
}
|
|
2679
2774
|
|
|
2775
|
+
// src/interactions/peaks-loader.ts
|
|
2776
|
+
import WaveformData2 from "waveform-data";
|
|
2777
|
+
async function loadWaveformDataFromUrl(src) {
|
|
2778
|
+
const response = await fetch(src);
|
|
2779
|
+
if (!response.ok) {
|
|
2780
|
+
throw new Error("[dawcore] Failed to fetch peaks data: " + response.statusText);
|
|
2781
|
+
}
|
|
2782
|
+
const { pathname } = new URL(src, globalThis.location?.href ?? "http://localhost");
|
|
2783
|
+
const isBinary = pathname.toLowerCase().endsWith(".dat");
|
|
2784
|
+
if (isBinary) {
|
|
2785
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2786
|
+
return WaveformData2.create(arrayBuffer);
|
|
2787
|
+
} else {
|
|
2788
|
+
const json = await response.json();
|
|
2789
|
+
return WaveformData2.create(json);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2680
2793
|
// src/elements/daw-editor.ts
|
|
2681
2794
|
var DawEditorElement = class extends LitElement8 {
|
|
2682
2795
|
constructor() {
|
|
2683
2796
|
super(...arguments);
|
|
2684
|
-
this.
|
|
2797
|
+
this._samplesPerPixel = 1024;
|
|
2685
2798
|
this.waveHeight = 128;
|
|
2686
2799
|
this.timescale = false;
|
|
2687
2800
|
this.mono = false;
|
|
@@ -2708,9 +2821,12 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2708
2821
|
this._engine = null;
|
|
2709
2822
|
this._enginePromise = null;
|
|
2710
2823
|
this._audioCache = /* @__PURE__ */ new Map();
|
|
2824
|
+
this._peaksCache = /* @__PURE__ */ new Map();
|
|
2711
2825
|
this._clipBuffers = /* @__PURE__ */ new Map();
|
|
2712
2826
|
this._clipOffsets = /* @__PURE__ */ new Map();
|
|
2713
2827
|
this._peakPipeline = new PeakPipeline();
|
|
2828
|
+
/** Coarsest scale from pre-computed peaks — zoom cannot go finer than this. 0 = no limit. */
|
|
2829
|
+
this._minSamplesPerPixel = 0;
|
|
2714
2830
|
this._trackElements = /* @__PURE__ */ new Map();
|
|
2715
2831
|
this._childObserver = null;
|
|
2716
2832
|
this._audioResume = new AudioResumeController(this);
|
|
@@ -2759,6 +2875,19 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2759
2875
|
this._onTrackControl = (e) => {
|
|
2760
2876
|
const { trackId, prop, value } = e.detail ?? {};
|
|
2761
2877
|
if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
|
|
2878
|
+
if (this._selectedTrackId !== trackId) {
|
|
2879
|
+
this._setSelectedTrackId(trackId);
|
|
2880
|
+
if (this._engine) {
|
|
2881
|
+
this._engine.selectTrack(trackId);
|
|
2882
|
+
}
|
|
2883
|
+
this.dispatchEvent(
|
|
2884
|
+
new CustomEvent("daw-track-select", {
|
|
2885
|
+
bubbles: true,
|
|
2886
|
+
composed: true,
|
|
2887
|
+
detail: { trackId }
|
|
2888
|
+
})
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2762
2891
|
const oldDescriptor = this._tracks.get(trackId);
|
|
2763
2892
|
if (oldDescriptor) {
|
|
2764
2893
|
const descriptor = { ...oldDescriptor, [prop]: value };
|
|
@@ -2816,20 +2945,24 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2816
2945
|
);
|
|
2817
2946
|
}
|
|
2818
2947
|
};
|
|
2819
|
-
this._onKeyDown = (e) => {
|
|
2820
|
-
if (!this.interactiveClips) return;
|
|
2821
|
-
if (e.key === "s" || e.key === "S") {
|
|
2822
|
-
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
2823
|
-
const tag = e.target?.tagName;
|
|
2824
|
-
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
2825
|
-
if (e.target?.isContentEditable) return;
|
|
2826
|
-
e.preventDefault();
|
|
2827
|
-
this.splitAtPlayhead();
|
|
2828
|
-
}
|
|
2829
|
-
};
|
|
2830
2948
|
// --- Recording ---
|
|
2831
2949
|
this.recordingStream = null;
|
|
2832
2950
|
}
|
|
2951
|
+
get samplesPerPixel() {
|
|
2952
|
+
return this._samplesPerPixel;
|
|
2953
|
+
}
|
|
2954
|
+
set samplesPerPixel(value) {
|
|
2955
|
+
const old = this._samplesPerPixel;
|
|
2956
|
+
if (!Number.isFinite(value) || value <= 0) return;
|
|
2957
|
+
const clamped = this._minSamplesPerPixel > 0 && value < this._minSamplesPerPixel ? this._minSamplesPerPixel : value;
|
|
2958
|
+
if (clamped !== value) {
|
|
2959
|
+
console.warn(
|
|
2960
|
+
"[dawcore] Zoom " + value + " spp rejected \u2014 pre-computed peaks limit is " + this._minSamplesPerPixel + " spp"
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
this._samplesPerPixel = clamped;
|
|
2964
|
+
this.requestUpdate("samplesPerPixel", old);
|
|
2965
|
+
}
|
|
2833
2966
|
get _clipHandler() {
|
|
2834
2967
|
return this.interactiveClips ? this._clipPointer : null;
|
|
2835
2968
|
}
|
|
@@ -2892,10 +3025,6 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2892
3025
|
// --- Lifecycle ---
|
|
2893
3026
|
connectedCallback() {
|
|
2894
3027
|
super.connectedCallback();
|
|
2895
|
-
if (!this.hasAttribute("tabindex")) {
|
|
2896
|
-
this.setAttribute("tabindex", "0");
|
|
2897
|
-
}
|
|
2898
|
-
this.addEventListener("keydown", this._onKeyDown);
|
|
2899
3028
|
this.addEventListener("daw-track-connected", this._onTrackConnected);
|
|
2900
3029
|
this.addEventListener("daw-track-update", this._onTrackUpdate);
|
|
2901
3030
|
this.addEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -2921,7 +3050,6 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2921
3050
|
}
|
|
2922
3051
|
disconnectedCallback() {
|
|
2923
3052
|
super.disconnectedCallback();
|
|
2924
|
-
this.removeEventListener("keydown", this._onKeyDown);
|
|
2925
3053
|
this.removeEventListener("daw-track-connected", this._onTrackConnected);
|
|
2926
3054
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
2927
3055
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -2930,9 +3058,11 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2930
3058
|
this._childObserver = null;
|
|
2931
3059
|
this._trackElements.clear();
|
|
2932
3060
|
this._audioCache.clear();
|
|
3061
|
+
this._peaksCache.clear();
|
|
2933
3062
|
this._clipBuffers.clear();
|
|
2934
3063
|
this._clipOffsets.clear();
|
|
2935
3064
|
this._peakPipeline.terminate();
|
|
3065
|
+
this._minSamplesPerPixel = 0;
|
|
2936
3066
|
try {
|
|
2937
3067
|
this._disposeEngine();
|
|
2938
3068
|
} catch (err) {
|
|
@@ -2982,6 +3112,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2982
3112
|
if (this._engine) {
|
|
2983
3113
|
this._engine.removeTrack(trackId);
|
|
2984
3114
|
}
|
|
3115
|
+
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
2985
3116
|
if (nextEngine.size === 0) {
|
|
2986
3117
|
this._currentTime = 0;
|
|
2987
3118
|
this._stopPlayhead();
|
|
@@ -2993,6 +3124,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2993
3124
|
if (clipEls.length === 0 && trackEl.src) {
|
|
2994
3125
|
clips.push({
|
|
2995
3126
|
src: trackEl.src,
|
|
3127
|
+
peaksSrc: "",
|
|
2996
3128
|
start: 0,
|
|
2997
3129
|
duration: 0,
|
|
2998
3130
|
offset: 0,
|
|
@@ -3006,6 +3138,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
3006
3138
|
for (const clipEl of clipEls) {
|
|
3007
3139
|
clips.push({
|
|
3008
3140
|
src: clipEl.src,
|
|
3141
|
+
peaksSrc: clipEl.peaksSrc,
|
|
3009
3142
|
start: clipEl.start,
|
|
3010
3143
|
duration: clipEl.duration,
|
|
3011
3144
|
offset: clipEl.offset,
|
|
@@ -3033,7 +3166,77 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
3033
3166
|
const clips = [];
|
|
3034
3167
|
for (const clipDesc of descriptor.clips) {
|
|
3035
3168
|
if (!clipDesc.src) continue;
|
|
3036
|
-
const
|
|
3169
|
+
const waveformDataPromise = clipDesc.peaksSrc ? this._fetchPeaks(clipDesc.peaksSrc) : null;
|
|
3170
|
+
const audioPromise = this._fetchAndDecode(clipDesc.src);
|
|
3171
|
+
let waveformData = null;
|
|
3172
|
+
if (waveformDataPromise) {
|
|
3173
|
+
try {
|
|
3174
|
+
waveformData = await waveformDataPromise;
|
|
3175
|
+
} catch (err) {
|
|
3176
|
+
console.warn(
|
|
3177
|
+
"[dawcore] Failed to load peaks from " + clipDesc.peaksSrc + ": " + String(err) + " \u2014 falling back to AudioBuffer generation"
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
if (waveformData) {
|
|
3182
|
+
const clip2 = createClipFromSeconds2({
|
|
3183
|
+
waveformData,
|
|
3184
|
+
startTime: clipDesc.start,
|
|
3185
|
+
duration: clipDesc.duration || waveformData.duration,
|
|
3186
|
+
offset: clipDesc.offset,
|
|
3187
|
+
gain: clipDesc.gain,
|
|
3188
|
+
name: clipDesc.name,
|
|
3189
|
+
sampleRate: waveformData.sample_rate,
|
|
3190
|
+
sourceDuration: waveformData.duration
|
|
3191
|
+
});
|
|
3192
|
+
const effectiveScale = Math.max(this.samplesPerPixel, waveformData.scale);
|
|
3193
|
+
const peakData2 = extractPeaks(
|
|
3194
|
+
waveformData,
|
|
3195
|
+
effectiveScale,
|
|
3196
|
+
this.mono,
|
|
3197
|
+
clip2.offsetSamples,
|
|
3198
|
+
clip2.durationSamples
|
|
3199
|
+
);
|
|
3200
|
+
this._clipOffsets.set(clip2.id, {
|
|
3201
|
+
offsetSamples: clip2.offsetSamples,
|
|
3202
|
+
durationSamples: clip2.durationSamples
|
|
3203
|
+
});
|
|
3204
|
+
this._peaksData = new Map(this._peaksData).set(clip2.id, peakData2);
|
|
3205
|
+
this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
|
|
3206
|
+
const previewTrack = createTrack2({
|
|
3207
|
+
name: descriptor.name,
|
|
3208
|
+
clips: [clip2],
|
|
3209
|
+
volume: descriptor.volume,
|
|
3210
|
+
pan: descriptor.pan,
|
|
3211
|
+
muted: descriptor.muted,
|
|
3212
|
+
soloed: descriptor.soloed
|
|
3213
|
+
});
|
|
3214
|
+
previewTrack.id = trackId;
|
|
3215
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
|
|
3216
|
+
this._recomputeDuration();
|
|
3217
|
+
let audioBuffer2;
|
|
3218
|
+
try {
|
|
3219
|
+
audioBuffer2 = await audioPromise;
|
|
3220
|
+
} catch (audioErr) {
|
|
3221
|
+
const nextPeaks = new Map(this._peaksData);
|
|
3222
|
+
nextPeaks.delete(clip2.id);
|
|
3223
|
+
this._peaksData = nextPeaks;
|
|
3224
|
+
this._clipOffsets.delete(clip2.id);
|
|
3225
|
+
const nextEngine = new Map(this._engineTracks);
|
|
3226
|
+
nextEngine.delete(trackId);
|
|
3227
|
+
this._engineTracks = nextEngine;
|
|
3228
|
+
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
3229
|
+
this._recomputeDuration();
|
|
3230
|
+
throw audioErr;
|
|
3231
|
+
}
|
|
3232
|
+
this._resolvedSampleRate = audioBuffer2.sampleRate;
|
|
3233
|
+
const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
|
|
3234
|
+
this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
|
|
3235
|
+
this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
|
|
3236
|
+
clips.push(updatedClip);
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
const audioBuffer = await audioPromise;
|
|
3037
3240
|
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
3038
3241
|
const clip = createClipFromSeconds2({
|
|
3039
3242
|
audioBuffer,
|
|
@@ -3115,6 +3318,16 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
3115
3318
|
throw err;
|
|
3116
3319
|
}
|
|
3117
3320
|
}
|
|
3321
|
+
_fetchPeaks(src) {
|
|
3322
|
+
const cached = this._peaksCache.get(src);
|
|
3323
|
+
if (cached) return cached;
|
|
3324
|
+
const promise = loadWaveformDataFromUrl(src).catch((err) => {
|
|
3325
|
+
this._peaksCache.delete(src);
|
|
3326
|
+
throw err;
|
|
3327
|
+
});
|
|
3328
|
+
this._peaksCache.set(src, promise);
|
|
3329
|
+
return promise;
|
|
3330
|
+
}
|
|
3118
3331
|
_recomputeDuration() {
|
|
3119
3332
|
let maxSample = 0;
|
|
3120
3333
|
for (const track of this._engineTracks.values()) {
|
|
@@ -3213,18 +3426,71 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
3213
3426
|
this._stopPlayhead();
|
|
3214
3427
|
this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
|
|
3215
3428
|
}
|
|
3429
|
+
/** Toggle between play and pause. */
|
|
3430
|
+
togglePlayPause() {
|
|
3431
|
+
if (this._isPlaying) {
|
|
3432
|
+
this.pause();
|
|
3433
|
+
} else {
|
|
3434
|
+
this.play();
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3216
3437
|
seekTo(time) {
|
|
3217
|
-
if (!this._engine)
|
|
3218
|
-
|
|
3219
|
-
|
|
3438
|
+
if (!this._engine) {
|
|
3439
|
+
console.warn("[dawcore] seekTo: engine not ready, call ignored");
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
if (this._isPlaying) {
|
|
3443
|
+
this.stop();
|
|
3444
|
+
this.play(time);
|
|
3445
|
+
} else {
|
|
3446
|
+
this._engine.seek(time);
|
|
3447
|
+
this._currentTime = time;
|
|
3448
|
+
this._stopPlayhead();
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
/** Undo the last structural edit. */
|
|
3452
|
+
undo() {
|
|
3453
|
+
if (!this._engine) {
|
|
3454
|
+
console.warn("[dawcore] undo: engine not ready, call ignored");
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
this._engine.undo();
|
|
3458
|
+
}
|
|
3459
|
+
/** Redo the last undone edit. */
|
|
3460
|
+
redo() {
|
|
3461
|
+
if (!this._engine) {
|
|
3462
|
+
console.warn("[dawcore] redo: engine not ready, call ignored");
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
this._engine.redo();
|
|
3466
|
+
}
|
|
3467
|
+
/** Whether undo is available. */
|
|
3468
|
+
get canUndo() {
|
|
3469
|
+
return this._engine?.canUndo ?? false;
|
|
3470
|
+
}
|
|
3471
|
+
/** Whether redo is available. */
|
|
3472
|
+
get canRedo() {
|
|
3473
|
+
return this._engine?.canRedo ?? false;
|
|
3220
3474
|
}
|
|
3221
3475
|
/** Split the clip under the playhead on the selected track. */
|
|
3222
3476
|
splitAtPlayhead() {
|
|
3223
3477
|
return splitAtPlayhead({
|
|
3224
3478
|
effectiveSampleRate: this.effectiveSampleRate,
|
|
3225
3479
|
currentTime: this._currentTime,
|
|
3480
|
+
isPlaying: this._isPlaying,
|
|
3226
3481
|
engine: this._engine,
|
|
3227
|
-
dispatchEvent: (e) => this.dispatchEvent(e)
|
|
3482
|
+
dispatchEvent: (e) => this.dispatchEvent(e),
|
|
3483
|
+
stop: () => {
|
|
3484
|
+
this._engine?.stop();
|
|
3485
|
+
this._stopPlayhead();
|
|
3486
|
+
},
|
|
3487
|
+
// Call engine.play directly (synchronous) — not the async editor play()
|
|
3488
|
+
// which yields to microtask queue via await engine.init(). Engine is
|
|
3489
|
+
// already initialized at split time; the async gap causes audio desync.
|
|
3490
|
+
play: (time) => {
|
|
3491
|
+
this._engine?.play(time);
|
|
3492
|
+
this._startPlayhead();
|
|
3493
|
+
}
|
|
3228
3494
|
});
|
|
3229
3495
|
}
|
|
3230
3496
|
get currentTime() {
|
|
@@ -3474,8 +3740,8 @@ DawEditorElement.styles = [
|
|
|
3474
3740
|
];
|
|
3475
3741
|
DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
|
|
3476
3742
|
__decorateClass([
|
|
3477
|
-
property6({ type: Number, attribute: "samples-per-pixel" })
|
|
3478
|
-
], DawEditorElement.prototype, "samplesPerPixel",
|
|
3743
|
+
property6({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
|
|
3744
|
+
], DawEditorElement.prototype, "samplesPerPixel", 1);
|
|
3479
3745
|
__decorateClass([
|
|
3480
3746
|
property6({ type: Number, attribute: "wave-height" })
|
|
3481
3747
|
], DawEditorElement.prototype, "waveHeight", 2);
|
|
@@ -3824,11 +4090,182 @@ __decorateClass([
|
|
|
3824
4090
|
DawRecordButtonElement = __decorateClass([
|
|
3825
4091
|
customElement13("daw-record-button")
|
|
3826
4092
|
], DawRecordButtonElement);
|
|
4093
|
+
|
|
4094
|
+
// src/elements/daw-keyboard-shortcuts.ts
|
|
4095
|
+
import { LitElement as LitElement11 } from "lit";
|
|
4096
|
+
import { customElement as customElement14, property as property9 } from "lit/decorators.js";
|
|
4097
|
+
import { handleKeyboardEvent } from "@waveform-playlist/core";
|
|
4098
|
+
var DawKeyboardShortcutsElement = class extends LitElement11 {
|
|
4099
|
+
constructor() {
|
|
4100
|
+
super(...arguments);
|
|
4101
|
+
this.playback = false;
|
|
4102
|
+
this.splitting = false;
|
|
4103
|
+
this.undo = false;
|
|
4104
|
+
// --- JS properties for remapping ---
|
|
4105
|
+
this.playbackShortcuts = null;
|
|
4106
|
+
this.splittingShortcuts = null;
|
|
4107
|
+
this.undoShortcuts = null;
|
|
4108
|
+
/** Additional custom shortcuts. */
|
|
4109
|
+
this.customShortcuts = [];
|
|
4110
|
+
this._editor = null;
|
|
4111
|
+
this._cachedShortcuts = null;
|
|
4112
|
+
// --- Event handler ---
|
|
4113
|
+
this._onKeyDown = (e) => {
|
|
4114
|
+
const shortcuts = this.shortcuts;
|
|
4115
|
+
if (shortcuts.length === 0) return;
|
|
4116
|
+
try {
|
|
4117
|
+
handleKeyboardEvent(e, shortcuts, true);
|
|
4118
|
+
} catch (err) {
|
|
4119
|
+
console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
|
|
4120
|
+
const target = this._editor ?? this;
|
|
4121
|
+
target.dispatchEvent(
|
|
4122
|
+
new CustomEvent("daw-error", {
|
|
4123
|
+
bubbles: true,
|
|
4124
|
+
composed: true,
|
|
4125
|
+
detail: { operation: "keyboard-shortcut", key: e.key, error: err }
|
|
4126
|
+
})
|
|
4127
|
+
);
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
/** All active shortcuts (read-only, cached). */
|
|
4132
|
+
get shortcuts() {
|
|
4133
|
+
if (!this._cachedShortcuts) {
|
|
4134
|
+
this._cachedShortcuts = this._buildShortcuts();
|
|
4135
|
+
}
|
|
4136
|
+
return this._cachedShortcuts;
|
|
4137
|
+
}
|
|
4138
|
+
/** Invalidate cached shortcuts when Lit properties change. */
|
|
4139
|
+
updated() {
|
|
4140
|
+
this._cachedShortcuts = null;
|
|
4141
|
+
}
|
|
4142
|
+
// --- Lifecycle ---
|
|
4143
|
+
connectedCallback() {
|
|
4144
|
+
super.connectedCallback();
|
|
4145
|
+
this._editor = this.closest("daw-editor");
|
|
4146
|
+
if (!this._editor) {
|
|
4147
|
+
console.warn(
|
|
4148
|
+
"[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
document.addEventListener("keydown", this._onKeyDown);
|
|
4152
|
+
}
|
|
4153
|
+
disconnectedCallback() {
|
|
4154
|
+
super.disconnectedCallback();
|
|
4155
|
+
document.removeEventListener("keydown", this._onKeyDown);
|
|
4156
|
+
this._editor = null;
|
|
4157
|
+
}
|
|
4158
|
+
// No shadow DOM — render-less element
|
|
4159
|
+
createRenderRoot() {
|
|
4160
|
+
return this;
|
|
4161
|
+
}
|
|
4162
|
+
// --- Shortcut building ---
|
|
4163
|
+
_buildShortcuts() {
|
|
4164
|
+
const editor = this._editor;
|
|
4165
|
+
if (!editor) return this.customShortcuts;
|
|
4166
|
+
const result = [];
|
|
4167
|
+
if (this.playback) {
|
|
4168
|
+
const map = this.playbackShortcuts;
|
|
4169
|
+
result.push(
|
|
4170
|
+
this._makeShortcut(
|
|
4171
|
+
map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
|
|
4172
|
+
() => editor.togglePlayPause(),
|
|
4173
|
+
"Play/Pause"
|
|
4174
|
+
),
|
|
4175
|
+
this._makeShortcut(
|
|
4176
|
+
map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
|
|
4177
|
+
() => editor.stop(),
|
|
4178
|
+
"Stop"
|
|
4179
|
+
),
|
|
4180
|
+
this._makeShortcut(
|
|
4181
|
+
map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
|
|
4182
|
+
() => editor.seekTo(0),
|
|
4183
|
+
"Rewind to start"
|
|
4184
|
+
)
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
if (this.splitting) {
|
|
4188
|
+
const map = this.splittingShortcuts;
|
|
4189
|
+
const binding = map?.splitAtPlayhead ?? {
|
|
4190
|
+
key: "s",
|
|
4191
|
+
ctrlKey: false,
|
|
4192
|
+
metaKey: false,
|
|
4193
|
+
altKey: false
|
|
4194
|
+
};
|
|
4195
|
+
result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
|
|
4196
|
+
}
|
|
4197
|
+
if (this.undo) {
|
|
4198
|
+
const map = this.undoShortcuts;
|
|
4199
|
+
const undoBinding = map?.undo ?? { key: "z" };
|
|
4200
|
+
const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
|
|
4201
|
+
if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
|
|
4202
|
+
const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
|
|
4203
|
+
result.push(
|
|
4204
|
+
this._makeShortcut(
|
|
4205
|
+
{ ...undoBinding, ctrlKey: true, ...undoShift },
|
|
4206
|
+
() => editor.undo(),
|
|
4207
|
+
"Undo"
|
|
4208
|
+
),
|
|
4209
|
+
this._makeShortcut(
|
|
4210
|
+
{ ...undoBinding, metaKey: true, ...undoShift },
|
|
4211
|
+
() => editor.undo(),
|
|
4212
|
+
"Undo"
|
|
4213
|
+
)
|
|
4214
|
+
);
|
|
4215
|
+
} else {
|
|
4216
|
+
result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
|
|
4217
|
+
}
|
|
4218
|
+
if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
|
|
4219
|
+
const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
|
|
4220
|
+
result.push(
|
|
4221
|
+
this._makeShortcut(
|
|
4222
|
+
{ ...redoBinding, ctrlKey: true, ...redoShift },
|
|
4223
|
+
() => editor.redo(),
|
|
4224
|
+
"Redo"
|
|
4225
|
+
),
|
|
4226
|
+
this._makeShortcut(
|
|
4227
|
+
{ ...redoBinding, metaKey: true, ...redoShift },
|
|
4228
|
+
() => editor.redo(),
|
|
4229
|
+
"Redo"
|
|
4230
|
+
)
|
|
4231
|
+
);
|
|
4232
|
+
} else {
|
|
4233
|
+
result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
result.push(...this.customShortcuts);
|
|
4237
|
+
return result;
|
|
4238
|
+
}
|
|
4239
|
+
_makeShortcut(binding, action, description) {
|
|
4240
|
+
return {
|
|
4241
|
+
key: binding.key,
|
|
4242
|
+
...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
|
|
4243
|
+
...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
|
|
4244
|
+
...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
|
|
4245
|
+
...binding.altKey !== void 0 && { altKey: binding.altKey },
|
|
4246
|
+
action,
|
|
4247
|
+
description
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
__decorateClass([
|
|
4252
|
+
property9({ type: Boolean })
|
|
4253
|
+
], DawKeyboardShortcutsElement.prototype, "playback", 2);
|
|
4254
|
+
__decorateClass([
|
|
4255
|
+
property9({ type: Boolean })
|
|
4256
|
+
], DawKeyboardShortcutsElement.prototype, "splitting", 2);
|
|
4257
|
+
__decorateClass([
|
|
4258
|
+
property9({ type: Boolean })
|
|
4259
|
+
], DawKeyboardShortcutsElement.prototype, "undo", 2);
|
|
4260
|
+
DawKeyboardShortcutsElement = __decorateClass([
|
|
4261
|
+
customElement14("daw-keyboard-shortcuts")
|
|
4262
|
+
], DawKeyboardShortcutsElement);
|
|
3827
4263
|
export {
|
|
3828
4264
|
AudioResumeController,
|
|
3829
4265
|
ClipPointerHandler,
|
|
3830
4266
|
DawClipElement,
|
|
3831
4267
|
DawEditorElement,
|
|
4268
|
+
DawKeyboardShortcutsElement,
|
|
3832
4269
|
DawPauseButtonElement,
|
|
3833
4270
|
DawPlayButtonElement,
|
|
3834
4271
|
DawPlayheadElement,
|