@dawcore/components 0.0.12 → 0.0.14
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 +190 -6
- package/dist/index.d.ts +190 -6
- package/dist/index.js +741 -130
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +740 -130
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
DawTransportElement: () => DawTransportElement,
|
|
58
58
|
DawWaveformElement: () => DawWaveformElement,
|
|
59
59
|
RecordingController: () => RecordingController,
|
|
60
|
+
isDomClip: () => isDomClip,
|
|
60
61
|
splitAtPlayhead: () => splitAtPlayhead
|
|
61
62
|
});
|
|
62
63
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -79,11 +80,54 @@ var DawClipElement = class extends import_lit.LitElement {
|
|
|
79
80
|
this.fadeOut = 0;
|
|
80
81
|
this.fadeType = "linear";
|
|
81
82
|
this.clipId = crypto.randomUUID();
|
|
83
|
+
// Removal is detected by the editor's MutationObserver — detached elements
|
|
84
|
+
// cannot bubble events to ancestors.
|
|
85
|
+
this._hasRendered = false;
|
|
82
86
|
}
|
|
83
87
|
// Light DOM — no visual rendering, just a data container
|
|
84
88
|
createRenderRoot() {
|
|
85
89
|
return this;
|
|
86
90
|
}
|
|
91
|
+
connectedCallback() {
|
|
92
|
+
super.connectedCallback();
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
this.dispatchEvent(
|
|
95
|
+
new CustomEvent("daw-clip-connected", {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
composed: true,
|
|
98
|
+
detail: { clipId: this.clipId, element: this }
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}, 0);
|
|
102
|
+
}
|
|
103
|
+
updated(changed) {
|
|
104
|
+
if (!this._hasRendered) {
|
|
105
|
+
this._hasRendered = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const clipProps = [
|
|
109
|
+
"src",
|
|
110
|
+
"peaksSrc",
|
|
111
|
+
"start",
|
|
112
|
+
"duration",
|
|
113
|
+
"offset",
|
|
114
|
+
"gain",
|
|
115
|
+
"name",
|
|
116
|
+
"fadeIn",
|
|
117
|
+
"fadeOut",
|
|
118
|
+
"fadeType"
|
|
119
|
+
];
|
|
120
|
+
if (clipProps.some((p) => changed.has(p))) {
|
|
121
|
+
const trackEl = this.closest("daw-track");
|
|
122
|
+
this.dispatchEvent(
|
|
123
|
+
new CustomEvent("daw-clip-update", {
|
|
124
|
+
bubbles: true,
|
|
125
|
+
composed: true,
|
|
126
|
+
detail: { trackId: trackEl?.trackId ?? "", clipId: this.clipId }
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
87
131
|
};
|
|
88
132
|
__decorateClass([
|
|
89
133
|
(0, import_decorators.property)()
|
|
@@ -891,6 +935,13 @@ DawStopButtonElement = __decorateClass([
|
|
|
891
935
|
// src/elements/daw-editor.ts
|
|
892
936
|
var import_lit13 = require("lit");
|
|
893
937
|
var import_decorators11 = require("lit/decorators.js");
|
|
938
|
+
|
|
939
|
+
// src/types.ts
|
|
940
|
+
function isDomClip(desc) {
|
|
941
|
+
return desc.kind === "dom";
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/elements/daw-editor.ts
|
|
894
945
|
var import_core8 = require("@waveform-playlist/core");
|
|
895
946
|
|
|
896
947
|
// src/workers/peaksWorker.ts
|
|
@@ -1893,6 +1944,7 @@ var ViewportController = class {
|
|
|
1893
1944
|
constructor(host) {
|
|
1894
1945
|
this._scrollContainer = null;
|
|
1895
1946
|
this._lastScrollLeft = 0;
|
|
1947
|
+
this._resizeObserver = null;
|
|
1896
1948
|
// Permissive defaults: render everything until scroll container is attached
|
|
1897
1949
|
this.visibleStart = -Infinity;
|
|
1898
1950
|
this.visibleEnd = Infinity;
|
|
@@ -1925,13 +1977,26 @@ var ViewportController = class {
|
|
|
1925
1977
|
}
|
|
1926
1978
|
hostDisconnected() {
|
|
1927
1979
|
this._scrollContainer?.removeEventListener("scroll", this._onScroll);
|
|
1980
|
+
this._resizeObserver?.disconnect();
|
|
1981
|
+
this._resizeObserver = null;
|
|
1928
1982
|
this._scrollContainer = null;
|
|
1929
1983
|
}
|
|
1930
1984
|
_attachScrollContainer(container) {
|
|
1931
1985
|
this._scrollContainer?.removeEventListener("scroll", this._onScroll);
|
|
1986
|
+
this._resizeObserver?.disconnect();
|
|
1932
1987
|
this._scrollContainer = container;
|
|
1933
1988
|
container.addEventListener("scroll", this._onScroll, { passive: true });
|
|
1934
1989
|
this._update(container.scrollLeft, container.clientWidth);
|
|
1990
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1991
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
1992
|
+
if (!this._scrollContainer) return;
|
|
1993
|
+
const next = this._scrollContainer.clientWidth;
|
|
1994
|
+
if (next === this.containerWidth) return;
|
|
1995
|
+
this._update(this._scrollContainer.scrollLeft, next);
|
|
1996
|
+
this._host.requestUpdate();
|
|
1997
|
+
});
|
|
1998
|
+
this._resizeObserver.observe(container);
|
|
1999
|
+
}
|
|
1935
2000
|
this._host.requestUpdate();
|
|
1936
2001
|
}
|
|
1937
2002
|
_update(scrollLeft, containerWidth) {
|
|
@@ -2897,6 +2962,7 @@ async function loadFiles(host, files) {
|
|
|
2897
2962
|
soloed: false,
|
|
2898
2963
|
clips: [
|
|
2899
2964
|
{
|
|
2965
|
+
kind: "drop",
|
|
2900
2966
|
src: "",
|
|
2901
2967
|
peaksSrc: "",
|
|
2902
2968
|
start: 0,
|
|
@@ -2983,6 +3049,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
|
|
|
2983
3049
|
if (desc) {
|
|
2984
3050
|
const sr = host.effectiveSampleRate;
|
|
2985
3051
|
const clipDesc = {
|
|
3052
|
+
kind: "drop",
|
|
2986
3053
|
src: "",
|
|
2987
3054
|
peaksSrc: "",
|
|
2988
3055
|
start: startSample / sr,
|
|
@@ -3219,6 +3286,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3219
3286
|
this.clipHeaders = false;
|
|
3220
3287
|
this.clipHeaderHeight = 20;
|
|
3221
3288
|
this.interactiveClips = false;
|
|
3289
|
+
this.indefinitePlayback = false;
|
|
3222
3290
|
this.scaleMode = "temporal";
|
|
3223
3291
|
this._ticksPerPixel = 24;
|
|
3224
3292
|
this._bpm = 120;
|
|
@@ -3335,6 +3403,51 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3335
3403
|
this._onTrackRemoved(trackId);
|
|
3336
3404
|
}
|
|
3337
3405
|
};
|
|
3406
|
+
// --- Clip lifecycle ---
|
|
3407
|
+
this._onClipConnected = (e) => {
|
|
3408
|
+
const detail = e.detail;
|
|
3409
|
+
const clipEl = detail.element;
|
|
3410
|
+
if (!(clipEl instanceof HTMLElement)) return;
|
|
3411
|
+
const trackEl = clipEl.closest("daw-track");
|
|
3412
|
+
if (!trackEl) return;
|
|
3413
|
+
const trackId = trackEl.trackId;
|
|
3414
|
+
if (!this._engineTracks.has(trackId)) {
|
|
3415
|
+
const desc = this._tracks.get(trackId);
|
|
3416
|
+
if (desc && !desc.clips.some((c) => isDomClip(c) && c.clipId === clipEl.clipId)) {
|
|
3417
|
+
console.warn(
|
|
3418
|
+
'[dawcore] daw-clip-connected fired while parent track "' + trackId + '" is still loading \u2014 late-appended clip may be missed. Wait for daw-track-ready before appending more <daw-clip> children, or use editor.addClip(trackId, config) after the track finishes loading.'
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
const clipDesc = {
|
|
3424
|
+
kind: "dom",
|
|
3425
|
+
clipId: clipEl.clipId,
|
|
3426
|
+
src: clipEl.src,
|
|
3427
|
+
peaksSrc: clipEl.peaksSrc,
|
|
3428
|
+
start: clipEl.start,
|
|
3429
|
+
duration: clipEl.duration,
|
|
3430
|
+
offset: clipEl.offset,
|
|
3431
|
+
gain: clipEl.gain,
|
|
3432
|
+
name: clipEl.name,
|
|
3433
|
+
fadeIn: clipEl.fadeIn,
|
|
3434
|
+
fadeOut: clipEl.fadeOut,
|
|
3435
|
+
fadeType: clipEl.fadeType
|
|
3436
|
+
};
|
|
3437
|
+
this._loadAndAppendClip(trackId, clipDesc);
|
|
3438
|
+
};
|
|
3439
|
+
this._onClipUpdate = (e) => {
|
|
3440
|
+
const clipEl = e.target;
|
|
3441
|
+
if (!(clipEl instanceof HTMLElement) || clipEl.tagName !== "DAW-CLIP") return;
|
|
3442
|
+
const detail = e.detail;
|
|
3443
|
+
if (!detail.trackId) {
|
|
3444
|
+
console.warn(
|
|
3445
|
+
"[dawcore] daw-clip-update fired from a <daw-clip> not nested in a <daw-track> \u2014 ignored"
|
|
3446
|
+
);
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
this._applyClipUpdate(detail.trackId, detail.clipId, clipEl);
|
|
3450
|
+
};
|
|
3338
3451
|
// --- File Drop ---
|
|
3339
3452
|
this._onDragOver = (e) => {
|
|
3340
3453
|
if (!this.fileDrop) return;
|
|
@@ -3467,9 +3580,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3467
3580
|
this.mono,
|
|
3468
3581
|
singleClipOffsets
|
|
3469
3582
|
);
|
|
3470
|
-
|
|
3471
|
-
if (!peakData) return null;
|
|
3472
|
-
return { data: peakData.data, length: peakData.length };
|
|
3583
|
+
return result.get(clipId) ?? null;
|
|
3473
3584
|
}
|
|
3474
3585
|
get effectiveSampleRate() {
|
|
3475
3586
|
return this._resolvedSampleRate ?? this.sampleRate;
|
|
@@ -3523,7 +3634,13 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3523
3634
|
const minTicks = 32 * num * this.ppqn;
|
|
3524
3635
|
return Math.ceil(Math.max(contentTicks, minTicks) / this.ticksPerPixel);
|
|
3525
3636
|
}
|
|
3526
|
-
|
|
3637
|
+
const naturalWidth = Math.ceil(
|
|
3638
|
+
this._duration * this.effectiveSampleRate / this.samplesPerPixel
|
|
3639
|
+
);
|
|
3640
|
+
if (this.indefinitePlayback) {
|
|
3641
|
+
return Math.max(naturalWidth, this._viewport.containerWidth);
|
|
3642
|
+
}
|
|
3643
|
+
return naturalWidth;
|
|
3527
3644
|
}
|
|
3528
3645
|
/** Grid height when no tracks exist — matches scroll area's rendered height. */
|
|
3529
3646
|
get _emptyGridHeight() {
|
|
@@ -3565,19 +3682,29 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3565
3682
|
this.addEventListener("daw-track-update", this._onTrackUpdate);
|
|
3566
3683
|
this.addEventListener("daw-track-control", this._onTrackControl);
|
|
3567
3684
|
this.addEventListener("daw-track-remove", this._onTrackRemoveRequest);
|
|
3685
|
+
this.addEventListener("daw-clip-connected", this._onClipConnected);
|
|
3686
|
+
this.addEventListener("daw-clip-update", this._onClipUpdate);
|
|
3568
3687
|
this._childObserver = new MutationObserver((mutations) => {
|
|
3569
3688
|
for (const mutation of mutations) {
|
|
3570
3689
|
for (const node of mutation.removedNodes) {
|
|
3571
3690
|
if (node instanceof HTMLElement) {
|
|
3572
3691
|
if (node.tagName === "DAW-TRACK") {
|
|
3573
3692
|
this._onTrackRemoved(node.trackId);
|
|
3693
|
+
} else if (node.tagName === "DAW-CLIP") {
|
|
3694
|
+
this._onClipRemovedFromDom(node);
|
|
3574
3695
|
}
|
|
3575
|
-
const
|
|
3576
|
-
if (
|
|
3577
|
-
for (const track of
|
|
3696
|
+
const nestedTracks = node.querySelectorAll?.("daw-track");
|
|
3697
|
+
if (nestedTracks) {
|
|
3698
|
+
for (const track of nestedTracks) {
|
|
3578
3699
|
this._onTrackRemoved(track.trackId);
|
|
3579
3700
|
}
|
|
3580
3701
|
}
|
|
3702
|
+
const nestedClips = node.querySelectorAll?.("daw-clip");
|
|
3703
|
+
if (nestedClips) {
|
|
3704
|
+
for (const clip of nestedClips) {
|
|
3705
|
+
this._onClipRemovedFromDom(clip);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3581
3708
|
}
|
|
3582
3709
|
}
|
|
3583
3710
|
}
|
|
@@ -3590,6 +3717,8 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3590
3717
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
3591
3718
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
3592
3719
|
this.removeEventListener("daw-track-remove", this._onTrackRemoveRequest);
|
|
3720
|
+
this.removeEventListener("daw-clip-connected", this._onClipConnected);
|
|
3721
|
+
this.removeEventListener("daw-clip-update", this._onClipUpdate);
|
|
3593
3722
|
this._childObserver?.disconnect();
|
|
3594
3723
|
this._childObserver = null;
|
|
3595
3724
|
this._trackElements.clear();
|
|
@@ -3655,11 +3784,257 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3655
3784
|
this._stopPlayhead();
|
|
3656
3785
|
}
|
|
3657
3786
|
}
|
|
3787
|
+
_onClipRemovedFromDom(clipEl) {
|
|
3788
|
+
const clipId = clipEl.clipId;
|
|
3789
|
+
for (const [trackId, t] of this._engineTracks.entries()) {
|
|
3790
|
+
if (t.clips.some((c) => c.id === clipId)) {
|
|
3791
|
+
this._removeClipFromTrack(trackId, clipId);
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
if (this._clipBuffers.has(clipId) || this._clipOffsets.has(clipId) || this._peaksData.has(clipId)) {
|
|
3796
|
+
console.warn(
|
|
3797
|
+
'[dawcore] _onClipRemovedFromDom: orphaned cache entries for clip "' + clipId + '" \u2014 purging (DOM/engine id mismatch?)'
|
|
3798
|
+
);
|
|
3799
|
+
this._purgeClipCaches(clipId);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
async _loadAndAppendClip(trackId, clipDesc) {
|
|
3803
|
+
if (!clipDesc.src) return;
|
|
3804
|
+
const clipId = clipDesc.clipId;
|
|
3805
|
+
let insertedClipId = null;
|
|
3806
|
+
try {
|
|
3807
|
+
const waveformDataPromise = clipDesc.peaksSrc ? this._resolvePeaks(clipDesc.peaksSrc) : Promise.resolve(null);
|
|
3808
|
+
const audioPromise = this._fetchAndDecode(clipDesc.src);
|
|
3809
|
+
const [waveformData, audioBuffer] = await Promise.all([waveformDataPromise, audioPromise]);
|
|
3810
|
+
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
3811
|
+
const clip = await this._finalizeAudioClip(clipDesc, audioBuffer, waveformData);
|
|
3812
|
+
insertedClipId = clip.id;
|
|
3813
|
+
const t = this._engineTracks.get(trackId);
|
|
3814
|
+
if (!t) {
|
|
3815
|
+
this._purgeClipCaches(clip.id);
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
const updatedTrack = { ...t, clips: [...t.clips, clip] };
|
|
3819
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
3820
|
+
const desc = this._tracks.get(trackId);
|
|
3821
|
+
if (desc) {
|
|
3822
|
+
this._tracks = new Map(this._tracks).set(trackId, {
|
|
3823
|
+
...desc,
|
|
3824
|
+
clips: [...desc.clips, clipDesc]
|
|
3825
|
+
});
|
|
3826
|
+
}
|
|
3827
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
3828
|
+
this.dispatchEvent(
|
|
3829
|
+
new CustomEvent("daw-clip-ready", {
|
|
3830
|
+
bubbles: true,
|
|
3831
|
+
composed: true,
|
|
3832
|
+
detail: { trackId, clipId: clip.id }
|
|
3833
|
+
})
|
|
3834
|
+
);
|
|
3835
|
+
} catch (err) {
|
|
3836
|
+
console.warn("[dawcore] _loadAndAppendClip failed: " + String(err));
|
|
3837
|
+
if (insertedClipId) this._purgeClipCaches(insertedClipId);
|
|
3838
|
+
this.dispatchEvent(
|
|
3839
|
+
new CustomEvent("daw-clip-error", {
|
|
3840
|
+
bubbles: true,
|
|
3841
|
+
composed: true,
|
|
3842
|
+
detail: { trackId, clipId: insertedClipId ?? clipId, error: err }
|
|
3843
|
+
})
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Resolve pre-computed peaks for a clip: fetch the .dat/.json, validate the
|
|
3849
|
+
* sample rate matches the AudioContext, return the WaveformData or null.
|
|
3850
|
+
* Warns on fetch failure and on sample-rate mismatch — never silent.
|
|
3851
|
+
*
|
|
3852
|
+
* Shared between `_loadTrack` (peaks-first preview path) and
|
|
3853
|
+
* `_loadAndAppendClip` (incremental late-append).
|
|
3854
|
+
*/
|
|
3855
|
+
async _resolvePeaks(peaksSrc) {
|
|
3856
|
+
try {
|
|
3857
|
+
const wd = await this._fetchPeaks(peaksSrc);
|
|
3858
|
+
const contextRate = this.audioContext.sampleRate;
|
|
3859
|
+
if (wd.sample_rate === contextRate) return wd;
|
|
3860
|
+
console.warn(
|
|
3861
|
+
"[dawcore] Pre-computed peaks at " + wd.sample_rate + " Hz do not match AudioContext at " + contextRate + " Hz \u2014 ignoring " + peaksSrc + ", generating from audio"
|
|
3862
|
+
);
|
|
3863
|
+
return null;
|
|
3864
|
+
} catch (err) {
|
|
3865
|
+
console.warn(
|
|
3866
|
+
"[dawcore] Failed to load peaks from " + peaksSrc + ": " + String(err) + " \u2014 falling back to AudioBuffer generation"
|
|
3867
|
+
);
|
|
3868
|
+
return null;
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Construct an AudioClip from a decoded buffer (and optional WaveformData),
|
|
3873
|
+
* align its id with the source `<daw-clip>.clipId` when present, populate
|
|
3874
|
+
* `_clipBuffers` / `_clipOffsets`, generate peaks via the worker pipeline,
|
|
3875
|
+
* and populate `_peaksData`. Returns the finished AudioClip.
|
|
3876
|
+
*
|
|
3877
|
+
* Shared between `_loadTrack`'s standard path and `_loadAndAppendClip`.
|
|
3878
|
+
* Not used by `_loadTrack`'s peaks-first preview path because that path
|
|
3879
|
+
* uses sync `extractPeaks` and inserts a preview track BEFORE audio decode.
|
|
3880
|
+
*/
|
|
3881
|
+
async _finalizeAudioClip(clipDesc, audioBuffer, waveformData) {
|
|
3882
|
+
let clip;
|
|
3883
|
+
if (waveformData) {
|
|
3884
|
+
const wdRate = waveformData.sample_rate;
|
|
3885
|
+
clip = (0, import_core8.createClip)({
|
|
3886
|
+
audioBuffer,
|
|
3887
|
+
waveformData,
|
|
3888
|
+
startSample: Math.round(clipDesc.start * wdRate),
|
|
3889
|
+
durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
|
|
3890
|
+
offsetSamples: Math.round(clipDesc.offset * wdRate),
|
|
3891
|
+
gain: clipDesc.gain,
|
|
3892
|
+
name: clipDesc.name,
|
|
3893
|
+
sampleRate: wdRate,
|
|
3894
|
+
sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
|
|
3895
|
+
});
|
|
3896
|
+
this._peakPipeline.cacheWaveformData(audioBuffer, waveformData);
|
|
3897
|
+
} else {
|
|
3898
|
+
clip = (0, import_core8.createClipFromSeconds)({
|
|
3899
|
+
audioBuffer,
|
|
3900
|
+
startTime: clipDesc.start,
|
|
3901
|
+
duration: clipDesc.duration || audioBuffer.duration,
|
|
3902
|
+
offset: clipDesc.offset,
|
|
3903
|
+
gain: clipDesc.gain,
|
|
3904
|
+
name: clipDesc.name,
|
|
3905
|
+
sampleRate: audioBuffer.sampleRate,
|
|
3906
|
+
sourceDuration: audioBuffer.duration
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
if (isDomClip(clipDesc)) clip.id = clipDesc.clipId;
|
|
3910
|
+
this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
|
|
3911
|
+
this._clipOffsets.set(clip.id, {
|
|
3912
|
+
offsetSamples: clip.offsetSamples,
|
|
3913
|
+
durationSamples: clip.durationSamples
|
|
3914
|
+
});
|
|
3915
|
+
let peakData;
|
|
3916
|
+
try {
|
|
3917
|
+
peakData = await this._peakPipeline.generatePeaks(
|
|
3918
|
+
audioBuffer,
|
|
3919
|
+
this._renderSpp,
|
|
3920
|
+
this.mono,
|
|
3921
|
+
clip.offsetSamples,
|
|
3922
|
+
clip.durationSamples
|
|
3923
|
+
);
|
|
3924
|
+
} catch (err) {
|
|
3925
|
+
this._purgeClipCaches(clip.id);
|
|
3926
|
+
throw err;
|
|
3927
|
+
}
|
|
3928
|
+
this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
|
|
3929
|
+
if (waveformData) {
|
|
3930
|
+
this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
|
|
3931
|
+
}
|
|
3932
|
+
return clip;
|
|
3933
|
+
}
|
|
3934
|
+
/** Remove a single clip from all per-clip caches. Used by error rollbacks. */
|
|
3935
|
+
_purgeClipCaches(clipId) {
|
|
3936
|
+
const nextBuffers = new Map(this._clipBuffers);
|
|
3937
|
+
nextBuffers.delete(clipId);
|
|
3938
|
+
this._clipBuffers = nextBuffers;
|
|
3939
|
+
const nextPeaks = new Map(this._peaksData);
|
|
3940
|
+
nextPeaks.delete(clipId);
|
|
3941
|
+
this._peaksData = nextPeaks;
|
|
3942
|
+
this._clipOffsets.delete(clipId);
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Recompute duration and forward an updated track to the engine. Single
|
|
3946
|
+
* source of truth for the incremental-vs-full-rebuild policy used by every
|
|
3947
|
+
* clip-level mutation (addClip, updateClip, removeClip, _applyClipUpdate).
|
|
3948
|
+
* Use the engine's incremental updateTrack when available; otherwise fall
|
|
3949
|
+
* back to full setTracks (legacy adapters).
|
|
3950
|
+
*/
|
|
3951
|
+
_commitTrackChange(trackId, updatedTrack) {
|
|
3952
|
+
this._recomputeDuration();
|
|
3953
|
+
if (this._engine?.updateTrack) this._engine.updateTrack(trackId, updatedTrack);
|
|
3954
|
+
else if (this._engine) this._engine.setTracks([...this._engineTracks.values()]);
|
|
3955
|
+
}
|
|
3956
|
+
_applyClipUpdate(trackId, clipId, clipEl) {
|
|
3957
|
+
const t = this._engineTracks.get(trackId);
|
|
3958
|
+
if (!t) {
|
|
3959
|
+
console.warn('[dawcore] _applyClipUpdate: no engine track for id "' + trackId + '"');
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
const idx = t.clips.findIndex((c) => c.id === clipId);
|
|
3963
|
+
if (idx === -1) {
|
|
3964
|
+
console.warn(
|
|
3965
|
+
'[dawcore] _applyClipUpdate: clip "' + clipId + '" not found in track "' + trackId + '" (DOM/engine clip-id misalignment?)'
|
|
3966
|
+
);
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
const oldClip = t.clips[idx];
|
|
3970
|
+
const sr = oldClip.sampleRate ?? this.effectiveSampleRate;
|
|
3971
|
+
const newStartSample = Math.round(clipEl.start * sr);
|
|
3972
|
+
const newDurationSamples = clipEl.duration > 0 ? Math.round(clipEl.duration * sr) : oldClip.durationSamples;
|
|
3973
|
+
const newOffsetSamples = Math.round(clipEl.offset * sr);
|
|
3974
|
+
const updatedClip = {
|
|
3975
|
+
...oldClip,
|
|
3976
|
+
startSample: newStartSample,
|
|
3977
|
+
durationSamples: newDurationSamples,
|
|
3978
|
+
offsetSamples: newOffsetSamples,
|
|
3979
|
+
gain: clipEl.gain,
|
|
3980
|
+
name: clipEl.name || oldClip.name
|
|
3981
|
+
};
|
|
3982
|
+
const updatedClips = [...t.clips];
|
|
3983
|
+
updatedClips[idx] = updatedClip;
|
|
3984
|
+
const updatedTrack = { ...t, clips: updatedClips };
|
|
3985
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
3986
|
+
const boundsChanged = oldClip.offsetSamples !== newOffsetSamples || oldClip.durationSamples !== newDurationSamples;
|
|
3987
|
+
if (boundsChanged) {
|
|
3988
|
+
this._clipOffsets.set(clipId, {
|
|
3989
|
+
offsetSamples: newOffsetSamples,
|
|
3990
|
+
durationSamples: newDurationSamples
|
|
3991
|
+
});
|
|
3992
|
+
const peaks = this.reextractClipPeaks(clipId, newOffsetSamples, newDurationSamples);
|
|
3993
|
+
if (peaks) {
|
|
3994
|
+
this._peaksData = new Map(this._peaksData).set(clipId, peaks);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
3998
|
+
}
|
|
3999
|
+
_removeClipFromTrack(trackId, clipId) {
|
|
4000
|
+
const t = this._engineTracks.get(trackId);
|
|
4001
|
+
if (!t) {
|
|
4002
|
+
console.warn('[dawcore] _removeClipFromTrack: no engine track for id "' + trackId + '"');
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
const updatedClips = t.clips.filter((c) => c.id !== clipId);
|
|
4006
|
+
if (updatedClips.length === t.clips.length) {
|
|
4007
|
+
console.warn(
|
|
4008
|
+
'[dawcore] _removeClipFromTrack: clip "' + clipId + '" not found in track "' + trackId + '"'
|
|
4009
|
+
);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
const updatedTrack = { ...t, clips: updatedClips };
|
|
4013
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
4014
|
+
const nextBuffers = new Map(this._clipBuffers);
|
|
4015
|
+
nextBuffers.delete(clipId);
|
|
4016
|
+
this._clipBuffers = nextBuffers;
|
|
4017
|
+
this._clipOffsets.delete(clipId);
|
|
4018
|
+
const nextPeaks = new Map(this._peaksData);
|
|
4019
|
+
nextPeaks.delete(clipId);
|
|
4020
|
+
this._peaksData = nextPeaks;
|
|
4021
|
+
const desc = this._tracks.get(trackId);
|
|
4022
|
+
if (desc) {
|
|
4023
|
+
this._tracks = new Map(this._tracks).set(trackId, {
|
|
4024
|
+
...desc,
|
|
4025
|
+
// Only DOM-sourced clips have an id to match; drop-sourced clips are
|
|
4026
|
+
// filtered through unchanged (their identity is the descriptor itself).
|
|
4027
|
+
clips: desc.clips.filter((c) => !(isDomClip(c) && c.clipId === clipId))
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
4031
|
+
}
|
|
3658
4032
|
_readTrackDescriptor(trackEl) {
|
|
3659
4033
|
const clipEls = trackEl.querySelectorAll("daw-clip");
|
|
3660
4034
|
const clips = [];
|
|
3661
4035
|
if (clipEls.length === 0 && trackEl.src) {
|
|
3662
4036
|
clips.push({
|
|
4037
|
+
kind: "drop",
|
|
3663
4038
|
src: trackEl.src,
|
|
3664
4039
|
peaksSrc: "",
|
|
3665
4040
|
start: 0,
|
|
@@ -3674,6 +4049,8 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3674
4049
|
} else {
|
|
3675
4050
|
for (const clipEl of clipEls) {
|
|
3676
4051
|
clips.push({
|
|
4052
|
+
kind: "dom",
|
|
4053
|
+
clipId: clipEl.clipId,
|
|
3677
4054
|
src: clipEl.src,
|
|
3678
4055
|
peaksSrc: clipEl.peaksSrc,
|
|
3679
4056
|
start: clipEl.start,
|
|
@@ -3703,111 +4080,92 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3703
4080
|
const clips = [];
|
|
3704
4081
|
for (const clipDesc of descriptor.clips) {
|
|
3705
4082
|
if (!clipDesc.src) continue;
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
const
|
|
3712
|
-
const
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
4083
|
+
try {
|
|
4084
|
+
const waveformDataPromise = clipDesc.peaksSrc ? this._resolvePeaks(clipDesc.peaksSrc) : Promise.resolve(null);
|
|
4085
|
+
const audioPromise = this._fetchAndDecode(clipDesc.src);
|
|
4086
|
+
const waveformData = await waveformDataPromise;
|
|
4087
|
+
if (waveformData) {
|
|
4088
|
+
const wdRate = waveformData.sample_rate;
|
|
4089
|
+
const clip2 = (0, import_core8.createClip)({
|
|
4090
|
+
waveformData,
|
|
4091
|
+
startSample: Math.round(clipDesc.start * wdRate),
|
|
4092
|
+
durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
|
|
4093
|
+
offsetSamples: Math.round(clipDesc.offset * wdRate),
|
|
4094
|
+
gain: clipDesc.gain,
|
|
4095
|
+
name: clipDesc.name,
|
|
4096
|
+
sampleRate: wdRate,
|
|
4097
|
+
sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
|
|
4098
|
+
});
|
|
4099
|
+
if (isDomClip(clipDesc)) clip2.id = clipDesc.clipId;
|
|
4100
|
+
const effectiveScale = Math.max(this._renderSpp, waveformData.scale);
|
|
4101
|
+
const peakData = extractPeaks(
|
|
4102
|
+
waveformData,
|
|
4103
|
+
effectiveScale,
|
|
4104
|
+
this.mono,
|
|
4105
|
+
clip2.offsetSamples,
|
|
4106
|
+
clip2.durationSamples
|
|
3723
4107
|
);
|
|
4108
|
+
this._clipOffsets.set(clip2.id, {
|
|
4109
|
+
offsetSamples: clip2.offsetSamples,
|
|
4110
|
+
durationSamples: clip2.durationSamples
|
|
4111
|
+
});
|
|
4112
|
+
this._peaksData = new Map(this._peaksData).set(clip2.id, peakData);
|
|
4113
|
+
this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
|
|
4114
|
+
const previewTrack = (0, import_core8.createTrack)({
|
|
4115
|
+
name: descriptor.name,
|
|
4116
|
+
clips: [clip2],
|
|
4117
|
+
volume: descriptor.volume,
|
|
4118
|
+
pan: descriptor.pan,
|
|
4119
|
+
muted: descriptor.muted,
|
|
4120
|
+
soloed: descriptor.soloed
|
|
4121
|
+
});
|
|
4122
|
+
previewTrack.id = trackId;
|
|
4123
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
|
|
4124
|
+
this._recomputeDuration();
|
|
4125
|
+
let audioBuffer2;
|
|
4126
|
+
try {
|
|
4127
|
+
audioBuffer2 = await audioPromise;
|
|
4128
|
+
} catch (audioErr) {
|
|
4129
|
+
const nextPeaks = new Map(this._peaksData);
|
|
4130
|
+
nextPeaks.delete(clip2.id);
|
|
4131
|
+
this._peaksData = nextPeaks;
|
|
4132
|
+
this._clipOffsets.delete(clip2.id);
|
|
4133
|
+
const nextEngine = new Map(this._engineTracks);
|
|
4134
|
+
nextEngine.delete(trackId);
|
|
4135
|
+
this._engineTracks = nextEngine;
|
|
4136
|
+
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
4137
|
+
this._recomputeDuration();
|
|
4138
|
+
throw audioErr;
|
|
4139
|
+
}
|
|
4140
|
+
this._resolvedSampleRate = audioBuffer2.sampleRate;
|
|
4141
|
+
const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
|
|
4142
|
+
this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
|
|
4143
|
+
this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
|
|
4144
|
+
clips.push(updatedClip);
|
|
4145
|
+
continue;
|
|
3724
4146
|
}
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
const
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
offsetSamples: Math.round(clipDesc.offset * wdRate),
|
|
3733
|
-
gain: clipDesc.gain,
|
|
3734
|
-
name: clipDesc.name,
|
|
3735
|
-
sampleRate: wdRate,
|
|
3736
|
-
sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
|
|
3737
|
-
});
|
|
3738
|
-
const effectiveScale = Math.max(this._renderSpp, waveformData.scale);
|
|
3739
|
-
const peakData2 = extractPeaks(
|
|
3740
|
-
waveformData,
|
|
3741
|
-
effectiveScale,
|
|
3742
|
-
this.mono,
|
|
3743
|
-
clip2.offsetSamples,
|
|
3744
|
-
clip2.durationSamples
|
|
4147
|
+
const audioBuffer = await audioPromise;
|
|
4148
|
+
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
4149
|
+
const clip = await this._finalizeAudioClip(clipDesc, audioBuffer, null);
|
|
4150
|
+
clips.push(clip);
|
|
4151
|
+
} catch (clipErr) {
|
|
4152
|
+
console.warn(
|
|
4153
|
+
'[dawcore] _loadTrack: clip "' + clipDesc.src + '" failed: ' + String(clipErr)
|
|
3745
4154
|
);
|
|
3746
|
-
this.
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
soloed: descriptor.soloed
|
|
3759
|
-
});
|
|
3760
|
-
previewTrack.id = trackId;
|
|
3761
|
-
this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
|
|
3762
|
-
this._recomputeDuration();
|
|
3763
|
-
let audioBuffer2;
|
|
3764
|
-
try {
|
|
3765
|
-
audioBuffer2 = await audioPromise;
|
|
3766
|
-
} catch (audioErr) {
|
|
3767
|
-
const nextPeaks = new Map(this._peaksData);
|
|
3768
|
-
nextPeaks.delete(clip2.id);
|
|
3769
|
-
this._peaksData = nextPeaks;
|
|
3770
|
-
this._clipOffsets.delete(clip2.id);
|
|
3771
|
-
const nextEngine = new Map(this._engineTracks);
|
|
3772
|
-
nextEngine.delete(trackId);
|
|
3773
|
-
this._engineTracks = nextEngine;
|
|
3774
|
-
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
3775
|
-
this._recomputeDuration();
|
|
3776
|
-
throw audioErr;
|
|
4155
|
+
if (this.isConnected) {
|
|
4156
|
+
this.dispatchEvent(
|
|
4157
|
+
new CustomEvent("daw-clip-error", {
|
|
4158
|
+
bubbles: true,
|
|
4159
|
+
composed: true,
|
|
4160
|
+
detail: {
|
|
4161
|
+
trackId,
|
|
4162
|
+
clipId: isDomClip(clipDesc) ? clipDesc.clipId : "",
|
|
4163
|
+
error: clipErr
|
|
4164
|
+
}
|
|
4165
|
+
})
|
|
4166
|
+
);
|
|
3777
4167
|
}
|
|
3778
|
-
this._resolvedSampleRate = audioBuffer2.sampleRate;
|
|
3779
|
-
const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
|
|
3780
|
-
this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
|
|
3781
|
-
this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
|
|
3782
|
-
clips.push(updatedClip);
|
|
3783
|
-
continue;
|
|
3784
4168
|
}
|
|
3785
|
-
const audioBuffer = await audioPromise;
|
|
3786
|
-
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
3787
|
-
const clip = (0, import_core8.createClipFromSeconds)({
|
|
3788
|
-
audioBuffer,
|
|
3789
|
-
startTime: clipDesc.start,
|
|
3790
|
-
duration: clipDesc.duration || audioBuffer.duration,
|
|
3791
|
-
offset: clipDesc.offset,
|
|
3792
|
-
gain: clipDesc.gain,
|
|
3793
|
-
name: clipDesc.name,
|
|
3794
|
-
sampleRate: audioBuffer.sampleRate,
|
|
3795
|
-
sourceDuration: audioBuffer.duration
|
|
3796
|
-
});
|
|
3797
|
-
this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
|
|
3798
|
-
this._clipOffsets.set(clip.id, {
|
|
3799
|
-
offsetSamples: clip.offsetSamples,
|
|
3800
|
-
durationSamples: clip.durationSamples
|
|
3801
|
-
});
|
|
3802
|
-
const peakData = await this._peakPipeline.generatePeaks(
|
|
3803
|
-
audioBuffer,
|
|
3804
|
-
this._renderSpp,
|
|
3805
|
-
this.mono,
|
|
3806
|
-
clip.offsetSamples,
|
|
3807
|
-
clip.durationSamples
|
|
3808
|
-
);
|
|
3809
|
-
this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
|
|
3810
|
-
clips.push(clip);
|
|
3811
4169
|
}
|
|
3812
4170
|
const track = (0, import_core8.createTrack)({
|
|
3813
4171
|
name: descriptor.name,
|
|
@@ -3817,6 +4175,12 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3817
4175
|
muted: descriptor.muted,
|
|
3818
4176
|
soloed: descriptor.soloed
|
|
3819
4177
|
});
|
|
4178
|
+
const requestedClips = descriptor.clips.filter((c) => c.src).length;
|
|
4179
|
+
if (requestedClips > 0 && clips.length === 0) {
|
|
4180
|
+
throw new Error(
|
|
4181
|
+
"all " + requestedClips + " clip(s) failed to load \u2014 see prior daw-clip-error events"
|
|
4182
|
+
);
|
|
4183
|
+
}
|
|
3820
4184
|
track.id = trackId;
|
|
3821
4185
|
this._engineTracks = new Map(this._engineTracks).set(trackId, track);
|
|
3822
4186
|
this._recomputeDuration();
|
|
@@ -3950,6 +4314,251 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
3950
4314
|
async loadFiles(files) {
|
|
3951
4315
|
return loadFiles(this, files);
|
|
3952
4316
|
}
|
|
4317
|
+
// --- Programmatic Track API ---
|
|
4318
|
+
/**
|
|
4319
|
+
* Build the engine if it hasn't been built yet. Lets consumers obtain a
|
|
4320
|
+
* non-null `editor.engine` before any track has been loaded — useful for
|
|
4321
|
+
* wiring analyzers, effects, or master taps before content arrives.
|
|
4322
|
+
*/
|
|
4323
|
+
async ready() {
|
|
4324
|
+
return this._ensureEngine();
|
|
4325
|
+
}
|
|
4326
|
+
/**
|
|
4327
|
+
* Wait for either `readyEvent` or `errorEvent` to fire on this editor for
|
|
4328
|
+
* the entity matching `matchesId`. Listeners are wired synchronously, then
|
|
4329
|
+
* `setup` is called (typical: appendChild). Resolves with `resolveValue`
|
|
4330
|
+
* on ready; rejects with a normalized Error on error. Used by addTrack and
|
|
4331
|
+
* addClip to share their Promise-with-listener-cleanup machinery.
|
|
4332
|
+
*/
|
|
4333
|
+
_awaitId(readyEvent, errorEvent, matchesId, resolveValue, setup) {
|
|
4334
|
+
return new Promise((resolve, reject) => {
|
|
4335
|
+
const onReady = (e) => {
|
|
4336
|
+
if (!matchesId(e.detail)) return;
|
|
4337
|
+
cleanup();
|
|
4338
|
+
resolve(resolveValue);
|
|
4339
|
+
};
|
|
4340
|
+
const onError = (e) => {
|
|
4341
|
+
const detail = e.detail;
|
|
4342
|
+
if (!matchesId(e.detail)) return;
|
|
4343
|
+
cleanup();
|
|
4344
|
+
const err = detail.error;
|
|
4345
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4346
|
+
};
|
|
4347
|
+
const cleanup = () => {
|
|
4348
|
+
this.removeEventListener(readyEvent, onReady);
|
|
4349
|
+
this.removeEventListener(errorEvent, onError);
|
|
4350
|
+
};
|
|
4351
|
+
this.addEventListener(readyEvent, onReady);
|
|
4352
|
+
this.addEventListener(errorEvent, onError);
|
|
4353
|
+
setup();
|
|
4354
|
+
});
|
|
4355
|
+
}
|
|
4356
|
+
/**
|
|
4357
|
+
* Append a `<daw-track>` element built from `config` and resolve once the
|
|
4358
|
+
* track finishes loading (or reject on `daw-track-error`). Goes through
|
|
4359
|
+
* the same `_loadTrack` pipeline as declarative tracks, so descriptors,
|
|
4360
|
+
* peaks, and clip buffers are populated correctly.
|
|
4361
|
+
*/
|
|
4362
|
+
addTrack(config = {}) {
|
|
4363
|
+
const trackEl = document.createElement("daw-track");
|
|
4364
|
+
if (config.name !== void 0) trackEl.setAttribute("name", config.name);
|
|
4365
|
+
if (config.volume !== void 0) trackEl.volume = config.volume;
|
|
4366
|
+
if (config.pan !== void 0) trackEl.pan = config.pan;
|
|
4367
|
+
if (config.muted) trackEl.setAttribute("muted", "");
|
|
4368
|
+
if (config.soloed) trackEl.setAttribute("soloed", "");
|
|
4369
|
+
for (const clipConfig of config.clips ?? []) {
|
|
4370
|
+
trackEl.appendChild(this._buildClipElement(clipConfig));
|
|
4371
|
+
}
|
|
4372
|
+
return this._awaitId(
|
|
4373
|
+
"daw-track-ready",
|
|
4374
|
+
"daw-track-error",
|
|
4375
|
+
(d) => d.trackId === trackEl.trackId,
|
|
4376
|
+
trackEl,
|
|
4377
|
+
() => this.appendChild(trackEl)
|
|
4378
|
+
);
|
|
4379
|
+
}
|
|
4380
|
+
/**
|
|
4381
|
+
* Remove a track by id. Equivalent to `trackElement.remove()` —
|
|
4382
|
+
* the editor's MutationObserver handles engine and cache cleanup.
|
|
4383
|
+
* No-op if no matching track exists.
|
|
4384
|
+
*/
|
|
4385
|
+
removeTrack(trackId) {
|
|
4386
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4387
|
+
if (trackEl) {
|
|
4388
|
+
trackEl.remove();
|
|
4389
|
+
} else if (this._engineTracks.has(trackId)) {
|
|
4390
|
+
this._onTrackRemoved(trackId);
|
|
4391
|
+
} else {
|
|
4392
|
+
console.warn('[dawcore] removeTrack: no track found for id "' + trackId + '"');
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
/**
|
|
4396
|
+
* Update reflected attributes on a track. For DOM-element tracks the changes
|
|
4397
|
+
* are written to the `<daw-track>` element (which fires `daw-track-update`);
|
|
4398
|
+
* for tracks without a DOM element (file drops) the descriptor and engine
|
|
4399
|
+
* state are updated in place.
|
|
4400
|
+
*/
|
|
4401
|
+
updateTrack(trackId, partial) {
|
|
4402
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4403
|
+
if (trackEl) {
|
|
4404
|
+
if (partial.name !== void 0) trackEl.setAttribute("name", partial.name);
|
|
4405
|
+
if (partial.volume !== void 0) trackEl.volume = partial.volume;
|
|
4406
|
+
if (partial.pan !== void 0) trackEl.pan = partial.pan;
|
|
4407
|
+
if (partial.muted !== void 0) {
|
|
4408
|
+
if (partial.muted) trackEl.setAttribute("muted", "");
|
|
4409
|
+
else trackEl.removeAttribute("muted");
|
|
4410
|
+
}
|
|
4411
|
+
if (partial.soloed !== void 0) {
|
|
4412
|
+
if (partial.soloed) trackEl.setAttribute("soloed", "");
|
|
4413
|
+
else trackEl.removeAttribute("soloed");
|
|
4414
|
+
}
|
|
4415
|
+
return;
|
|
4416
|
+
}
|
|
4417
|
+
const oldDesc = this._tracks.get(trackId);
|
|
4418
|
+
if (!oldDesc) return;
|
|
4419
|
+
const newDesc = {
|
|
4420
|
+
...oldDesc,
|
|
4421
|
+
...partial.name !== void 0 && { name: partial.name },
|
|
4422
|
+
...partial.volume !== void 0 && { volume: partial.volume },
|
|
4423
|
+
...partial.pan !== void 0 && { pan: partial.pan },
|
|
4424
|
+
...partial.muted !== void 0 && { muted: partial.muted },
|
|
4425
|
+
...partial.soloed !== void 0 && { soloed: partial.soloed }
|
|
4426
|
+
};
|
|
4427
|
+
this._tracks = new Map(this._tracks).set(trackId, newDesc);
|
|
4428
|
+
if (this._engine) {
|
|
4429
|
+
if (partial.volume !== void 0) this._engine.setTrackVolume(trackId, partial.volume);
|
|
4430
|
+
if (partial.pan !== void 0) this._engine.setTrackPan(trackId, partial.pan);
|
|
4431
|
+
if (partial.muted !== void 0) this._engine.setTrackMute(trackId, partial.muted);
|
|
4432
|
+
if (partial.soloed !== void 0) this._engine.setTrackSolo(trackId, partial.soloed);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* Append a clip to an existing track. Builds a `<daw-clip>` from `config`
|
|
4437
|
+
* and appends it to the track's DOM element when one exists; resolves with
|
|
4438
|
+
* the new clip's id once the audio decode + peak generation finish.
|
|
4439
|
+
*/
|
|
4440
|
+
addClip(trackId, config) {
|
|
4441
|
+
if (!config.src) {
|
|
4442
|
+
return Promise.reject(
|
|
4443
|
+
new Error(
|
|
4444
|
+
"addClip: config.src is required \u2014 pass a URL to load. Empty/recording clips are not yet supported via addClip."
|
|
4445
|
+
)
|
|
4446
|
+
);
|
|
4447
|
+
}
|
|
4448
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4449
|
+
if (!trackEl) {
|
|
4450
|
+
return Promise.reject(
|
|
4451
|
+
new Error(
|
|
4452
|
+
'addClip: no <daw-track> element for trackId "' + trackId + '" \u2014 addClip currently requires a DOM-backed track. Use editor.addTrack(config) first.'
|
|
4453
|
+
)
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4456
|
+
const clipEl = this._buildClipElement(config);
|
|
4457
|
+
return this._awaitId(
|
|
4458
|
+
"daw-clip-ready",
|
|
4459
|
+
"daw-clip-error",
|
|
4460
|
+
(d) => d.clipId === clipEl.clipId,
|
|
4461
|
+
clipEl.clipId,
|
|
4462
|
+
() => trackEl.appendChild(clipEl)
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
/**
|
|
4466
|
+
* Remove a clip by id. Removes the matching `<daw-clip>` DOM element when
|
|
4467
|
+
* present (MutationObserver handles cleanup); otherwise updates engine
|
|
4468
|
+
* state directly. No-op if no matching clip exists.
|
|
4469
|
+
*/
|
|
4470
|
+
removeClip(trackId, clipId) {
|
|
4471
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4472
|
+
if (trackEl) {
|
|
4473
|
+
const clipEl = [...trackEl.querySelectorAll("daw-clip")].find(
|
|
4474
|
+
(c) => c.clipId === clipId
|
|
4475
|
+
);
|
|
4476
|
+
if (clipEl) {
|
|
4477
|
+
clipEl.remove();
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
if (this._engineTracks.has(trackId)) {
|
|
4482
|
+
this._removeClipFromTrack(trackId, clipId);
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
console.warn(
|
|
4486
|
+
'[dawcore] removeClip: no track found for id "' + trackId + '" (clipId "' + clipId + '")'
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
/**
|
|
4490
|
+
* Update a clip's position (start/duration/offset) or properties (gain/name).
|
|
4491
|
+
* For DOM-element clips, writes properties on the `<daw-clip>` element which
|
|
4492
|
+
* fires `daw-clip-update`; otherwise applies directly via `_applyClipUpdate`.
|
|
4493
|
+
*
|
|
4494
|
+
* Re-decoding (changing `src`) is not supported via this method — remove and
|
|
4495
|
+
* re-add the clip instead.
|
|
4496
|
+
*
|
|
4497
|
+
* Note: `fadeIn` / `fadeOut` / `fadeType` on the partial are written to the
|
|
4498
|
+
* `<daw-clip>` element (so they round-trip in the descriptor), but engine-side
|
|
4499
|
+
* fade application from `<daw-clip>` properties is not yet implemented — see
|
|
4500
|
+
* the broader fade-engine integration tracked separately.
|
|
4501
|
+
*/
|
|
4502
|
+
updateClip(trackId, clipId, partial) {
|
|
4503
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4504
|
+
if (trackEl) {
|
|
4505
|
+
const clipEl = [...trackEl.querySelectorAll("daw-clip")].find(
|
|
4506
|
+
(c) => c.clipId === clipId
|
|
4507
|
+
);
|
|
4508
|
+
if (clipEl) {
|
|
4509
|
+
if (partial.start !== void 0) clipEl.start = partial.start;
|
|
4510
|
+
if (partial.duration !== void 0) clipEl.duration = partial.duration;
|
|
4511
|
+
if (partial.offset !== void 0) clipEl.offset = partial.offset;
|
|
4512
|
+
if (partial.gain !== void 0) clipEl.gain = partial.gain;
|
|
4513
|
+
if (partial.name !== void 0) clipEl.setAttribute("name", partial.name);
|
|
4514
|
+
if (partial.fadeIn !== void 0) clipEl.fadeIn = partial.fadeIn;
|
|
4515
|
+
if (partial.fadeOut !== void 0) clipEl.fadeOut = partial.fadeOut;
|
|
4516
|
+
if (partial.fadeType !== void 0) clipEl.setAttribute("fade-type", partial.fadeType);
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
const t = this._engineTracks.get(trackId);
|
|
4521
|
+
if (!t) {
|
|
4522
|
+
console.warn('[dawcore] updateClip: no track found for id "' + trackId + '"');
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
const idx = t.clips.findIndex((c) => c.id === clipId);
|
|
4526
|
+
if (idx === -1) {
|
|
4527
|
+
console.warn(
|
|
4528
|
+
'[dawcore] updateClip: clip "' + clipId + '" not found in track "' + trackId + '"'
|
|
4529
|
+
);
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
const oldClip = t.clips[idx];
|
|
4533
|
+
const sr = oldClip.sampleRate ?? this.effectiveSampleRate;
|
|
4534
|
+
const updatedClip = {
|
|
4535
|
+
...oldClip,
|
|
4536
|
+
...partial.start !== void 0 && { startSample: Math.round(partial.start * sr) },
|
|
4537
|
+
...partial.duration !== void 0 && partial.duration > 0 && { durationSamples: Math.round(partial.duration * sr) },
|
|
4538
|
+
...partial.offset !== void 0 && { offsetSamples: Math.round(partial.offset * sr) },
|
|
4539
|
+
...partial.gain !== void 0 && { gain: partial.gain },
|
|
4540
|
+
...partial.name !== void 0 && { name: partial.name }
|
|
4541
|
+
};
|
|
4542
|
+
const updatedClips = [...t.clips];
|
|
4543
|
+
updatedClips[idx] = updatedClip;
|
|
4544
|
+
const updatedTrack = { ...t, clips: updatedClips };
|
|
4545
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
4546
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
4547
|
+
}
|
|
4548
|
+
_buildClipElement(config) {
|
|
4549
|
+
const clipEl = document.createElement("daw-clip");
|
|
4550
|
+
if (config.src !== void 0) clipEl.setAttribute("src", config.src);
|
|
4551
|
+
if (config.peaksSrc !== void 0) clipEl.setAttribute("peaks-src", config.peaksSrc);
|
|
4552
|
+
if (config.start !== void 0) clipEl.start = config.start;
|
|
4553
|
+
if (config.duration !== void 0) clipEl.duration = config.duration;
|
|
4554
|
+
if (config.offset !== void 0) clipEl.offset = config.offset;
|
|
4555
|
+
if (config.gain !== void 0) clipEl.gain = config.gain;
|
|
4556
|
+
if (config.name !== void 0) clipEl.setAttribute("name", config.name);
|
|
4557
|
+
if (config.fadeIn !== void 0) clipEl.fadeIn = config.fadeIn;
|
|
4558
|
+
if (config.fadeOut !== void 0) clipEl.fadeOut = config.fadeOut;
|
|
4559
|
+
if (config.fadeType !== void 0) clipEl.setAttribute("fade-type", config.fadeType);
|
|
4560
|
+
return clipEl;
|
|
4561
|
+
}
|
|
3953
4562
|
// --- Playback ---
|
|
3954
4563
|
async play(startTime) {
|
|
3955
4564
|
try {
|
|
@@ -4123,38 +4732,34 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
4123
4732
|
if (!playhead || !this._engine) return;
|
|
4124
4733
|
const engine = this._engine;
|
|
4125
4734
|
const ctx = this.audioContext;
|
|
4735
|
+
const audibleTime = () => {
|
|
4736
|
+
const outputLatency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4737
|
+
const t = engine.getCurrentTime() - outputLatency - engine.lookAhead;
|
|
4738
|
+
return Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
4739
|
+
};
|
|
4126
4740
|
if (this.scaleMode === "beats") {
|
|
4127
4741
|
const secondsToTicksFn = (s) => this._secondsToTicks(s);
|
|
4128
|
-
playhead.startBeatsAnimationWithMap(
|
|
4129
|
-
() => {
|
|
4130
|
-
const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4131
|
-
return Math.max(0, engine.getCurrentTime() - latency);
|
|
4132
|
-
},
|
|
4133
|
-
secondsToTicksFn,
|
|
4134
|
-
this.ticksPerPixel
|
|
4135
|
-
);
|
|
4742
|
+
playhead.startBeatsAnimationWithMap(audibleTime, secondsToTicksFn, this.ticksPerPixel);
|
|
4136
4743
|
} else {
|
|
4137
|
-
playhead.startAnimation(
|
|
4138
|
-
() => {
|
|
4139
|
-
const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4140
|
-
return Math.max(0, engine.getCurrentTime() - latency);
|
|
4141
|
-
},
|
|
4142
|
-
this.effectiveSampleRate,
|
|
4143
|
-
this.samplesPerPixel
|
|
4144
|
-
);
|
|
4744
|
+
playhead.startAnimation(audibleTime, this.effectiveSampleRate, this.samplesPerPixel);
|
|
4145
4745
|
}
|
|
4146
4746
|
}
|
|
4147
4747
|
_stopPlayhead() {
|
|
4148
4748
|
const playhead = this._getPlayhead();
|
|
4149
4749
|
if (!playhead) return;
|
|
4750
|
+
const ctx = this.audioContext;
|
|
4751
|
+
const outputLatency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4752
|
+
const lookAhead = this._engine?.lookAhead ?? 0;
|
|
4753
|
+
const t = this._currentTime - outputLatency - lookAhead;
|
|
4754
|
+
const visualTime = Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
4150
4755
|
if (this.scaleMode === "beats") {
|
|
4151
4756
|
playhead.stopBeatsAnimationWithMap(
|
|
4152
|
-
|
|
4757
|
+
visualTime,
|
|
4153
4758
|
(s) => this._secondsToTicks(s),
|
|
4154
4759
|
this.ticksPerPixel
|
|
4155
4760
|
);
|
|
4156
4761
|
} else {
|
|
4157
|
-
playhead.stopAnimation(
|
|
4762
|
+
playhead.stopAnimation(visualTime, this.effectiveSampleRate, this.samplesPerPixel);
|
|
4158
4763
|
}
|
|
4159
4764
|
}
|
|
4160
4765
|
_getPlayhead() {
|
|
@@ -4202,7 +4807,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
4202
4807
|
};
|
|
4203
4808
|
});
|
|
4204
4809
|
return import_lit13.html`
|
|
4205
|
-
${orderedTracks.length > 0 ? import_lit13.html`<div class="controls-column">
|
|
4810
|
+
${orderedTracks.length > 0 || this.indefinitePlayback ? import_lit13.html`<div class="controls-column">
|
|
4206
4811
|
${this.timescale ? import_lit13.html`<div style="height: 30px;"></div>` : ""}
|
|
4207
4812
|
${orderedTracks.map(
|
|
4208
4813
|
(t) => import_lit13.html`
|
|
@@ -4228,7 +4833,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
4228
4833
|
@dragleave=${this._onDragLeave}
|
|
4229
4834
|
@drop=${this._onDrop}
|
|
4230
4835
|
>
|
|
4231
|
-
${(orderedTracks.length > 0 || this.scaleMode === "beats") && this.timescale ? import_lit13.html`<daw-ruler
|
|
4836
|
+
${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? import_lit13.html`<daw-ruler
|
|
4232
4837
|
.samplesPerPixel=${spp}
|
|
4233
4838
|
.sampleRate=${this.effectiveSampleRate}
|
|
4234
4839
|
.duration=${this._duration}
|
|
@@ -4248,7 +4853,7 @@ var DawEditorElement = class extends import_lit13.LitElement {
|
|
|
4248
4853
|
.length=${this._totalWidth}
|
|
4249
4854
|
.height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
|
|
4250
4855
|
></daw-grid>` : ""}
|
|
4251
|
-
${orderedTracks.length > 0 || this.scaleMode === "beats" ? import_lit13.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
|
|
4856
|
+
${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? import_lit13.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
|
|
4252
4857
|
<daw-playhead></daw-playhead>` : ""}
|
|
4253
4858
|
${orderedTracks.map((t) => {
|
|
4254
4859
|
const channelHeight = this.waveHeight;
|
|
@@ -4445,6 +5050,9 @@ __decorateClass([
|
|
|
4445
5050
|
__decorateClass([
|
|
4446
5051
|
(0, import_decorators11.property)({ type: Boolean, attribute: "interactive-clips" })
|
|
4447
5052
|
], DawEditorElement.prototype, "interactiveClips", 2);
|
|
5053
|
+
__decorateClass([
|
|
5054
|
+
(0, import_decorators11.property)({ type: Boolean, attribute: "indefinite-playback" })
|
|
5055
|
+
], DawEditorElement.prototype, "indefinitePlayback", 2);
|
|
4448
5056
|
__decorateClass([
|
|
4449
5057
|
(0, import_decorators11.property)({ type: String, attribute: "scale-mode" })
|
|
4450
5058
|
], DawEditorElement.prototype, "scaleMode", 2);
|
|
@@ -4584,12 +5192,14 @@ var DawRulerElement = class extends import_lit14.LitElement {
|
|
|
4584
5192
|
ppqn: this.ppqn
|
|
4585
5193
|
});
|
|
4586
5194
|
this._tickData = null;
|
|
4587
|
-
} else if (this.duration > 0) {
|
|
5195
|
+
} else if (this.duration > 0 || this.totalWidth > 0) {
|
|
5196
|
+
const widthDerivedDuration = this.totalWidth * this.samplesPerPixel / this.sampleRate;
|
|
5197
|
+
const effectiveDuration = Math.max(this.duration, widthDerivedDuration);
|
|
4588
5198
|
this._musicalTickData = null;
|
|
4589
5199
|
this._tickData = computeTemporalTicks(
|
|
4590
5200
|
this.samplesPerPixel,
|
|
4591
5201
|
this.sampleRate,
|
|
4592
|
-
|
|
5202
|
+
effectiveDuration,
|
|
4593
5203
|
this.rulerHeight
|
|
4594
5204
|
);
|
|
4595
5205
|
} else {
|
|
@@ -5042,6 +5652,7 @@ DawKeyboardShortcutsElement = __decorateClass([
|
|
|
5042
5652
|
DawTransportElement,
|
|
5043
5653
|
DawWaveformElement,
|
|
5044
5654
|
RecordingController,
|
|
5655
|
+
isDomClip,
|
|
5045
5656
|
splitAtPlayhead
|
|
5046
5657
|
});
|
|
5047
5658
|
//# sourceMappingURL=index.js.map
|