@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.mjs
CHANGED
|
@@ -27,11 +27,54 @@ var DawClipElement = class extends LitElement {
|
|
|
27
27
|
this.fadeOut = 0;
|
|
28
28
|
this.fadeType = "linear";
|
|
29
29
|
this.clipId = crypto.randomUUID();
|
|
30
|
+
// Removal is detected by the editor's MutationObserver — detached elements
|
|
31
|
+
// cannot bubble events to ancestors.
|
|
32
|
+
this._hasRendered = false;
|
|
30
33
|
}
|
|
31
34
|
// Light DOM — no visual rendering, just a data container
|
|
32
35
|
createRenderRoot() {
|
|
33
36
|
return this;
|
|
34
37
|
}
|
|
38
|
+
connectedCallback() {
|
|
39
|
+
super.connectedCallback();
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
this.dispatchEvent(
|
|
42
|
+
new CustomEvent("daw-clip-connected", {
|
|
43
|
+
bubbles: true,
|
|
44
|
+
composed: true,
|
|
45
|
+
detail: { clipId: this.clipId, element: this }
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}, 0);
|
|
49
|
+
}
|
|
50
|
+
updated(changed) {
|
|
51
|
+
if (!this._hasRendered) {
|
|
52
|
+
this._hasRendered = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const clipProps = [
|
|
56
|
+
"src",
|
|
57
|
+
"peaksSrc",
|
|
58
|
+
"start",
|
|
59
|
+
"duration",
|
|
60
|
+
"offset",
|
|
61
|
+
"gain",
|
|
62
|
+
"name",
|
|
63
|
+
"fadeIn",
|
|
64
|
+
"fadeOut",
|
|
65
|
+
"fadeType"
|
|
66
|
+
];
|
|
67
|
+
if (clipProps.some((p) => changed.has(p))) {
|
|
68
|
+
const trackEl = this.closest("daw-track");
|
|
69
|
+
this.dispatchEvent(
|
|
70
|
+
new CustomEvent("daw-clip-update", {
|
|
71
|
+
bubbles: true,
|
|
72
|
+
composed: true,
|
|
73
|
+
detail: { trackId: trackEl?.trackId ?? "", clipId: this.clipId }
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
35
78
|
};
|
|
36
79
|
__decorateClass([
|
|
37
80
|
property()
|
|
@@ -839,6 +882,13 @@ DawStopButtonElement = __decorateClass([
|
|
|
839
882
|
// src/elements/daw-editor.ts
|
|
840
883
|
import { LitElement as LitElement9, html as html8, css as css8 } from "lit";
|
|
841
884
|
import { customElement as customElement11, property as property7, state as state3 } from "lit/decorators.js";
|
|
885
|
+
|
|
886
|
+
// src/types.ts
|
|
887
|
+
function isDomClip(desc) {
|
|
888
|
+
return desc.kind === "dom";
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/elements/daw-editor.ts
|
|
842
892
|
import {
|
|
843
893
|
createClip as createClip3,
|
|
844
894
|
createClipFromSeconds,
|
|
@@ -1846,6 +1896,7 @@ var ViewportController = class {
|
|
|
1846
1896
|
constructor(host) {
|
|
1847
1897
|
this._scrollContainer = null;
|
|
1848
1898
|
this._lastScrollLeft = 0;
|
|
1899
|
+
this._resizeObserver = null;
|
|
1849
1900
|
// Permissive defaults: render everything until scroll container is attached
|
|
1850
1901
|
this.visibleStart = -Infinity;
|
|
1851
1902
|
this.visibleEnd = Infinity;
|
|
@@ -1878,13 +1929,26 @@ var ViewportController = class {
|
|
|
1878
1929
|
}
|
|
1879
1930
|
hostDisconnected() {
|
|
1880
1931
|
this._scrollContainer?.removeEventListener("scroll", this._onScroll);
|
|
1932
|
+
this._resizeObserver?.disconnect();
|
|
1933
|
+
this._resizeObserver = null;
|
|
1881
1934
|
this._scrollContainer = null;
|
|
1882
1935
|
}
|
|
1883
1936
|
_attachScrollContainer(container) {
|
|
1884
1937
|
this._scrollContainer?.removeEventListener("scroll", this._onScroll);
|
|
1938
|
+
this._resizeObserver?.disconnect();
|
|
1885
1939
|
this._scrollContainer = container;
|
|
1886
1940
|
container.addEventListener("scroll", this._onScroll, { passive: true });
|
|
1887
1941
|
this._update(container.scrollLeft, container.clientWidth);
|
|
1942
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1943
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
1944
|
+
if (!this._scrollContainer) return;
|
|
1945
|
+
const next = this._scrollContainer.clientWidth;
|
|
1946
|
+
if (next === this.containerWidth) return;
|
|
1947
|
+
this._update(this._scrollContainer.scrollLeft, next);
|
|
1948
|
+
this._host.requestUpdate();
|
|
1949
|
+
});
|
|
1950
|
+
this._resizeObserver.observe(container);
|
|
1951
|
+
}
|
|
1888
1952
|
this._host.requestUpdate();
|
|
1889
1953
|
}
|
|
1890
1954
|
_update(scrollLeft, containerWidth) {
|
|
@@ -2850,6 +2914,7 @@ async function loadFiles(host, files) {
|
|
|
2850
2914
|
soloed: false,
|
|
2851
2915
|
clips: [
|
|
2852
2916
|
{
|
|
2917
|
+
kind: "drop",
|
|
2853
2918
|
src: "",
|
|
2854
2919
|
peaksSrc: "",
|
|
2855
2920
|
start: 0,
|
|
@@ -2936,6 +3001,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
|
|
|
2936
3001
|
if (desc) {
|
|
2937
3002
|
const sr = host.effectiveSampleRate;
|
|
2938
3003
|
const clipDesc = {
|
|
3004
|
+
kind: "drop",
|
|
2939
3005
|
src: "",
|
|
2940
3006
|
peaksSrc: "",
|
|
2941
3007
|
start: startSample / sr,
|
|
@@ -3172,6 +3238,7 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3172
3238
|
this.clipHeaders = false;
|
|
3173
3239
|
this.clipHeaderHeight = 20;
|
|
3174
3240
|
this.interactiveClips = false;
|
|
3241
|
+
this.indefinitePlayback = false;
|
|
3175
3242
|
this.scaleMode = "temporal";
|
|
3176
3243
|
this._ticksPerPixel = 24;
|
|
3177
3244
|
this._bpm = 120;
|
|
@@ -3288,6 +3355,51 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3288
3355
|
this._onTrackRemoved(trackId);
|
|
3289
3356
|
}
|
|
3290
3357
|
};
|
|
3358
|
+
// --- Clip lifecycle ---
|
|
3359
|
+
this._onClipConnected = (e) => {
|
|
3360
|
+
const detail = e.detail;
|
|
3361
|
+
const clipEl = detail.element;
|
|
3362
|
+
if (!(clipEl instanceof HTMLElement)) return;
|
|
3363
|
+
const trackEl = clipEl.closest("daw-track");
|
|
3364
|
+
if (!trackEl) return;
|
|
3365
|
+
const trackId = trackEl.trackId;
|
|
3366
|
+
if (!this._engineTracks.has(trackId)) {
|
|
3367
|
+
const desc = this._tracks.get(trackId);
|
|
3368
|
+
if (desc && !desc.clips.some((c) => isDomClip(c) && c.clipId === clipEl.clipId)) {
|
|
3369
|
+
console.warn(
|
|
3370
|
+
'[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.'
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
const clipDesc = {
|
|
3376
|
+
kind: "dom",
|
|
3377
|
+
clipId: clipEl.clipId,
|
|
3378
|
+
src: clipEl.src,
|
|
3379
|
+
peaksSrc: clipEl.peaksSrc,
|
|
3380
|
+
start: clipEl.start,
|
|
3381
|
+
duration: clipEl.duration,
|
|
3382
|
+
offset: clipEl.offset,
|
|
3383
|
+
gain: clipEl.gain,
|
|
3384
|
+
name: clipEl.name,
|
|
3385
|
+
fadeIn: clipEl.fadeIn,
|
|
3386
|
+
fadeOut: clipEl.fadeOut,
|
|
3387
|
+
fadeType: clipEl.fadeType
|
|
3388
|
+
};
|
|
3389
|
+
this._loadAndAppendClip(trackId, clipDesc);
|
|
3390
|
+
};
|
|
3391
|
+
this._onClipUpdate = (e) => {
|
|
3392
|
+
const clipEl = e.target;
|
|
3393
|
+
if (!(clipEl instanceof HTMLElement) || clipEl.tagName !== "DAW-CLIP") return;
|
|
3394
|
+
const detail = e.detail;
|
|
3395
|
+
if (!detail.trackId) {
|
|
3396
|
+
console.warn(
|
|
3397
|
+
"[dawcore] daw-clip-update fired from a <daw-clip> not nested in a <daw-track> \u2014 ignored"
|
|
3398
|
+
);
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
this._applyClipUpdate(detail.trackId, detail.clipId, clipEl);
|
|
3402
|
+
};
|
|
3291
3403
|
// --- File Drop ---
|
|
3292
3404
|
this._onDragOver = (e) => {
|
|
3293
3405
|
if (!this.fileDrop) return;
|
|
@@ -3420,9 +3532,7 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3420
3532
|
this.mono,
|
|
3421
3533
|
singleClipOffsets
|
|
3422
3534
|
);
|
|
3423
|
-
|
|
3424
|
-
if (!peakData) return null;
|
|
3425
|
-
return { data: peakData.data, length: peakData.length };
|
|
3535
|
+
return result.get(clipId) ?? null;
|
|
3426
3536
|
}
|
|
3427
3537
|
get effectiveSampleRate() {
|
|
3428
3538
|
return this._resolvedSampleRate ?? this.sampleRate;
|
|
@@ -3476,7 +3586,13 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3476
3586
|
const minTicks = 32 * num * this.ppqn;
|
|
3477
3587
|
return Math.ceil(Math.max(contentTicks, minTicks) / this.ticksPerPixel);
|
|
3478
3588
|
}
|
|
3479
|
-
|
|
3589
|
+
const naturalWidth = Math.ceil(
|
|
3590
|
+
this._duration * this.effectiveSampleRate / this.samplesPerPixel
|
|
3591
|
+
);
|
|
3592
|
+
if (this.indefinitePlayback) {
|
|
3593
|
+
return Math.max(naturalWidth, this._viewport.containerWidth);
|
|
3594
|
+
}
|
|
3595
|
+
return naturalWidth;
|
|
3480
3596
|
}
|
|
3481
3597
|
/** Grid height when no tracks exist — matches scroll area's rendered height. */
|
|
3482
3598
|
get _emptyGridHeight() {
|
|
@@ -3518,19 +3634,29 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3518
3634
|
this.addEventListener("daw-track-update", this._onTrackUpdate);
|
|
3519
3635
|
this.addEventListener("daw-track-control", this._onTrackControl);
|
|
3520
3636
|
this.addEventListener("daw-track-remove", this._onTrackRemoveRequest);
|
|
3637
|
+
this.addEventListener("daw-clip-connected", this._onClipConnected);
|
|
3638
|
+
this.addEventListener("daw-clip-update", this._onClipUpdate);
|
|
3521
3639
|
this._childObserver = new MutationObserver((mutations) => {
|
|
3522
3640
|
for (const mutation of mutations) {
|
|
3523
3641
|
for (const node of mutation.removedNodes) {
|
|
3524
3642
|
if (node instanceof HTMLElement) {
|
|
3525
3643
|
if (node.tagName === "DAW-TRACK") {
|
|
3526
3644
|
this._onTrackRemoved(node.trackId);
|
|
3645
|
+
} else if (node.tagName === "DAW-CLIP") {
|
|
3646
|
+
this._onClipRemovedFromDom(node);
|
|
3527
3647
|
}
|
|
3528
|
-
const
|
|
3529
|
-
if (
|
|
3530
|
-
for (const track of
|
|
3648
|
+
const nestedTracks = node.querySelectorAll?.("daw-track");
|
|
3649
|
+
if (nestedTracks) {
|
|
3650
|
+
for (const track of nestedTracks) {
|
|
3531
3651
|
this._onTrackRemoved(track.trackId);
|
|
3532
3652
|
}
|
|
3533
3653
|
}
|
|
3654
|
+
const nestedClips = node.querySelectorAll?.("daw-clip");
|
|
3655
|
+
if (nestedClips) {
|
|
3656
|
+
for (const clip of nestedClips) {
|
|
3657
|
+
this._onClipRemovedFromDom(clip);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3534
3660
|
}
|
|
3535
3661
|
}
|
|
3536
3662
|
}
|
|
@@ -3543,6 +3669,8 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3543
3669
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
3544
3670
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
3545
3671
|
this.removeEventListener("daw-track-remove", this._onTrackRemoveRequest);
|
|
3672
|
+
this.removeEventListener("daw-clip-connected", this._onClipConnected);
|
|
3673
|
+
this.removeEventListener("daw-clip-update", this._onClipUpdate);
|
|
3546
3674
|
this._childObserver?.disconnect();
|
|
3547
3675
|
this._childObserver = null;
|
|
3548
3676
|
this._trackElements.clear();
|
|
@@ -3608,11 +3736,257 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3608
3736
|
this._stopPlayhead();
|
|
3609
3737
|
}
|
|
3610
3738
|
}
|
|
3739
|
+
_onClipRemovedFromDom(clipEl) {
|
|
3740
|
+
const clipId = clipEl.clipId;
|
|
3741
|
+
for (const [trackId, t] of this._engineTracks.entries()) {
|
|
3742
|
+
if (t.clips.some((c) => c.id === clipId)) {
|
|
3743
|
+
this._removeClipFromTrack(trackId, clipId);
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
if (this._clipBuffers.has(clipId) || this._clipOffsets.has(clipId) || this._peaksData.has(clipId)) {
|
|
3748
|
+
console.warn(
|
|
3749
|
+
'[dawcore] _onClipRemovedFromDom: orphaned cache entries for clip "' + clipId + '" \u2014 purging (DOM/engine id mismatch?)'
|
|
3750
|
+
);
|
|
3751
|
+
this._purgeClipCaches(clipId);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
async _loadAndAppendClip(trackId, clipDesc) {
|
|
3755
|
+
if (!clipDesc.src) return;
|
|
3756
|
+
const clipId = clipDesc.clipId;
|
|
3757
|
+
let insertedClipId = null;
|
|
3758
|
+
try {
|
|
3759
|
+
const waveformDataPromise = clipDesc.peaksSrc ? this._resolvePeaks(clipDesc.peaksSrc) : Promise.resolve(null);
|
|
3760
|
+
const audioPromise = this._fetchAndDecode(clipDesc.src);
|
|
3761
|
+
const [waveformData, audioBuffer] = await Promise.all([waveformDataPromise, audioPromise]);
|
|
3762
|
+
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
3763
|
+
const clip = await this._finalizeAudioClip(clipDesc, audioBuffer, waveformData);
|
|
3764
|
+
insertedClipId = clip.id;
|
|
3765
|
+
const t = this._engineTracks.get(trackId);
|
|
3766
|
+
if (!t) {
|
|
3767
|
+
this._purgeClipCaches(clip.id);
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
const updatedTrack = { ...t, clips: [...t.clips, clip] };
|
|
3771
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
3772
|
+
const desc = this._tracks.get(trackId);
|
|
3773
|
+
if (desc) {
|
|
3774
|
+
this._tracks = new Map(this._tracks).set(trackId, {
|
|
3775
|
+
...desc,
|
|
3776
|
+
clips: [...desc.clips, clipDesc]
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
3780
|
+
this.dispatchEvent(
|
|
3781
|
+
new CustomEvent("daw-clip-ready", {
|
|
3782
|
+
bubbles: true,
|
|
3783
|
+
composed: true,
|
|
3784
|
+
detail: { trackId, clipId: clip.id }
|
|
3785
|
+
})
|
|
3786
|
+
);
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
console.warn("[dawcore] _loadAndAppendClip failed: " + String(err));
|
|
3789
|
+
if (insertedClipId) this._purgeClipCaches(insertedClipId);
|
|
3790
|
+
this.dispatchEvent(
|
|
3791
|
+
new CustomEvent("daw-clip-error", {
|
|
3792
|
+
bubbles: true,
|
|
3793
|
+
composed: true,
|
|
3794
|
+
detail: { trackId, clipId: insertedClipId ?? clipId, error: err }
|
|
3795
|
+
})
|
|
3796
|
+
);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Resolve pre-computed peaks for a clip: fetch the .dat/.json, validate the
|
|
3801
|
+
* sample rate matches the AudioContext, return the WaveformData or null.
|
|
3802
|
+
* Warns on fetch failure and on sample-rate mismatch — never silent.
|
|
3803
|
+
*
|
|
3804
|
+
* Shared between `_loadTrack` (peaks-first preview path) and
|
|
3805
|
+
* `_loadAndAppendClip` (incremental late-append).
|
|
3806
|
+
*/
|
|
3807
|
+
async _resolvePeaks(peaksSrc) {
|
|
3808
|
+
try {
|
|
3809
|
+
const wd = await this._fetchPeaks(peaksSrc);
|
|
3810
|
+
const contextRate = this.audioContext.sampleRate;
|
|
3811
|
+
if (wd.sample_rate === contextRate) return wd;
|
|
3812
|
+
console.warn(
|
|
3813
|
+
"[dawcore] Pre-computed peaks at " + wd.sample_rate + " Hz do not match AudioContext at " + contextRate + " Hz \u2014 ignoring " + peaksSrc + ", generating from audio"
|
|
3814
|
+
);
|
|
3815
|
+
return null;
|
|
3816
|
+
} catch (err) {
|
|
3817
|
+
console.warn(
|
|
3818
|
+
"[dawcore] Failed to load peaks from " + peaksSrc + ": " + String(err) + " \u2014 falling back to AudioBuffer generation"
|
|
3819
|
+
);
|
|
3820
|
+
return null;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Construct an AudioClip from a decoded buffer (and optional WaveformData),
|
|
3825
|
+
* align its id with the source `<daw-clip>.clipId` when present, populate
|
|
3826
|
+
* `_clipBuffers` / `_clipOffsets`, generate peaks via the worker pipeline,
|
|
3827
|
+
* and populate `_peaksData`. Returns the finished AudioClip.
|
|
3828
|
+
*
|
|
3829
|
+
* Shared between `_loadTrack`'s standard path and `_loadAndAppendClip`.
|
|
3830
|
+
* Not used by `_loadTrack`'s peaks-first preview path because that path
|
|
3831
|
+
* uses sync `extractPeaks` and inserts a preview track BEFORE audio decode.
|
|
3832
|
+
*/
|
|
3833
|
+
async _finalizeAudioClip(clipDesc, audioBuffer, waveformData) {
|
|
3834
|
+
let clip;
|
|
3835
|
+
if (waveformData) {
|
|
3836
|
+
const wdRate = waveformData.sample_rate;
|
|
3837
|
+
clip = createClip3({
|
|
3838
|
+
audioBuffer,
|
|
3839
|
+
waveformData,
|
|
3840
|
+
startSample: Math.round(clipDesc.start * wdRate),
|
|
3841
|
+
durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
|
|
3842
|
+
offsetSamples: Math.round(clipDesc.offset * wdRate),
|
|
3843
|
+
gain: clipDesc.gain,
|
|
3844
|
+
name: clipDesc.name,
|
|
3845
|
+
sampleRate: wdRate,
|
|
3846
|
+
sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
|
|
3847
|
+
});
|
|
3848
|
+
this._peakPipeline.cacheWaveformData(audioBuffer, waveformData);
|
|
3849
|
+
} else {
|
|
3850
|
+
clip = createClipFromSeconds({
|
|
3851
|
+
audioBuffer,
|
|
3852
|
+
startTime: clipDesc.start,
|
|
3853
|
+
duration: clipDesc.duration || audioBuffer.duration,
|
|
3854
|
+
offset: clipDesc.offset,
|
|
3855
|
+
gain: clipDesc.gain,
|
|
3856
|
+
name: clipDesc.name,
|
|
3857
|
+
sampleRate: audioBuffer.sampleRate,
|
|
3858
|
+
sourceDuration: audioBuffer.duration
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
if (isDomClip(clipDesc)) clip.id = clipDesc.clipId;
|
|
3862
|
+
this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
|
|
3863
|
+
this._clipOffsets.set(clip.id, {
|
|
3864
|
+
offsetSamples: clip.offsetSamples,
|
|
3865
|
+
durationSamples: clip.durationSamples
|
|
3866
|
+
});
|
|
3867
|
+
let peakData;
|
|
3868
|
+
try {
|
|
3869
|
+
peakData = await this._peakPipeline.generatePeaks(
|
|
3870
|
+
audioBuffer,
|
|
3871
|
+
this._renderSpp,
|
|
3872
|
+
this.mono,
|
|
3873
|
+
clip.offsetSamples,
|
|
3874
|
+
clip.durationSamples
|
|
3875
|
+
);
|
|
3876
|
+
} catch (err) {
|
|
3877
|
+
this._purgeClipCaches(clip.id);
|
|
3878
|
+
throw err;
|
|
3879
|
+
}
|
|
3880
|
+
this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
|
|
3881
|
+
if (waveformData) {
|
|
3882
|
+
this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
|
|
3883
|
+
}
|
|
3884
|
+
return clip;
|
|
3885
|
+
}
|
|
3886
|
+
/** Remove a single clip from all per-clip caches. Used by error rollbacks. */
|
|
3887
|
+
_purgeClipCaches(clipId) {
|
|
3888
|
+
const nextBuffers = new Map(this._clipBuffers);
|
|
3889
|
+
nextBuffers.delete(clipId);
|
|
3890
|
+
this._clipBuffers = nextBuffers;
|
|
3891
|
+
const nextPeaks = new Map(this._peaksData);
|
|
3892
|
+
nextPeaks.delete(clipId);
|
|
3893
|
+
this._peaksData = nextPeaks;
|
|
3894
|
+
this._clipOffsets.delete(clipId);
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Recompute duration and forward an updated track to the engine. Single
|
|
3898
|
+
* source of truth for the incremental-vs-full-rebuild policy used by every
|
|
3899
|
+
* clip-level mutation (addClip, updateClip, removeClip, _applyClipUpdate).
|
|
3900
|
+
* Use the engine's incremental updateTrack when available; otherwise fall
|
|
3901
|
+
* back to full setTracks (legacy adapters).
|
|
3902
|
+
*/
|
|
3903
|
+
_commitTrackChange(trackId, updatedTrack) {
|
|
3904
|
+
this._recomputeDuration();
|
|
3905
|
+
if (this._engine?.updateTrack) this._engine.updateTrack(trackId, updatedTrack);
|
|
3906
|
+
else if (this._engine) this._engine.setTracks([...this._engineTracks.values()]);
|
|
3907
|
+
}
|
|
3908
|
+
_applyClipUpdate(trackId, clipId, clipEl) {
|
|
3909
|
+
const t = this._engineTracks.get(trackId);
|
|
3910
|
+
if (!t) {
|
|
3911
|
+
console.warn('[dawcore] _applyClipUpdate: no engine track for id "' + trackId + '"');
|
|
3912
|
+
return;
|
|
3913
|
+
}
|
|
3914
|
+
const idx = t.clips.findIndex((c) => c.id === clipId);
|
|
3915
|
+
if (idx === -1) {
|
|
3916
|
+
console.warn(
|
|
3917
|
+
'[dawcore] _applyClipUpdate: clip "' + clipId + '" not found in track "' + trackId + '" (DOM/engine clip-id misalignment?)'
|
|
3918
|
+
);
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
const oldClip = t.clips[idx];
|
|
3922
|
+
const sr = oldClip.sampleRate ?? this.effectiveSampleRate;
|
|
3923
|
+
const newStartSample = Math.round(clipEl.start * sr);
|
|
3924
|
+
const newDurationSamples = clipEl.duration > 0 ? Math.round(clipEl.duration * sr) : oldClip.durationSamples;
|
|
3925
|
+
const newOffsetSamples = Math.round(clipEl.offset * sr);
|
|
3926
|
+
const updatedClip = {
|
|
3927
|
+
...oldClip,
|
|
3928
|
+
startSample: newStartSample,
|
|
3929
|
+
durationSamples: newDurationSamples,
|
|
3930
|
+
offsetSamples: newOffsetSamples,
|
|
3931
|
+
gain: clipEl.gain,
|
|
3932
|
+
name: clipEl.name || oldClip.name
|
|
3933
|
+
};
|
|
3934
|
+
const updatedClips = [...t.clips];
|
|
3935
|
+
updatedClips[idx] = updatedClip;
|
|
3936
|
+
const updatedTrack = { ...t, clips: updatedClips };
|
|
3937
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
3938
|
+
const boundsChanged = oldClip.offsetSamples !== newOffsetSamples || oldClip.durationSamples !== newDurationSamples;
|
|
3939
|
+
if (boundsChanged) {
|
|
3940
|
+
this._clipOffsets.set(clipId, {
|
|
3941
|
+
offsetSamples: newOffsetSamples,
|
|
3942
|
+
durationSamples: newDurationSamples
|
|
3943
|
+
});
|
|
3944
|
+
const peaks = this.reextractClipPeaks(clipId, newOffsetSamples, newDurationSamples);
|
|
3945
|
+
if (peaks) {
|
|
3946
|
+
this._peaksData = new Map(this._peaksData).set(clipId, peaks);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
3950
|
+
}
|
|
3951
|
+
_removeClipFromTrack(trackId, clipId) {
|
|
3952
|
+
const t = this._engineTracks.get(trackId);
|
|
3953
|
+
if (!t) {
|
|
3954
|
+
console.warn('[dawcore] _removeClipFromTrack: no engine track for id "' + trackId + '"');
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
const updatedClips = t.clips.filter((c) => c.id !== clipId);
|
|
3958
|
+
if (updatedClips.length === t.clips.length) {
|
|
3959
|
+
console.warn(
|
|
3960
|
+
'[dawcore] _removeClipFromTrack: clip "' + clipId + '" not found in track "' + trackId + '"'
|
|
3961
|
+
);
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
const updatedTrack = { ...t, clips: updatedClips };
|
|
3965
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
3966
|
+
const nextBuffers = new Map(this._clipBuffers);
|
|
3967
|
+
nextBuffers.delete(clipId);
|
|
3968
|
+
this._clipBuffers = nextBuffers;
|
|
3969
|
+
this._clipOffsets.delete(clipId);
|
|
3970
|
+
const nextPeaks = new Map(this._peaksData);
|
|
3971
|
+
nextPeaks.delete(clipId);
|
|
3972
|
+
this._peaksData = nextPeaks;
|
|
3973
|
+
const desc = this._tracks.get(trackId);
|
|
3974
|
+
if (desc) {
|
|
3975
|
+
this._tracks = new Map(this._tracks).set(trackId, {
|
|
3976
|
+
...desc,
|
|
3977
|
+
// Only DOM-sourced clips have an id to match; drop-sourced clips are
|
|
3978
|
+
// filtered through unchanged (their identity is the descriptor itself).
|
|
3979
|
+
clips: desc.clips.filter((c) => !(isDomClip(c) && c.clipId === clipId))
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
3983
|
+
}
|
|
3611
3984
|
_readTrackDescriptor(trackEl) {
|
|
3612
3985
|
const clipEls = trackEl.querySelectorAll("daw-clip");
|
|
3613
3986
|
const clips = [];
|
|
3614
3987
|
if (clipEls.length === 0 && trackEl.src) {
|
|
3615
3988
|
clips.push({
|
|
3989
|
+
kind: "drop",
|
|
3616
3990
|
src: trackEl.src,
|
|
3617
3991
|
peaksSrc: "",
|
|
3618
3992
|
start: 0,
|
|
@@ -3627,6 +4001,8 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3627
4001
|
} else {
|
|
3628
4002
|
for (const clipEl of clipEls) {
|
|
3629
4003
|
clips.push({
|
|
4004
|
+
kind: "dom",
|
|
4005
|
+
clipId: clipEl.clipId,
|
|
3630
4006
|
src: clipEl.src,
|
|
3631
4007
|
peaksSrc: clipEl.peaksSrc,
|
|
3632
4008
|
start: clipEl.start,
|
|
@@ -3656,111 +4032,92 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3656
4032
|
const clips = [];
|
|
3657
4033
|
for (const clipDesc of descriptor.clips) {
|
|
3658
4034
|
if (!clipDesc.src) continue;
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
const
|
|
3665
|
-
const
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
4035
|
+
try {
|
|
4036
|
+
const waveformDataPromise = clipDesc.peaksSrc ? this._resolvePeaks(clipDesc.peaksSrc) : Promise.resolve(null);
|
|
4037
|
+
const audioPromise = this._fetchAndDecode(clipDesc.src);
|
|
4038
|
+
const waveformData = await waveformDataPromise;
|
|
4039
|
+
if (waveformData) {
|
|
4040
|
+
const wdRate = waveformData.sample_rate;
|
|
4041
|
+
const clip2 = createClip3({
|
|
4042
|
+
waveformData,
|
|
4043
|
+
startSample: Math.round(clipDesc.start * wdRate),
|
|
4044
|
+
durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
|
|
4045
|
+
offsetSamples: Math.round(clipDesc.offset * wdRate),
|
|
4046
|
+
gain: clipDesc.gain,
|
|
4047
|
+
name: clipDesc.name,
|
|
4048
|
+
sampleRate: wdRate,
|
|
4049
|
+
sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
|
|
4050
|
+
});
|
|
4051
|
+
if (isDomClip(clipDesc)) clip2.id = clipDesc.clipId;
|
|
4052
|
+
const effectiveScale = Math.max(this._renderSpp, waveformData.scale);
|
|
4053
|
+
const peakData = extractPeaks(
|
|
4054
|
+
waveformData,
|
|
4055
|
+
effectiveScale,
|
|
4056
|
+
this.mono,
|
|
4057
|
+
clip2.offsetSamples,
|
|
4058
|
+
clip2.durationSamples
|
|
3676
4059
|
);
|
|
4060
|
+
this._clipOffsets.set(clip2.id, {
|
|
4061
|
+
offsetSamples: clip2.offsetSamples,
|
|
4062
|
+
durationSamples: clip2.durationSamples
|
|
4063
|
+
});
|
|
4064
|
+
this._peaksData = new Map(this._peaksData).set(clip2.id, peakData);
|
|
4065
|
+
this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
|
|
4066
|
+
const previewTrack = createTrack2({
|
|
4067
|
+
name: descriptor.name,
|
|
4068
|
+
clips: [clip2],
|
|
4069
|
+
volume: descriptor.volume,
|
|
4070
|
+
pan: descriptor.pan,
|
|
4071
|
+
muted: descriptor.muted,
|
|
4072
|
+
soloed: descriptor.soloed
|
|
4073
|
+
});
|
|
4074
|
+
previewTrack.id = trackId;
|
|
4075
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
|
|
4076
|
+
this._recomputeDuration();
|
|
4077
|
+
let audioBuffer2;
|
|
4078
|
+
try {
|
|
4079
|
+
audioBuffer2 = await audioPromise;
|
|
4080
|
+
} catch (audioErr) {
|
|
4081
|
+
const nextPeaks = new Map(this._peaksData);
|
|
4082
|
+
nextPeaks.delete(clip2.id);
|
|
4083
|
+
this._peaksData = nextPeaks;
|
|
4084
|
+
this._clipOffsets.delete(clip2.id);
|
|
4085
|
+
const nextEngine = new Map(this._engineTracks);
|
|
4086
|
+
nextEngine.delete(trackId);
|
|
4087
|
+
this._engineTracks = nextEngine;
|
|
4088
|
+
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
4089
|
+
this._recomputeDuration();
|
|
4090
|
+
throw audioErr;
|
|
4091
|
+
}
|
|
4092
|
+
this._resolvedSampleRate = audioBuffer2.sampleRate;
|
|
4093
|
+
const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
|
|
4094
|
+
this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
|
|
4095
|
+
this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
|
|
4096
|
+
clips.push(updatedClip);
|
|
4097
|
+
continue;
|
|
3677
4098
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
const
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
offsetSamples: Math.round(clipDesc.offset * wdRate),
|
|
3686
|
-
gain: clipDesc.gain,
|
|
3687
|
-
name: clipDesc.name,
|
|
3688
|
-
sampleRate: wdRate,
|
|
3689
|
-
sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
|
|
3690
|
-
});
|
|
3691
|
-
const effectiveScale = Math.max(this._renderSpp, waveformData.scale);
|
|
3692
|
-
const peakData2 = extractPeaks(
|
|
3693
|
-
waveformData,
|
|
3694
|
-
effectiveScale,
|
|
3695
|
-
this.mono,
|
|
3696
|
-
clip2.offsetSamples,
|
|
3697
|
-
clip2.durationSamples
|
|
4099
|
+
const audioBuffer = await audioPromise;
|
|
4100
|
+
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
4101
|
+
const clip = await this._finalizeAudioClip(clipDesc, audioBuffer, null);
|
|
4102
|
+
clips.push(clip);
|
|
4103
|
+
} catch (clipErr) {
|
|
4104
|
+
console.warn(
|
|
4105
|
+
'[dawcore] _loadTrack: clip "' + clipDesc.src + '" failed: ' + String(clipErr)
|
|
3698
4106
|
);
|
|
3699
|
-
this.
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
soloed: descriptor.soloed
|
|
3712
|
-
});
|
|
3713
|
-
previewTrack.id = trackId;
|
|
3714
|
-
this._engineTracks = new Map(this._engineTracks).set(trackId, previewTrack);
|
|
3715
|
-
this._recomputeDuration();
|
|
3716
|
-
let audioBuffer2;
|
|
3717
|
-
try {
|
|
3718
|
-
audioBuffer2 = await audioPromise;
|
|
3719
|
-
} catch (audioErr) {
|
|
3720
|
-
const nextPeaks = new Map(this._peaksData);
|
|
3721
|
-
nextPeaks.delete(clip2.id);
|
|
3722
|
-
this._peaksData = nextPeaks;
|
|
3723
|
-
this._clipOffsets.delete(clip2.id);
|
|
3724
|
-
const nextEngine = new Map(this._engineTracks);
|
|
3725
|
-
nextEngine.delete(trackId);
|
|
3726
|
-
this._engineTracks = nextEngine;
|
|
3727
|
-
this._minSamplesPerPixel = this._peakPipeline.getMaxCachedScale(this._clipBuffers);
|
|
3728
|
-
this._recomputeDuration();
|
|
3729
|
-
throw audioErr;
|
|
4107
|
+
if (this.isConnected) {
|
|
4108
|
+
this.dispatchEvent(
|
|
4109
|
+
new CustomEvent("daw-clip-error", {
|
|
4110
|
+
bubbles: true,
|
|
4111
|
+
composed: true,
|
|
4112
|
+
detail: {
|
|
4113
|
+
trackId,
|
|
4114
|
+
clipId: isDomClip(clipDesc) ? clipDesc.clipId : "",
|
|
4115
|
+
error: clipErr
|
|
4116
|
+
}
|
|
4117
|
+
})
|
|
4118
|
+
);
|
|
3730
4119
|
}
|
|
3731
|
-
this._resolvedSampleRate = audioBuffer2.sampleRate;
|
|
3732
|
-
const updatedClip = { ...clip2, audioBuffer: audioBuffer2 };
|
|
3733
|
-
this._clipBuffers = new Map(this._clipBuffers).set(clip2.id, audioBuffer2);
|
|
3734
|
-
this._peakPipeline.cacheWaveformData(audioBuffer2, waveformData);
|
|
3735
|
-
clips.push(updatedClip);
|
|
3736
|
-
continue;
|
|
3737
4120
|
}
|
|
3738
|
-
const audioBuffer = await audioPromise;
|
|
3739
|
-
this._resolvedSampleRate = audioBuffer.sampleRate;
|
|
3740
|
-
const clip = createClipFromSeconds({
|
|
3741
|
-
audioBuffer,
|
|
3742
|
-
startTime: clipDesc.start,
|
|
3743
|
-
duration: clipDesc.duration || audioBuffer.duration,
|
|
3744
|
-
offset: clipDesc.offset,
|
|
3745
|
-
gain: clipDesc.gain,
|
|
3746
|
-
name: clipDesc.name,
|
|
3747
|
-
sampleRate: audioBuffer.sampleRate,
|
|
3748
|
-
sourceDuration: audioBuffer.duration
|
|
3749
|
-
});
|
|
3750
|
-
this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
|
|
3751
|
-
this._clipOffsets.set(clip.id, {
|
|
3752
|
-
offsetSamples: clip.offsetSamples,
|
|
3753
|
-
durationSamples: clip.durationSamples
|
|
3754
|
-
});
|
|
3755
|
-
const peakData = await this._peakPipeline.generatePeaks(
|
|
3756
|
-
audioBuffer,
|
|
3757
|
-
this._renderSpp,
|
|
3758
|
-
this.mono,
|
|
3759
|
-
clip.offsetSamples,
|
|
3760
|
-
clip.durationSamples
|
|
3761
|
-
);
|
|
3762
|
-
this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
|
|
3763
|
-
clips.push(clip);
|
|
3764
4121
|
}
|
|
3765
4122
|
const track = createTrack2({
|
|
3766
4123
|
name: descriptor.name,
|
|
@@ -3770,6 +4127,12 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3770
4127
|
muted: descriptor.muted,
|
|
3771
4128
|
soloed: descriptor.soloed
|
|
3772
4129
|
});
|
|
4130
|
+
const requestedClips = descriptor.clips.filter((c) => c.src).length;
|
|
4131
|
+
if (requestedClips > 0 && clips.length === 0) {
|
|
4132
|
+
throw new Error(
|
|
4133
|
+
"all " + requestedClips + " clip(s) failed to load \u2014 see prior daw-clip-error events"
|
|
4134
|
+
);
|
|
4135
|
+
}
|
|
3773
4136
|
track.id = trackId;
|
|
3774
4137
|
this._engineTracks = new Map(this._engineTracks).set(trackId, track);
|
|
3775
4138
|
this._recomputeDuration();
|
|
@@ -3903,6 +4266,251 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
3903
4266
|
async loadFiles(files) {
|
|
3904
4267
|
return loadFiles(this, files);
|
|
3905
4268
|
}
|
|
4269
|
+
// --- Programmatic Track API ---
|
|
4270
|
+
/**
|
|
4271
|
+
* Build the engine if it hasn't been built yet. Lets consumers obtain a
|
|
4272
|
+
* non-null `editor.engine` before any track has been loaded — useful for
|
|
4273
|
+
* wiring analyzers, effects, or master taps before content arrives.
|
|
4274
|
+
*/
|
|
4275
|
+
async ready() {
|
|
4276
|
+
return this._ensureEngine();
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* Wait for either `readyEvent` or `errorEvent` to fire on this editor for
|
|
4280
|
+
* the entity matching `matchesId`. Listeners are wired synchronously, then
|
|
4281
|
+
* `setup` is called (typical: appendChild). Resolves with `resolveValue`
|
|
4282
|
+
* on ready; rejects with a normalized Error on error. Used by addTrack and
|
|
4283
|
+
* addClip to share their Promise-with-listener-cleanup machinery.
|
|
4284
|
+
*/
|
|
4285
|
+
_awaitId(readyEvent, errorEvent, matchesId, resolveValue, setup) {
|
|
4286
|
+
return new Promise((resolve, reject) => {
|
|
4287
|
+
const onReady = (e) => {
|
|
4288
|
+
if (!matchesId(e.detail)) return;
|
|
4289
|
+
cleanup();
|
|
4290
|
+
resolve(resolveValue);
|
|
4291
|
+
};
|
|
4292
|
+
const onError = (e) => {
|
|
4293
|
+
const detail = e.detail;
|
|
4294
|
+
if (!matchesId(e.detail)) return;
|
|
4295
|
+
cleanup();
|
|
4296
|
+
const err = detail.error;
|
|
4297
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4298
|
+
};
|
|
4299
|
+
const cleanup = () => {
|
|
4300
|
+
this.removeEventListener(readyEvent, onReady);
|
|
4301
|
+
this.removeEventListener(errorEvent, onError);
|
|
4302
|
+
};
|
|
4303
|
+
this.addEventListener(readyEvent, onReady);
|
|
4304
|
+
this.addEventListener(errorEvent, onError);
|
|
4305
|
+
setup();
|
|
4306
|
+
});
|
|
4307
|
+
}
|
|
4308
|
+
/**
|
|
4309
|
+
* Append a `<daw-track>` element built from `config` and resolve once the
|
|
4310
|
+
* track finishes loading (or reject on `daw-track-error`). Goes through
|
|
4311
|
+
* the same `_loadTrack` pipeline as declarative tracks, so descriptors,
|
|
4312
|
+
* peaks, and clip buffers are populated correctly.
|
|
4313
|
+
*/
|
|
4314
|
+
addTrack(config = {}) {
|
|
4315
|
+
const trackEl = document.createElement("daw-track");
|
|
4316
|
+
if (config.name !== void 0) trackEl.setAttribute("name", config.name);
|
|
4317
|
+
if (config.volume !== void 0) trackEl.volume = config.volume;
|
|
4318
|
+
if (config.pan !== void 0) trackEl.pan = config.pan;
|
|
4319
|
+
if (config.muted) trackEl.setAttribute("muted", "");
|
|
4320
|
+
if (config.soloed) trackEl.setAttribute("soloed", "");
|
|
4321
|
+
for (const clipConfig of config.clips ?? []) {
|
|
4322
|
+
trackEl.appendChild(this._buildClipElement(clipConfig));
|
|
4323
|
+
}
|
|
4324
|
+
return this._awaitId(
|
|
4325
|
+
"daw-track-ready",
|
|
4326
|
+
"daw-track-error",
|
|
4327
|
+
(d) => d.trackId === trackEl.trackId,
|
|
4328
|
+
trackEl,
|
|
4329
|
+
() => this.appendChild(trackEl)
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* Remove a track by id. Equivalent to `trackElement.remove()` —
|
|
4334
|
+
* the editor's MutationObserver handles engine and cache cleanup.
|
|
4335
|
+
* No-op if no matching track exists.
|
|
4336
|
+
*/
|
|
4337
|
+
removeTrack(trackId) {
|
|
4338
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4339
|
+
if (trackEl) {
|
|
4340
|
+
trackEl.remove();
|
|
4341
|
+
} else if (this._engineTracks.has(trackId)) {
|
|
4342
|
+
this._onTrackRemoved(trackId);
|
|
4343
|
+
} else {
|
|
4344
|
+
console.warn('[dawcore] removeTrack: no track found for id "' + trackId + '"');
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* Update reflected attributes on a track. For DOM-element tracks the changes
|
|
4349
|
+
* are written to the `<daw-track>` element (which fires `daw-track-update`);
|
|
4350
|
+
* for tracks without a DOM element (file drops) the descriptor and engine
|
|
4351
|
+
* state are updated in place.
|
|
4352
|
+
*/
|
|
4353
|
+
updateTrack(trackId, partial) {
|
|
4354
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4355
|
+
if (trackEl) {
|
|
4356
|
+
if (partial.name !== void 0) trackEl.setAttribute("name", partial.name);
|
|
4357
|
+
if (partial.volume !== void 0) trackEl.volume = partial.volume;
|
|
4358
|
+
if (partial.pan !== void 0) trackEl.pan = partial.pan;
|
|
4359
|
+
if (partial.muted !== void 0) {
|
|
4360
|
+
if (partial.muted) trackEl.setAttribute("muted", "");
|
|
4361
|
+
else trackEl.removeAttribute("muted");
|
|
4362
|
+
}
|
|
4363
|
+
if (partial.soloed !== void 0) {
|
|
4364
|
+
if (partial.soloed) trackEl.setAttribute("soloed", "");
|
|
4365
|
+
else trackEl.removeAttribute("soloed");
|
|
4366
|
+
}
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
const oldDesc = this._tracks.get(trackId);
|
|
4370
|
+
if (!oldDesc) return;
|
|
4371
|
+
const newDesc = {
|
|
4372
|
+
...oldDesc,
|
|
4373
|
+
...partial.name !== void 0 && { name: partial.name },
|
|
4374
|
+
...partial.volume !== void 0 && { volume: partial.volume },
|
|
4375
|
+
...partial.pan !== void 0 && { pan: partial.pan },
|
|
4376
|
+
...partial.muted !== void 0 && { muted: partial.muted },
|
|
4377
|
+
...partial.soloed !== void 0 && { soloed: partial.soloed }
|
|
4378
|
+
};
|
|
4379
|
+
this._tracks = new Map(this._tracks).set(trackId, newDesc);
|
|
4380
|
+
if (this._engine) {
|
|
4381
|
+
if (partial.volume !== void 0) this._engine.setTrackVolume(trackId, partial.volume);
|
|
4382
|
+
if (partial.pan !== void 0) this._engine.setTrackPan(trackId, partial.pan);
|
|
4383
|
+
if (partial.muted !== void 0) this._engine.setTrackMute(trackId, partial.muted);
|
|
4384
|
+
if (partial.soloed !== void 0) this._engine.setTrackSolo(trackId, partial.soloed);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
/**
|
|
4388
|
+
* Append a clip to an existing track. Builds a `<daw-clip>` from `config`
|
|
4389
|
+
* and appends it to the track's DOM element when one exists; resolves with
|
|
4390
|
+
* the new clip's id once the audio decode + peak generation finish.
|
|
4391
|
+
*/
|
|
4392
|
+
addClip(trackId, config) {
|
|
4393
|
+
if (!config.src) {
|
|
4394
|
+
return Promise.reject(
|
|
4395
|
+
new Error(
|
|
4396
|
+
"addClip: config.src is required \u2014 pass a URL to load. Empty/recording clips are not yet supported via addClip."
|
|
4397
|
+
)
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
4400
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4401
|
+
if (!trackEl) {
|
|
4402
|
+
return Promise.reject(
|
|
4403
|
+
new Error(
|
|
4404
|
+
'addClip: no <daw-track> element for trackId "' + trackId + '" \u2014 addClip currently requires a DOM-backed track. Use editor.addTrack(config) first.'
|
|
4405
|
+
)
|
|
4406
|
+
);
|
|
4407
|
+
}
|
|
4408
|
+
const clipEl = this._buildClipElement(config);
|
|
4409
|
+
return this._awaitId(
|
|
4410
|
+
"daw-clip-ready",
|
|
4411
|
+
"daw-clip-error",
|
|
4412
|
+
(d) => d.clipId === clipEl.clipId,
|
|
4413
|
+
clipEl.clipId,
|
|
4414
|
+
() => trackEl.appendChild(clipEl)
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Remove a clip by id. Removes the matching `<daw-clip>` DOM element when
|
|
4419
|
+
* present (MutationObserver handles cleanup); otherwise updates engine
|
|
4420
|
+
* state directly. No-op if no matching clip exists.
|
|
4421
|
+
*/
|
|
4422
|
+
removeClip(trackId, clipId) {
|
|
4423
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4424
|
+
if (trackEl) {
|
|
4425
|
+
const clipEl = [...trackEl.querySelectorAll("daw-clip")].find(
|
|
4426
|
+
(c) => c.clipId === clipId
|
|
4427
|
+
);
|
|
4428
|
+
if (clipEl) {
|
|
4429
|
+
clipEl.remove();
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
if (this._engineTracks.has(trackId)) {
|
|
4434
|
+
this._removeClipFromTrack(trackId, clipId);
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
console.warn(
|
|
4438
|
+
'[dawcore] removeClip: no track found for id "' + trackId + '" (clipId "' + clipId + '")'
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Update a clip's position (start/duration/offset) or properties (gain/name).
|
|
4443
|
+
* For DOM-element clips, writes properties on the `<daw-clip>` element which
|
|
4444
|
+
* fires `daw-clip-update`; otherwise applies directly via `_applyClipUpdate`.
|
|
4445
|
+
*
|
|
4446
|
+
* Re-decoding (changing `src`) is not supported via this method — remove and
|
|
4447
|
+
* re-add the clip instead.
|
|
4448
|
+
*
|
|
4449
|
+
* Note: `fadeIn` / `fadeOut` / `fadeType` on the partial are written to the
|
|
4450
|
+
* `<daw-clip>` element (so they round-trip in the descriptor), but engine-side
|
|
4451
|
+
* fade application from `<daw-clip>` properties is not yet implemented — see
|
|
4452
|
+
* the broader fade-engine integration tracked separately.
|
|
4453
|
+
*/
|
|
4454
|
+
updateClip(trackId, clipId, partial) {
|
|
4455
|
+
const trackEl = this._trackElements.get(trackId);
|
|
4456
|
+
if (trackEl) {
|
|
4457
|
+
const clipEl = [...trackEl.querySelectorAll("daw-clip")].find(
|
|
4458
|
+
(c) => c.clipId === clipId
|
|
4459
|
+
);
|
|
4460
|
+
if (clipEl) {
|
|
4461
|
+
if (partial.start !== void 0) clipEl.start = partial.start;
|
|
4462
|
+
if (partial.duration !== void 0) clipEl.duration = partial.duration;
|
|
4463
|
+
if (partial.offset !== void 0) clipEl.offset = partial.offset;
|
|
4464
|
+
if (partial.gain !== void 0) clipEl.gain = partial.gain;
|
|
4465
|
+
if (partial.name !== void 0) clipEl.setAttribute("name", partial.name);
|
|
4466
|
+
if (partial.fadeIn !== void 0) clipEl.fadeIn = partial.fadeIn;
|
|
4467
|
+
if (partial.fadeOut !== void 0) clipEl.fadeOut = partial.fadeOut;
|
|
4468
|
+
if (partial.fadeType !== void 0) clipEl.setAttribute("fade-type", partial.fadeType);
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
const t = this._engineTracks.get(trackId);
|
|
4473
|
+
if (!t) {
|
|
4474
|
+
console.warn('[dawcore] updateClip: no track found for id "' + trackId + '"');
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
const idx = t.clips.findIndex((c) => c.id === clipId);
|
|
4478
|
+
if (idx === -1) {
|
|
4479
|
+
console.warn(
|
|
4480
|
+
'[dawcore] updateClip: clip "' + clipId + '" not found in track "' + trackId + '"'
|
|
4481
|
+
);
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4484
|
+
const oldClip = t.clips[idx];
|
|
4485
|
+
const sr = oldClip.sampleRate ?? this.effectiveSampleRate;
|
|
4486
|
+
const updatedClip = {
|
|
4487
|
+
...oldClip,
|
|
4488
|
+
...partial.start !== void 0 && { startSample: Math.round(partial.start * sr) },
|
|
4489
|
+
...partial.duration !== void 0 && partial.duration > 0 && { durationSamples: Math.round(partial.duration * sr) },
|
|
4490
|
+
...partial.offset !== void 0 && { offsetSamples: Math.round(partial.offset * sr) },
|
|
4491
|
+
...partial.gain !== void 0 && { gain: partial.gain },
|
|
4492
|
+
...partial.name !== void 0 && { name: partial.name }
|
|
4493
|
+
};
|
|
4494
|
+
const updatedClips = [...t.clips];
|
|
4495
|
+
updatedClips[idx] = updatedClip;
|
|
4496
|
+
const updatedTrack = { ...t, clips: updatedClips };
|
|
4497
|
+
this._engineTracks = new Map(this._engineTracks).set(trackId, updatedTrack);
|
|
4498
|
+
this._commitTrackChange(trackId, updatedTrack);
|
|
4499
|
+
}
|
|
4500
|
+
_buildClipElement(config) {
|
|
4501
|
+
const clipEl = document.createElement("daw-clip");
|
|
4502
|
+
if (config.src !== void 0) clipEl.setAttribute("src", config.src);
|
|
4503
|
+
if (config.peaksSrc !== void 0) clipEl.setAttribute("peaks-src", config.peaksSrc);
|
|
4504
|
+
if (config.start !== void 0) clipEl.start = config.start;
|
|
4505
|
+
if (config.duration !== void 0) clipEl.duration = config.duration;
|
|
4506
|
+
if (config.offset !== void 0) clipEl.offset = config.offset;
|
|
4507
|
+
if (config.gain !== void 0) clipEl.gain = config.gain;
|
|
4508
|
+
if (config.name !== void 0) clipEl.setAttribute("name", config.name);
|
|
4509
|
+
if (config.fadeIn !== void 0) clipEl.fadeIn = config.fadeIn;
|
|
4510
|
+
if (config.fadeOut !== void 0) clipEl.fadeOut = config.fadeOut;
|
|
4511
|
+
if (config.fadeType !== void 0) clipEl.setAttribute("fade-type", config.fadeType);
|
|
4512
|
+
return clipEl;
|
|
4513
|
+
}
|
|
3906
4514
|
// --- Playback ---
|
|
3907
4515
|
async play(startTime) {
|
|
3908
4516
|
try {
|
|
@@ -4076,38 +4684,34 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
4076
4684
|
if (!playhead || !this._engine) return;
|
|
4077
4685
|
const engine = this._engine;
|
|
4078
4686
|
const ctx = this.audioContext;
|
|
4687
|
+
const audibleTime = () => {
|
|
4688
|
+
const outputLatency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4689
|
+
const t = engine.getCurrentTime() - outputLatency - engine.lookAhead;
|
|
4690
|
+
return Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
4691
|
+
};
|
|
4079
4692
|
if (this.scaleMode === "beats") {
|
|
4080
4693
|
const secondsToTicksFn = (s) => this._secondsToTicks(s);
|
|
4081
|
-
playhead.startBeatsAnimationWithMap(
|
|
4082
|
-
() => {
|
|
4083
|
-
const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4084
|
-
return Math.max(0, engine.getCurrentTime() - latency);
|
|
4085
|
-
},
|
|
4086
|
-
secondsToTicksFn,
|
|
4087
|
-
this.ticksPerPixel
|
|
4088
|
-
);
|
|
4694
|
+
playhead.startBeatsAnimationWithMap(audibleTime, secondsToTicksFn, this.ticksPerPixel);
|
|
4089
4695
|
} else {
|
|
4090
|
-
playhead.startAnimation(
|
|
4091
|
-
() => {
|
|
4092
|
-
const latency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4093
|
-
return Math.max(0, engine.getCurrentTime() - latency);
|
|
4094
|
-
},
|
|
4095
|
-
this.effectiveSampleRate,
|
|
4096
|
-
this.samplesPerPixel
|
|
4097
|
-
);
|
|
4696
|
+
playhead.startAnimation(audibleTime, this.effectiveSampleRate, this.samplesPerPixel);
|
|
4098
4697
|
}
|
|
4099
4698
|
}
|
|
4100
4699
|
_stopPlayhead() {
|
|
4101
4700
|
const playhead = this._getPlayhead();
|
|
4102
4701
|
if (!playhead) return;
|
|
4702
|
+
const ctx = this.audioContext;
|
|
4703
|
+
const outputLatency = "outputLatency" in ctx ? ctx.outputLatency : 0;
|
|
4704
|
+
const lookAhead = this._engine?.lookAhead ?? 0;
|
|
4705
|
+
const t = this._currentTime - outputLatency - lookAhead;
|
|
4706
|
+
const visualTime = Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
4103
4707
|
if (this.scaleMode === "beats") {
|
|
4104
4708
|
playhead.stopBeatsAnimationWithMap(
|
|
4105
|
-
|
|
4709
|
+
visualTime,
|
|
4106
4710
|
(s) => this._secondsToTicks(s),
|
|
4107
4711
|
this.ticksPerPixel
|
|
4108
4712
|
);
|
|
4109
4713
|
} else {
|
|
4110
|
-
playhead.stopAnimation(
|
|
4714
|
+
playhead.stopAnimation(visualTime, this.effectiveSampleRate, this.samplesPerPixel);
|
|
4111
4715
|
}
|
|
4112
4716
|
}
|
|
4113
4717
|
_getPlayhead() {
|
|
@@ -4155,7 +4759,7 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
4155
4759
|
};
|
|
4156
4760
|
});
|
|
4157
4761
|
return html8`
|
|
4158
|
-
${orderedTracks.length > 0 ? html8`<div class="controls-column">
|
|
4762
|
+
${orderedTracks.length > 0 || this.indefinitePlayback ? html8`<div class="controls-column">
|
|
4159
4763
|
${this.timescale ? html8`<div style="height: 30px;"></div>` : ""}
|
|
4160
4764
|
${orderedTracks.map(
|
|
4161
4765
|
(t) => html8`
|
|
@@ -4181,7 +4785,7 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
4181
4785
|
@dragleave=${this._onDragLeave}
|
|
4182
4786
|
@drop=${this._onDrop}
|
|
4183
4787
|
>
|
|
4184
|
-
${(orderedTracks.length > 0 || this.scaleMode === "beats") && this.timescale ? html8`<daw-ruler
|
|
4788
|
+
${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? html8`<daw-ruler
|
|
4185
4789
|
.samplesPerPixel=${spp}
|
|
4186
4790
|
.sampleRate=${this.effectiveSampleRate}
|
|
4187
4791
|
.duration=${this._duration}
|
|
@@ -4201,7 +4805,7 @@ var DawEditorElement = class extends LitElement9 {
|
|
|
4201
4805
|
.length=${this._totalWidth}
|
|
4202
4806
|
.height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
|
|
4203
4807
|
></daw-grid>` : ""}
|
|
4204
|
-
${orderedTracks.length > 0 || this.scaleMode === "beats" ? html8`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
|
|
4808
|
+
${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? html8`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
|
|
4205
4809
|
<daw-playhead></daw-playhead>` : ""}
|
|
4206
4810
|
${orderedTracks.map((t) => {
|
|
4207
4811
|
const channelHeight = this.waveHeight;
|
|
@@ -4398,6 +5002,9 @@ __decorateClass([
|
|
|
4398
5002
|
__decorateClass([
|
|
4399
5003
|
property7({ type: Boolean, attribute: "interactive-clips" })
|
|
4400
5004
|
], DawEditorElement.prototype, "interactiveClips", 2);
|
|
5005
|
+
__decorateClass([
|
|
5006
|
+
property7({ type: Boolean, attribute: "indefinite-playback" })
|
|
5007
|
+
], DawEditorElement.prototype, "indefinitePlayback", 2);
|
|
4401
5008
|
__decorateClass([
|
|
4402
5009
|
property7({ type: String, attribute: "scale-mode" })
|
|
4403
5010
|
], DawEditorElement.prototype, "scaleMode", 2);
|
|
@@ -4537,12 +5144,14 @@ var DawRulerElement = class extends LitElement10 {
|
|
|
4537
5144
|
ppqn: this.ppqn
|
|
4538
5145
|
});
|
|
4539
5146
|
this._tickData = null;
|
|
4540
|
-
} else if (this.duration > 0) {
|
|
5147
|
+
} else if (this.duration > 0 || this.totalWidth > 0) {
|
|
5148
|
+
const widthDerivedDuration = this.totalWidth * this.samplesPerPixel / this.sampleRate;
|
|
5149
|
+
const effectiveDuration = Math.max(this.duration, widthDerivedDuration);
|
|
4541
5150
|
this._musicalTickData = null;
|
|
4542
5151
|
this._tickData = computeTemporalTicks(
|
|
4543
5152
|
this.samplesPerPixel,
|
|
4544
5153
|
this.sampleRate,
|
|
4545
|
-
|
|
5154
|
+
effectiveDuration,
|
|
4546
5155
|
this.rulerHeight
|
|
4547
5156
|
);
|
|
4548
5157
|
} else {
|
|
@@ -4994,6 +5603,7 @@ export {
|
|
|
4994
5603
|
DawTransportElement,
|
|
4995
5604
|
DawWaveformElement,
|
|
4996
5605
|
RecordingController,
|
|
5606
|
+
isDomClip,
|
|
4997
5607
|
splitAtPlayhead
|
|
4998
5608
|
};
|
|
4999
5609
|
//# sourceMappingURL=index.mjs.map
|