@signalsandsorcery/plugin-sdk 2.28.1 → 2.35.1
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 +909 -73
- package/dist/index.d.ts +909 -73
- package/dist/index.js +3796 -792
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3775 -798
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AUDIO_EFFECTS: () => AUDIO_EFFECTS,
|
|
34
|
+
AUDIO_EFFECT_LABEL: () => AUDIO_EFFECT_LABEL,
|
|
33
35
|
ConfirmDialog: () => ConfirmDialog,
|
|
34
36
|
CrossfadeModal: () => CrossfadeModal,
|
|
35
37
|
CrossfadeTrackRow: () => CrossfadeTrackRow,
|
|
@@ -51,6 +53,7 @@ __export(index_exports, {
|
|
|
51
53
|
FadeTrackRow: () => FadeTrackRow,
|
|
52
54
|
FxToggleBar: () => FxToggleBar,
|
|
53
55
|
GUTTER_W: () => GUTTER_W,
|
|
56
|
+
GeneratorPanelShell: () => GeneratorPanelShell,
|
|
54
57
|
ImportTrackModal: () => ImportTrackModal,
|
|
55
58
|
InstrumentDrawer: () => TrackDrawer,
|
|
56
59
|
LevelMeter: () => LevelMeter,
|
|
@@ -68,44 +71,68 @@ __export(index_exports, {
|
|
|
68
71
|
ScrollingWaveform: () => ScrollingWaveform,
|
|
69
72
|
SorceryProgressBar: () => SorceryProgressBar,
|
|
70
73
|
TEXTURAL_ROLES: () => TEXTURAL_ROLES,
|
|
74
|
+
TRANSITION_DESIGNER_DRAFT_KEY: () => TRANSITION_DESIGNER_DRAFT_KEY,
|
|
71
75
|
TrackDrawer: () => TrackDrawer,
|
|
72
76
|
TrackMeterStrip: () => TrackMeterStrip,
|
|
73
77
|
TrackRow: () => TrackRow,
|
|
78
|
+
TransitionDesigner: () => TransitionDesigner,
|
|
74
79
|
VolumeSlider: () => VolumeSlider,
|
|
75
80
|
WaveformView: () => WaveformView,
|
|
76
81
|
analyzeWavPeak: () => analyzeWavPeak,
|
|
82
|
+
asAudioEffect: () => asAudioEffect,
|
|
77
83
|
asCrossfadeMeta: () => asCrossfadeMeta,
|
|
78
84
|
asFadeMeta: () => asFadeMeta,
|
|
85
|
+
asTransitionDesignerDraft: () => asTransitionDesignerDraft,
|
|
79
86
|
buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
|
|
80
87
|
buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
|
|
81
88
|
buildFadeVolumeCurve: () => buildFadeVolumeCurve,
|
|
89
|
+
buildRowSlots: () => buildRowSlots,
|
|
82
90
|
calculateTimeBasedTarget: () => calculateTimeBasedTarget,
|
|
83
91
|
cellToPx: () => cellToPx,
|
|
84
92
|
centerScrollTop: () => centerScrollTop,
|
|
85
93
|
computePeaks: () => computePeaks,
|
|
94
|
+
createSurgeSoundAdapter: () => createSurgeSoundAdapter,
|
|
95
|
+
dbIdsFromKeys: () => dbIdsFromKeys,
|
|
86
96
|
dbToSlider: () => dbToSlider,
|
|
87
97
|
defaultFadeGesture: () => defaultFadeGesture,
|
|
88
98
|
drawWaveform: () => drawWaveform,
|
|
89
99
|
formatConcurrentTracks: () => formatConcurrentTracks,
|
|
100
|
+
hashString: () => hashString,
|
|
90
101
|
moveItem: () => moveItem,
|
|
102
|
+
newTrackState: () => newTrackState,
|
|
103
|
+
normalizeSlots: () => normalizeSlots,
|
|
104
|
+
padPair: () => padPair,
|
|
105
|
+
padSlots: () => padSlots,
|
|
91
106
|
parseCrossfadePairs: () => parseCrossfadePairs,
|
|
92
107
|
parseFades: () => parseFades,
|
|
108
|
+
parseLLMNoteResponse: () => parseLLMNoteResponse,
|
|
109
|
+
parseTrackGroups: () => parseTrackGroups,
|
|
93
110
|
pickTopKWeighted: () => pickTopKWeighted,
|
|
94
111
|
pitchToName: () => pitchToName,
|
|
112
|
+
pluginFxToToggleFx: () => pluginFxToToggleFx,
|
|
95
113
|
pxToCell: () => pxToCell,
|
|
114
|
+
reconcileSlots: () => reconcileSlots,
|
|
96
115
|
resizeNoteDuration: () => resizeNoteDuration,
|
|
116
|
+
resolveTrackGroups: () => resolveTrackGroups,
|
|
117
|
+
rowKey: () => rowKey,
|
|
118
|
+
rowType: () => rowType,
|
|
97
119
|
scorePromptMatch: () => scorePromptMatch,
|
|
98
120
|
sliderToDb: () => sliderToDb,
|
|
121
|
+
slotsEqual: () => slotsEqual,
|
|
122
|
+
soundIdentity: () => soundIdentity,
|
|
99
123
|
synthesizeCuePoints: () => synthesizeCuePoints,
|
|
100
124
|
tokenizePrompt: () => tokenizePrompt,
|
|
125
|
+
trackDataKey: () => trackDataKey,
|
|
101
126
|
transposeNotes: () => transposeNotes,
|
|
102
127
|
useAnySolo: () => useAnySolo,
|
|
128
|
+
useGeneratorPanelCore: () => useGeneratorPanelCore,
|
|
103
129
|
useSceneState: () => useSceneState,
|
|
104
130
|
useSoundHistory: () => useSoundHistory,
|
|
105
131
|
useTrackLevel: () => useTrackLevel,
|
|
106
132
|
useTrackLevels: () => useTrackLevels,
|
|
107
133
|
useTrackMeter: () => useTrackMeter,
|
|
108
134
|
useTrackReorder: () => useTrackReorder,
|
|
135
|
+
useTransitionOps: () => useTransitionOps,
|
|
109
136
|
useTransportPlaying: () => useTransportPlaying
|
|
110
137
|
});
|
|
111
138
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1455,6 +1482,7 @@ var LevelMeter = ({
|
|
|
1455
1482
|
|
|
1456
1483
|
// src/hooks/useTrackLevels.ts
|
|
1457
1484
|
var import_react5 = require("react");
|
|
1485
|
+
var meterDiagRLast = /* @__PURE__ */ new Map();
|
|
1458
1486
|
var POLL_INTERVAL_MS = 33;
|
|
1459
1487
|
var HIDDEN_RECHECK_MS = 250;
|
|
1460
1488
|
var METER_FLOOR_DB = -120;
|
|
@@ -1586,6 +1614,11 @@ function useTrackMeter(handle, trackId) {
|
|
|
1586
1614
|
}
|
|
1587
1615
|
const update = () => {
|
|
1588
1616
|
const level = handle.getLevel(trackId);
|
|
1617
|
+
const dNow = Date.now();
|
|
1618
|
+
if ((meterDiagRLast.get(trackId) ?? 0) < dNow - 3e3) {
|
|
1619
|
+
meterDiagRLast.set(trackId, dNow);
|
|
1620
|
+
console.log(`[MeterDiagR] lookup trackId=${trackId} \u2192 ${level === null ? "MISS (no level for this id)" : "hit"}`);
|
|
1621
|
+
}
|
|
1589
1622
|
const now = performance.now();
|
|
1590
1623
|
const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
|
|
1591
1624
|
lastTickRef.current = now;
|
|
@@ -2287,8 +2320,7 @@ function TrackRow({
|
|
|
2287
2320
|
{
|
|
2288
2321
|
"data-testid": "sdk-mute-button",
|
|
2289
2322
|
onClick: onMuteToggle,
|
|
2290
|
-
|
|
2291
|
-
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2323
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2292
2324
|
title: isMuted ? "Unmute track" : "Mute track",
|
|
2293
2325
|
children: "M"
|
|
2294
2326
|
}
|
|
@@ -2539,6 +2571,18 @@ function CrossfadeTrackRow({
|
|
|
2539
2571
|
}
|
|
2540
2572
|
|
|
2541
2573
|
// src/crossfade-meta.ts
|
|
2574
|
+
function hashString(s) {
|
|
2575
|
+
let h = 5381;
|
|
2576
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i) | 0;
|
|
2577
|
+
return (h >>> 0).toString(36);
|
|
2578
|
+
}
|
|
2579
|
+
function soundIdentity(snap) {
|
|
2580
|
+
if (!snap) return "";
|
|
2581
|
+
if (snap.kind === "preset") return `p:${hashString(snap.state)}`;
|
|
2582
|
+
if (snap.kind === "sample") return `s:${snap.samplePath}`;
|
|
2583
|
+
if (snap.kind === "instrument") return `i:${snap.instrumentId ?? ""}:${hashString(JSON.stringify(snap.zones))}`;
|
|
2584
|
+
return "";
|
|
2585
|
+
}
|
|
2542
2586
|
var EQUAL_POWER_GAIN = 0.707;
|
|
2543
2587
|
function asCrossfadeMeta(val) {
|
|
2544
2588
|
if (!val || typeof val !== "object") return null;
|
|
@@ -2688,9 +2732,11 @@ function asFadeMeta(val) {
|
|
|
2688
2732
|
const m = val;
|
|
2689
2733
|
if (m.direction !== "in" && m.direction !== "out") return null;
|
|
2690
2734
|
if (m.gesture !== "volume" && m.gesture !== "build") return null;
|
|
2735
|
+
const effect = m.effect === "stutter" || m.effect === "chopped" || m.effect === "delay" || m.effect === "fade" ? m.effect : void 0;
|
|
2691
2736
|
return {
|
|
2692
2737
|
direction: m.direction,
|
|
2693
2738
|
gesture: m.gesture,
|
|
2739
|
+
effect,
|
|
2694
2740
|
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2695
2741
|
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2696
2742
|
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
@@ -2777,6 +2823,7 @@ function FadeTrackRow({
|
|
|
2777
2823
|
layer,
|
|
2778
2824
|
direction,
|
|
2779
2825
|
gesture,
|
|
2826
|
+
effect,
|
|
2780
2827
|
sliderPos = 0.5,
|
|
2781
2828
|
onMuteToggle,
|
|
2782
2829
|
onSoloToggle,
|
|
@@ -2790,7 +2837,8 @@ function FadeTrackRow({
|
|
|
2790
2837
|
const [confirmDelete, setConfirmDelete] = import_react11.default.useState(false);
|
|
2791
2838
|
const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
|
|
2792
2839
|
const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
|
|
2793
|
-
const
|
|
2840
|
+
const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
|
|
2841
|
+
const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
|
|
2794
2842
|
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2795
2843
|
"div",
|
|
2796
2844
|
{
|
|
@@ -3525,880 +3573,3809 @@ function CrossfadeModal({
|
|
|
3525
3573
|
) });
|
|
3526
3574
|
}
|
|
3527
3575
|
|
|
3528
|
-
// src/components/
|
|
3576
|
+
// src/components/TransitionDesigner.tsx
|
|
3577
|
+
var import_react16 = require("react");
|
|
3578
|
+
|
|
3579
|
+
// src/hooks/useTrackReorder.ts
|
|
3529
3580
|
var import_react15 = require("react");
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
if (
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
const
|
|
3536
|
-
|
|
3581
|
+
function moveItem(arr, from, to) {
|
|
3582
|
+
const next = arr.slice();
|
|
3583
|
+
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3584
|
+
return next;
|
|
3585
|
+
}
|
|
3586
|
+
const [moved] = next.splice(from, 1);
|
|
3587
|
+
next.splice(to, 0, moved);
|
|
3588
|
+
return next;
|
|
3537
3589
|
}
|
|
3538
|
-
|
|
3590
|
+
function useTrackReorder({
|
|
3539
3591
|
host,
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
const [
|
|
3547
|
-
const
|
|
3548
|
-
const
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3592
|
+
items,
|
|
3593
|
+
setItems,
|
|
3594
|
+
getId,
|
|
3595
|
+
onError
|
|
3596
|
+
}) {
|
|
3597
|
+
const [draggingIndex, setDraggingIndex] = (0, import_react15.useState)(null);
|
|
3598
|
+
const [dragOverIndex, setDragOverIndex] = (0, import_react15.useState)(null);
|
|
3599
|
+
const fromRef = (0, import_react15.useRef)(null);
|
|
3600
|
+
const itemsRef = (0, import_react15.useRef)(items);
|
|
3601
|
+
itemsRef.current = items;
|
|
3602
|
+
const dragPropsFor = (0, import_react15.useCallback)(
|
|
3603
|
+
(index) => ({
|
|
3604
|
+
handleProps: {
|
|
3605
|
+
draggable: true,
|
|
3606
|
+
onDragStart: (e) => {
|
|
3607
|
+
fromRef.current = index;
|
|
3608
|
+
setDraggingIndex(index);
|
|
3609
|
+
if (e.dataTransfer) {
|
|
3610
|
+
e.dataTransfer.effectAllowed = "move";
|
|
3611
|
+
try {
|
|
3612
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
3613
|
+
} catch {
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
},
|
|
3617
|
+
onDragEnd: () => {
|
|
3618
|
+
fromRef.current = null;
|
|
3619
|
+
setDraggingIndex(null);
|
|
3620
|
+
setDragOverIndex(null);
|
|
3621
|
+
}
|
|
3622
|
+
},
|
|
3623
|
+
rowProps: {
|
|
3624
|
+
onDragEnter: (e) => {
|
|
3625
|
+
if (fromRef.current === null) return;
|
|
3626
|
+
e.preventDefault();
|
|
3627
|
+
setDragOverIndex(index);
|
|
3628
|
+
},
|
|
3629
|
+
onDragOver: (e) => {
|
|
3630
|
+
if (fromRef.current === null) return;
|
|
3631
|
+
e.preventDefault();
|
|
3632
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3633
|
+
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3634
|
+
},
|
|
3635
|
+
onDragLeave: () => {
|
|
3636
|
+
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3637
|
+
},
|
|
3638
|
+
onDrop: (e) => {
|
|
3639
|
+
e.preventDefault();
|
|
3640
|
+
const from = fromRef.current;
|
|
3641
|
+
fromRef.current = null;
|
|
3642
|
+
setDraggingIndex(null);
|
|
3643
|
+
setDragOverIndex(null);
|
|
3644
|
+
if (from === null || from === index) return;
|
|
3645
|
+
const prev = itemsRef.current;
|
|
3646
|
+
const next = moveItem(prev, from, index);
|
|
3647
|
+
setItems(next);
|
|
3648
|
+
const ids = next.map(getId);
|
|
3649
|
+
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3650
|
+
setItems(prev);
|
|
3651
|
+
onError?.(err);
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
},
|
|
3655
|
+
isDragging: draggingIndex === index,
|
|
3656
|
+
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3657
|
+
}),
|
|
3658
|
+
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3659
|
+
);
|
|
3660
|
+
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// src/transition-designer-meta.ts
|
|
3664
|
+
var TRANSITION_DESIGNER_DRAFT_KEY = "transitionDesigner:draft";
|
|
3665
|
+
var AUDIO_EFFECTS = ["fade", "stutter", "chopped", "delay"];
|
|
3666
|
+
var AUDIO_EFFECT_LABEL = {
|
|
3667
|
+
fade: "Fade",
|
|
3668
|
+
stutter: "Stutter",
|
|
3669
|
+
chopped: "Chopped",
|
|
3670
|
+
delay: "Delay"
|
|
3671
|
+
};
|
|
3672
|
+
function asAudioEffect(v) {
|
|
3673
|
+
return v === "fade" || v === "stutter" || v === "chopped" || v === "delay" ? v : null;
|
|
3674
|
+
}
|
|
3675
|
+
function rowType(hasOrigin, hasTarget) {
|
|
3676
|
+
if (hasOrigin && hasTarget) return "crossfade";
|
|
3677
|
+
if (hasOrigin) return "fade-out";
|
|
3678
|
+
if (hasTarget) return "fade-in";
|
|
3679
|
+
return null;
|
|
3680
|
+
}
|
|
3681
|
+
function asTransitionDesignerDraft(val) {
|
|
3682
|
+
if (!val || typeof val !== "object") return null;
|
|
3683
|
+
const d = val;
|
|
3684
|
+
const clean = (a) => Array.isArray(a) ? a.filter((x) => x === null || typeof x === "string") : [];
|
|
3685
|
+
const cleanEffects = (e) => {
|
|
3686
|
+
const out = {};
|
|
3687
|
+
if (e && typeof e === "object") {
|
|
3688
|
+
for (const [k, v] of Object.entries(e)) {
|
|
3689
|
+
const eff = asAudioEffect(v);
|
|
3690
|
+
if (eff) out[k] = eff;
|
|
3574
3691
|
}
|
|
3575
|
-
} catch (err) {
|
|
3576
|
-
console.error("[DownloadPackButton] start failed:", err);
|
|
3577
|
-
setStatus("error");
|
|
3578
|
-
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
3579
3692
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3693
|
+
return out;
|
|
3694
|
+
};
|
|
3695
|
+
return {
|
|
3696
|
+
originOrder: clean(d.originOrder),
|
|
3697
|
+
targetOrder: clean(d.targetOrder),
|
|
3698
|
+
rowEffects: cleanEffects(d.rowEffects)
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
function reconcileSlots(saved, poolIds) {
|
|
3702
|
+
const pool = new Set(poolIds);
|
|
3703
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3704
|
+
const out = [];
|
|
3705
|
+
for (const slot of saved ?? []) {
|
|
3706
|
+
if (slot === null) {
|
|
3707
|
+
out.push(null);
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
if (pool.has(slot) && !seen.has(slot)) {
|
|
3711
|
+
out.push(slot);
|
|
3712
|
+
seen.add(slot);
|
|
3599
3713
|
}
|
|
3600
|
-
})();
|
|
3601
|
-
const tooltip = (() => {
|
|
3602
|
-
if (status === "error") return errorMessage || "Download failed. Click to retry.";
|
|
3603
|
-
if (isWorking) return `${buttonLabel} \u2014 ${displayName}`;
|
|
3604
|
-
if (status === "complete") return "Installation complete";
|
|
3605
|
-
return `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ""}`;
|
|
3606
|
-
})();
|
|
3607
|
-
const baseClasses = variant === "large" ? "px-4 py-2 text-sm font-medium rounded border transition-colors" : "px-2 py-0.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors";
|
|
3608
|
-
let className;
|
|
3609
|
-
if (status === "error") {
|
|
3610
|
-
className = `${baseClasses} text-red-400 border-red-400/50 hover:text-red-300 hover:border-red-300`;
|
|
3611
|
-
} else if (status === "complete") {
|
|
3612
|
-
className = `${baseClasses} text-green-400 border-green-400/50`;
|
|
3613
|
-
} else if (isDisabled) {
|
|
3614
|
-
className = `${baseClasses} text-sas-accent border-sas-accent/50 cursor-wait`;
|
|
3615
|
-
} else {
|
|
3616
|
-
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3617
|
-
}
|
|
3618
|
-
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
|
|
3619
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3620
|
-
"button",
|
|
3621
|
-
{
|
|
3622
|
-
"data-testid": `download-pack-button-${packId}`,
|
|
3623
|
-
onClick: handleClick,
|
|
3624
|
-
disabled: isDisabled,
|
|
3625
|
-
className,
|
|
3626
|
-
title: tooltip,
|
|
3627
|
-
children: buttonLabel
|
|
3628
|
-
}
|
|
3629
|
-
),
|
|
3630
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3631
|
-
] });
|
|
3632
|
-
};
|
|
3633
|
-
|
|
3634
|
-
// src/components/SamplePackCTACard.tsx
|
|
3635
|
-
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3636
|
-
var SamplePackCTACard = ({
|
|
3637
|
-
host,
|
|
3638
|
-
pack,
|
|
3639
|
-
status,
|
|
3640
|
-
onDownloadComplete
|
|
3641
|
-
}) => {
|
|
3642
|
-
if (status === "checking") {
|
|
3643
|
-
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3644
|
-
"div",
|
|
3645
|
-
{
|
|
3646
|
-
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
3647
|
-
className: "flex items-center justify-center py-16 text-sas-muted text-sm",
|
|
3648
|
-
children: "Checking sample library..."
|
|
3649
|
-
}
|
|
3650
|
-
);
|
|
3651
3714
|
}
|
|
3652
|
-
const
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
{
|
|
3657
|
-
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3658
|
-
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3659
|
-
children: [
|
|
3660
|
-
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
3661
|
-
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
3662
|
-
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
3663
|
-
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3664
|
-
DownloadPackButton,
|
|
3665
|
-
{
|
|
3666
|
-
host,
|
|
3667
|
-
packId: pack.packId,
|
|
3668
|
-
displayName: pack.displayName,
|
|
3669
|
-
sizeBytes: pack.sizeBytes,
|
|
3670
|
-
variant: "large",
|
|
3671
|
-
onDownloadComplete
|
|
3672
|
-
}
|
|
3673
|
-
)
|
|
3674
|
-
]
|
|
3715
|
+
for (const id of poolIds) {
|
|
3716
|
+
if (!seen.has(id)) {
|
|
3717
|
+
out.push(id);
|
|
3718
|
+
seen.add(id);
|
|
3675
3719
|
}
|
|
3720
|
+
}
|
|
3721
|
+
return out;
|
|
3722
|
+
}
|
|
3723
|
+
function buildRowSlots(originSlots, targetSlots) {
|
|
3724
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3725
|
+
const rows = [];
|
|
3726
|
+
for (let i = 0; i < n; i++) {
|
|
3727
|
+
const originId = originSlots[i] ?? null;
|
|
3728
|
+
const targetId = targetSlots[i] ?? null;
|
|
3729
|
+
rows.push({ originId, targetId, type: rowType(originId !== null, targetId !== null) });
|
|
3730
|
+
}
|
|
3731
|
+
return rows;
|
|
3732
|
+
}
|
|
3733
|
+
function normalizeSlots(originSlots, targetSlots) {
|
|
3734
|
+
const rows = buildRowSlots(originSlots, targetSlots).filter(
|
|
3735
|
+
(r) => r.originId !== null || r.targetId !== null
|
|
3676
3736
|
);
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3737
|
+
const trimTrailing = (a) => {
|
|
3738
|
+
let end = a.length;
|
|
3739
|
+
while (end > 0 && a[end - 1] === null) end--;
|
|
3740
|
+
return a.slice(0, end);
|
|
3741
|
+
};
|
|
3742
|
+
return {
|
|
3743
|
+
originOrder: trimTrailing(rows.map((r) => r.originId)),
|
|
3744
|
+
targetOrder: trimTrailing(rows.map((r) => r.targetId))
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
function padSlots(slots, n) {
|
|
3748
|
+
if (slots.length >= n) return slots.slice();
|
|
3749
|
+
return [...slots, ...new Array(n - slots.length).fill(null)];
|
|
3750
|
+
}
|
|
3751
|
+
function padPair(originSlots, targetSlots) {
|
|
3752
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3753
|
+
return [padSlots(originSlots, n), padSlots(targetSlots, n)];
|
|
3754
|
+
}
|
|
3755
|
+
function slotsEqual(a, b) {
|
|
3756
|
+
if (a.length !== b.length) return false;
|
|
3757
|
+
for (let i = 0; i < a.length; i++) {
|
|
3758
|
+
if (a[i] !== b[i]) return false;
|
|
3688
3759
|
}
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3760
|
+
return true;
|
|
3761
|
+
}
|
|
3762
|
+
function rowKey(row) {
|
|
3763
|
+
if (row.type === "crossfade") return `xf:${row.originId}|${row.targetId}`;
|
|
3764
|
+
if (row.type === "fade-out") return `fo:${row.originId}`;
|
|
3765
|
+
if (row.type === "fade-in") return `fi:${row.targetId}`;
|
|
3766
|
+
return null;
|
|
3767
|
+
}
|
|
3768
|
+
function dbIdsFromKeys(keys) {
|
|
3769
|
+
const out = /* @__PURE__ */ new Set();
|
|
3770
|
+
for (const k of keys) {
|
|
3771
|
+
const body = k.slice(3);
|
|
3772
|
+
if (k.startsWith("xf:")) {
|
|
3773
|
+
const sep = body.indexOf("|");
|
|
3774
|
+
out.add(body.slice(0, sep));
|
|
3775
|
+
out.add(body.slice(sep + 1));
|
|
3776
|
+
} else {
|
|
3777
|
+
out.add(body);
|
|
3699
3778
|
}
|
|
3700
|
-
let mn = Infinity;
|
|
3701
|
-
let mx = -Infinity;
|
|
3702
|
-
for (let j = startIdx; j < endIdx; j++) {
|
|
3703
|
-
let v = 0;
|
|
3704
|
-
for (let c = 0; c < numberOfChannels; c++) {
|
|
3705
|
-
v += channels[c][j];
|
|
3706
|
-
}
|
|
3707
|
-
v /= numberOfChannels;
|
|
3708
|
-
if (v < mn) mn = v;
|
|
3709
|
-
if (v > mx) mx = v;
|
|
3710
|
-
}
|
|
3711
|
-
if (!Number.isFinite(mn)) mn = 0;
|
|
3712
|
-
if (!Number.isFinite(mx)) mx = 0;
|
|
3713
|
-
out[i * 2] = mn;
|
|
3714
|
-
out[i * 2 + 1] = mx;
|
|
3715
|
-
}
|
|
3716
|
-
return { sampleRate, totalSamples: totalForBinning, peaks: out };
|
|
3717
|
-
}
|
|
3718
|
-
function drawWaveform(canvas, peaks, options = {}) {
|
|
3719
|
-
const dpr = window.devicePixelRatio || 1;
|
|
3720
|
-
const cssWidth = canvas.clientWidth;
|
|
3721
|
-
const cssHeight = canvas.clientHeight;
|
|
3722
|
-
if (cssWidth === 0 || cssHeight === 0) return;
|
|
3723
|
-
canvas.width = Math.floor(cssWidth * dpr);
|
|
3724
|
-
canvas.height = Math.floor(cssHeight * dpr);
|
|
3725
|
-
const ctx = canvas.getContext("2d");
|
|
3726
|
-
if (!ctx) return;
|
|
3727
|
-
ctx.scale(dpr, dpr);
|
|
3728
|
-
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
3729
|
-
ctx.fillStyle = options.fillStyle ?? "rgba(255, 255, 255, 0.4)";
|
|
3730
|
-
const bins = peaks.peaks.length / 2;
|
|
3731
|
-
const mid = cssHeight / 2;
|
|
3732
|
-
for (let x = 0; x < cssWidth; x++) {
|
|
3733
|
-
const binIdx = Math.floor(x / cssWidth * bins);
|
|
3734
|
-
const mn = peaks.peaks[binIdx * 2];
|
|
3735
|
-
const mx = peaks.peaks[binIdx * 2 + 1];
|
|
3736
|
-
const yTop = mid - mx * mid;
|
|
3737
|
-
const yBot = mid - mn * mid;
|
|
3738
|
-
ctx.fillRect(x, yTop, 1, Math.max(1, yBot - yTop));
|
|
3739
3779
|
}
|
|
3780
|
+
return out;
|
|
3740
3781
|
}
|
|
3741
3782
|
|
|
3742
|
-
// src/components/
|
|
3743
|
-
var
|
|
3744
|
-
var
|
|
3783
|
+
// src/components/TransitionDesigner.tsx
|
|
3784
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3785
|
+
var CROSSFADE_ESTIMATE_MS = 15e3;
|
|
3786
|
+
var FADE_ESTIMATE_MS = 11e3;
|
|
3787
|
+
var CREATE_ALL_CONCURRENCY = 5;
|
|
3788
|
+
var TYPE_LABEL = {
|
|
3789
|
+
crossfade: "Crossfade",
|
|
3790
|
+
"fade-out": "Fade out",
|
|
3791
|
+
"fade-in": "Fade in"
|
|
3792
|
+
};
|
|
3793
|
+
function shortId3(dbId) {
|
|
3794
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3795
|
+
}
|
|
3796
|
+
function TransitionDesigner({
|
|
3745
3797
|
host,
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3798
|
+
fromSceneId,
|
|
3799
|
+
toSceneId,
|
|
3800
|
+
transitionSceneId,
|
|
3801
|
+
excludeSourceDbIds,
|
|
3802
|
+
onCreateCrossfade,
|
|
3803
|
+
onCreateFade,
|
|
3804
|
+
onCreateAudioTransition,
|
|
3805
|
+
familyLabel,
|
|
3806
|
+
testIdPrefix = "transition-designer"
|
|
3807
|
+
}) {
|
|
3808
|
+
const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
|
|
3809
|
+
const [fromName, setFromName] = (0, import_react16.useState)(null);
|
|
3810
|
+
const [toName, setToName] = (0, import_react16.useState)(null);
|
|
3811
|
+
const [originSlots, setOriginSlots] = (0, import_react16.useState)([]);
|
|
3812
|
+
const [targetSlots, setTargetSlots] = (0, import_react16.useState)([]);
|
|
3813
|
+
const [creatingKeys, setCreatingKeys] = (0, import_react16.useState)(() => /* @__PURE__ */ new Set());
|
|
3814
|
+
const [rowErrors, setRowErrors] = (0, import_react16.useState)({});
|
|
3815
|
+
const [rowEffects, setRowEffects] = (0, import_react16.useState)({});
|
|
3816
|
+
const rowEffectsRef = (0, import_react16.useRef)(rowEffects);
|
|
3817
|
+
rowEffectsRef.current = rowEffects;
|
|
3818
|
+
const audioEffectsEnabled = !!onCreateAudioTransition;
|
|
3819
|
+
const excludeRef = (0, import_react16.useRef)(excludeSourceDbIds);
|
|
3820
|
+
excludeRef.current = excludeSourceDbIds;
|
|
3821
|
+
const originSlotsRef = (0, import_react16.useRef)(originSlots);
|
|
3822
|
+
originSlotsRef.current = originSlots;
|
|
3823
|
+
const targetSlotsRef = (0, import_react16.useRef)(targetSlots);
|
|
3824
|
+
targetSlotsRef.current = targetSlots;
|
|
3825
|
+
const creatingKeysRef = (0, import_react16.useRef)(creatingKeys);
|
|
3826
|
+
creatingKeysRef.current = creatingKeys;
|
|
3827
|
+
const dragRef = (0, import_react16.useRef)(null);
|
|
3828
|
+
const [dragging, setDragging] = (0, import_react16.useState)(null);
|
|
3829
|
+
const [dragOver, setDragOver] = (0, import_react16.useState)(null);
|
|
3830
|
+
const excludeSet = (0, import_react16.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3831
|
+
const originPool = (0, import_react16.useMemo)(
|
|
3832
|
+
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3833
|
+
[load, excludeSet]
|
|
3834
|
+
);
|
|
3835
|
+
const targetPool = (0, import_react16.useMemo)(
|
|
3836
|
+
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3837
|
+
[load, excludeSet]
|
|
3838
|
+
);
|
|
3839
|
+
const originById = (0, import_react16.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
|
|
3840
|
+
const targetById = (0, import_react16.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
|
|
3841
|
+
const originByIdRef = (0, import_react16.useRef)(originById);
|
|
3842
|
+
originByIdRef.current = originById;
|
|
3843
|
+
const targetByIdRef = (0, import_react16.useRef)(targetById);
|
|
3844
|
+
targetByIdRef.current = targetById;
|
|
3845
|
+
const refresh = (0, import_react16.useCallback)(async () => {
|
|
3846
|
+
if (!host.listSceneFamilyTracks) {
|
|
3847
|
+
setLoad({ status: "error", message: "This host does not support transition tracks." });
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
setLoad({ status: "loading" });
|
|
3851
|
+
try {
|
|
3852
|
+
const [origin, target, fName, tName, draftRaw] = await Promise.all([
|
|
3853
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
3854
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
3855
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3856
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
|
|
3857
|
+
host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
|
|
3858
|
+
]);
|
|
3859
|
+
const draft = asTransitionDesignerDraft(draftRaw);
|
|
3860
|
+
const exSet = new Set(excludeRef.current ?? []);
|
|
3861
|
+
const originIds = origin.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3862
|
+
const targetIds = target.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3863
|
+
const [po, pt] = padPair(
|
|
3864
|
+
reconcileSlots(draft?.originOrder, originIds),
|
|
3865
|
+
reconcileSlots(draft?.targetOrder, targetIds)
|
|
3866
|
+
);
|
|
3867
|
+
setOriginSlots(po);
|
|
3868
|
+
setTargetSlots(pt);
|
|
3869
|
+
setRowEffects(draft?.rowEffects ?? {});
|
|
3870
|
+
setFromName(fName);
|
|
3871
|
+
setToName(tName);
|
|
3872
|
+
setLoad({ status: "ready", origin, target });
|
|
3873
|
+
} catch (err) {
|
|
3874
|
+
setLoad({
|
|
3875
|
+
status: "error",
|
|
3876
|
+
message: err instanceof Error ? err.message : "Failed to load tracks."
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
}, [host, fromSceneId, toSceneId, transitionSceneId]);
|
|
3754
3880
|
(0, import_react16.useEffect)(() => {
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3881
|
+
void refresh();
|
|
3882
|
+
}, [refresh]);
|
|
3883
|
+
(0, import_react16.useEffect)(() => {
|
|
3884
|
+
if (load.status !== "ready") return;
|
|
3885
|
+
const [po, pt] = padPair(
|
|
3886
|
+
reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
|
|
3887
|
+
reconcileSlots(targetSlotsRef.current, targetPool.map((t) => t.dbId))
|
|
3888
|
+
);
|
|
3889
|
+
if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
|
|
3890
|
+
if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
|
|
3891
|
+
}, [originPool, targetPool, load.status]);
|
|
3892
|
+
const mutate = (0, import_react16.useCallback)(
|
|
3893
|
+
(nextOrigin, nextTarget) => {
|
|
3894
|
+
const norm = normalizeSlots(nextOrigin, nextTarget);
|
|
3895
|
+
const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
|
|
3896
|
+
setOriginSlots(po);
|
|
3897
|
+
setTargetSlots(pt);
|
|
3898
|
+
if (host.setSceneData) {
|
|
3899
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: rowEffectsRef.current }).catch(() => {
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
},
|
|
3903
|
+
[host, transitionSceneId]
|
|
3904
|
+
);
|
|
3905
|
+
const setRowEffect = (0, import_react16.useCallback)(
|
|
3906
|
+
(sourceDbId, effect) => {
|
|
3907
|
+
setRowEffects((prev) => {
|
|
3908
|
+
const next = { ...prev, [sourceDbId]: effect };
|
|
3909
|
+
if (host.setSceneData) {
|
|
3910
|
+
const norm = normalizeSlots(originSlotsRef.current, targetSlotsRef.current);
|
|
3911
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: next }).catch(() => {
|
|
3772
3912
|
});
|
|
3773
3913
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
};
|
|
3779
|
-
}, [host, filePath, bins, targetSamples]);
|
|
3780
|
-
(0, import_react16.useEffect)(() => {
|
|
3781
|
-
if (!peaks) return;
|
|
3782
|
-
const canvas = canvasRef.current;
|
|
3783
|
-
if (!canvas) return;
|
|
3784
|
-
drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : void 0);
|
|
3785
|
-
const observer = new ResizeObserver(() => {
|
|
3786
|
-
drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : void 0);
|
|
3787
|
-
});
|
|
3788
|
-
observer.observe(canvas);
|
|
3789
|
-
return () => observer.disconnect();
|
|
3790
|
-
}, [peaks, fillStyle]);
|
|
3791
|
-
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3792
|
-
"canvas",
|
|
3793
|
-
{
|
|
3794
|
-
ref: canvasRef,
|
|
3795
|
-
"data-testid": "waveform-view",
|
|
3796
|
-
className: className ?? "w-full h-10"
|
|
3797
|
-
}
|
|
3914
|
+
return next;
|
|
3915
|
+
});
|
|
3916
|
+
},
|
|
3917
|
+
[host, transitionSceneId]
|
|
3798
3918
|
);
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
const
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
const
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3919
|
+
const insertGapAbove = (0, import_react16.useCallback)(
|
|
3920
|
+
(col, index) => {
|
|
3921
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3922
|
+
const next = [...slots.slice(0, index), null, ...slots.slice(index)];
|
|
3923
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3924
|
+
else mutate(originSlots, next);
|
|
3925
|
+
},
|
|
3926
|
+
[originSlots, targetSlots, mutate]
|
|
3927
|
+
);
|
|
3928
|
+
const removeGap = (0, import_react16.useCallback)(
|
|
3929
|
+
(col, index) => {
|
|
3930
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3931
|
+
const next = slots.filter((_, i) => i !== index);
|
|
3932
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3933
|
+
else mutate(originSlots, next);
|
|
3934
|
+
},
|
|
3935
|
+
[originSlots, targetSlots, mutate]
|
|
3936
|
+
);
|
|
3937
|
+
const handleDrop = (0, import_react16.useCallback)(
|
|
3938
|
+
(col, to) => {
|
|
3939
|
+
const from = dragRef.current;
|
|
3940
|
+
dragRef.current = null;
|
|
3941
|
+
setDragging(null);
|
|
3942
|
+
setDragOver(null);
|
|
3943
|
+
if (!from || from.col !== col || from.index === to) return;
|
|
3944
|
+
if (col === "origin") mutate(moveItem(originSlots, from.index, to), targetSlots);
|
|
3945
|
+
else mutate(originSlots, moveItem(targetSlots, from.index, to));
|
|
3946
|
+
},
|
|
3947
|
+
[originSlots, targetSlots, mutate]
|
|
3948
|
+
);
|
|
3949
|
+
const rows = (0, import_react16.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
|
|
3950
|
+
const creatingDbIds = (0, import_react16.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
|
|
3951
|
+
const eligibleCount = (0, import_react16.useMemo)(
|
|
3952
|
+
() => rows.filter((r) => {
|
|
3953
|
+
const k = rowKey(r);
|
|
3954
|
+
return k !== null && !creatingKeys.has(k);
|
|
3955
|
+
}).length,
|
|
3956
|
+
[rows, creatingKeys]
|
|
3957
|
+
);
|
|
3958
|
+
const createRow = (0, import_react16.useCallback)(
|
|
3959
|
+
async (row) => {
|
|
3960
|
+
const key = rowKey(row);
|
|
3961
|
+
if (!key || !row.type || creatingKeysRef.current.has(key)) return;
|
|
3962
|
+
setCreatingKeys((prev) => new Set(prev).add(key));
|
|
3963
|
+
setRowErrors((prev) => {
|
|
3964
|
+
if (!(key in prev)) return prev;
|
|
3965
|
+
const next = { ...prev };
|
|
3966
|
+
delete next[key];
|
|
3967
|
+
return next;
|
|
3968
|
+
});
|
|
3969
|
+
try {
|
|
3970
|
+
if (row.type === "crossfade") {
|
|
3971
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3972
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3973
|
+
if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3974
|
+
await onCreateCrossfade(
|
|
3975
|
+
{ dbId: o.dbId, name: o.name, role: o.role },
|
|
3976
|
+
{ dbId: t.dbId, name: t.name, role: t.role }
|
|
3977
|
+
);
|
|
3978
|
+
} else if (row.type === "fade-out") {
|
|
3979
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3980
|
+
if (!o) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3981
|
+
const eff = rowEffectsRef.current[o.dbId] ?? "fade";
|
|
3982
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3983
|
+
await onCreateAudioTransition({ dbId: o.dbId, name: o.name, role: o.role }, "out", eff);
|
|
3984
|
+
} else {
|
|
3985
|
+
await onCreateFade({ dbId: o.dbId, name: o.name, role: o.role }, "out", defaultFadeGesture(o.role));
|
|
3850
3986
|
}
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
const start = writeIdxRef.current;
|
|
3860
|
-
for (let x = 0; x < cols; x++) {
|
|
3861
|
-
const ringIdx = (start + x) % cols;
|
|
3862
|
-
const a = ring[ringIdx];
|
|
3863
|
-
const half = a * mid;
|
|
3864
|
-
ctx.fillRect(x * colW, mid - half, Math.max(1, colW), Math.max(1, half * 2));
|
|
3865
|
-
}
|
|
3987
|
+
} else {
|
|
3988
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3989
|
+
if (!t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3990
|
+
const eff = rowEffectsRef.current[t.dbId] ?? "fade";
|
|
3991
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3992
|
+
await onCreateAudioTransition({ dbId: t.dbId, name: t.name, role: t.role }, "in", eff);
|
|
3993
|
+
} else {
|
|
3994
|
+
await onCreateFade({ dbId: t.dbId, name: t.name, role: t.role }, "in", defaultFadeGesture(t.role));
|
|
3866
3995
|
}
|
|
3867
3996
|
}
|
|
3997
|
+
} catch (err) {
|
|
3998
|
+
setRowErrors((prev) => ({
|
|
3999
|
+
...prev,
|
|
4000
|
+
[key]: err instanceof Error ? err.message : "Failed to create transition."
|
|
4001
|
+
}));
|
|
4002
|
+
} finally {
|
|
4003
|
+
setCreatingKeys((prev) => {
|
|
4004
|
+
const next = new Set(prev);
|
|
4005
|
+
next.delete(key);
|
|
4006
|
+
return next;
|
|
4007
|
+
});
|
|
3868
4008
|
}
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
4009
|
+
},
|
|
4010
|
+
[onCreateCrossfade, onCreateFade, onCreateAudioTransition]
|
|
4011
|
+
);
|
|
4012
|
+
const createAll = (0, import_react16.useCallback)(async () => {
|
|
4013
|
+
const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
|
|
4014
|
+
const k = rowKey(r);
|
|
4015
|
+
return k !== null && !creatingKeysRef.current.has(k);
|
|
4016
|
+
});
|
|
4017
|
+
if (eligible.length === 0) return;
|
|
4018
|
+
let cursor = 0;
|
|
4019
|
+
const worker = async () => {
|
|
4020
|
+
while (cursor < eligible.length) {
|
|
4021
|
+
const row = eligible[cursor];
|
|
4022
|
+
cursor += 1;
|
|
4023
|
+
await createRow(row);
|
|
3876
4024
|
}
|
|
3877
4025
|
};
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
cuePoints,
|
|
3898
|
-
offsetSamples,
|
|
3899
|
-
projectBpm,
|
|
3900
|
-
meter = 4,
|
|
3901
|
-
onChange,
|
|
3902
|
-
disabled = false
|
|
3903
|
-
}) {
|
|
3904
|
-
const trackRef = (0, import_react18.useRef)(null);
|
|
3905
|
-
const [draftOffset, setDraftOffset] = (0, import_react18.useState)(offsetSamples);
|
|
3906
|
-
const [isDragging, setIsDragging] = (0, import_react18.useState)(false);
|
|
3907
|
-
(0, import_react18.useEffect)(() => {
|
|
3908
|
-
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3909
|
-
}, [offsetSamples, isDragging]);
|
|
3910
|
-
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3911
|
-
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3912
|
-
const beatsForRange = (0, import_react18.useMemo)(() => {
|
|
3913
|
-
return Math.round(60 / projectBpm * sampleRate);
|
|
3914
|
-
}, [projectBpm, sampleRate]);
|
|
3915
|
-
const rangeSamples = beatsForRange * meter;
|
|
3916
|
-
const sampleToFraction = (0, import_react18.useCallback)(
|
|
3917
|
-
(sample) => {
|
|
3918
|
-
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3919
|
-
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
4026
|
+
await Promise.all(
|
|
4027
|
+
Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
|
|
4028
|
+
);
|
|
4029
|
+
}, [createRow]);
|
|
4030
|
+
const fromLabel = fromName ?? "origin";
|
|
4031
|
+
const toLabel = toName ?? "target";
|
|
4032
|
+
const cellDragProps = (col, index, locked) => ({
|
|
4033
|
+
draggable: !locked,
|
|
4034
|
+
onDragStart: (e) => {
|
|
4035
|
+
if (locked) return;
|
|
4036
|
+
dragRef.current = { col, index };
|
|
4037
|
+
setDragging({ col, index });
|
|
4038
|
+
if (e.dataTransfer) {
|
|
4039
|
+
e.dataTransfer.effectAllowed = "move";
|
|
4040
|
+
try {
|
|
4041
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
4042
|
+
} catch {
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
3920
4045
|
},
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3926
|
-
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
4046
|
+
onDragEnd: () => {
|
|
4047
|
+
dragRef.current = null;
|
|
4048
|
+
setDragging(null);
|
|
4049
|
+
setDragOver(null);
|
|
3927
4050
|
},
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3934
|
-
const negatives = positives.slice(1).map((p) => -p);
|
|
3935
|
-
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3936
|
-
}, [cuePoints]);
|
|
3937
|
-
const snapToBeat = (0, import_react18.useCallback)(
|
|
3938
|
-
(sample) => {
|
|
3939
|
-
if (snapTargets.length === 0) return sample;
|
|
3940
|
-
let best = snapTargets[0];
|
|
3941
|
-
let bestDist = Math.abs(sample - best);
|
|
3942
|
-
for (const t of snapTargets) {
|
|
3943
|
-
const d = Math.abs(sample - t);
|
|
3944
|
-
if (d < bestDist) {
|
|
3945
|
-
best = t;
|
|
3946
|
-
bestDist = d;
|
|
3947
|
-
}
|
|
3948
|
-
}
|
|
3949
|
-
return best;
|
|
4051
|
+
onDragEnter: (e) => {
|
|
4052
|
+
const d = dragRef.current;
|
|
4053
|
+
if (!d || d.col !== col) return;
|
|
4054
|
+
e.preventDefault();
|
|
4055
|
+
setDragOver({ col, index });
|
|
3950
4056
|
},
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
(e) => {
|
|
3955
|
-
if (disabled || !cuePoints) return;
|
|
4057
|
+
onDragOver: (e) => {
|
|
4058
|
+
const d = dragRef.current;
|
|
4059
|
+
if (!d || d.col !== col) return;
|
|
3956
4060
|
e.preventDefault();
|
|
3957
|
-
|
|
3958
|
-
if (!track) return;
|
|
3959
|
-
track.setPointerCapture(e.pointerId);
|
|
3960
|
-
setIsDragging(true);
|
|
3961
|
-
const updateFromEvent = (clientX, shiftHeld) => {
|
|
3962
|
-
const rect = track.getBoundingClientRect();
|
|
3963
|
-
const fraction = (clientX - rect.left) / rect.width;
|
|
3964
|
-
const raw = fractionToSample(fraction);
|
|
3965
|
-
return shiftHeld ? raw : snapToBeat(raw);
|
|
3966
|
-
};
|
|
3967
|
-
setDraftOffset(updateFromEvent(e.clientX, e.shiftKey));
|
|
3968
|
-
const onMove = (ev) => {
|
|
3969
|
-
setDraftOffset(updateFromEvent(ev.clientX, ev.shiftKey));
|
|
3970
|
-
};
|
|
3971
|
-
const onUp = (ev) => {
|
|
3972
|
-
const final = updateFromEvent(ev.clientX, ev.shiftKey);
|
|
3973
|
-
track.releasePointerCapture(e.pointerId);
|
|
3974
|
-
track.removeEventListener("pointermove", onMove);
|
|
3975
|
-
track.removeEventListener("pointerup", onUp);
|
|
3976
|
-
track.removeEventListener("pointercancel", onUp);
|
|
3977
|
-
setIsDragging(false);
|
|
3978
|
-
setDraftOffset(final);
|
|
3979
|
-
onChange(final);
|
|
3980
|
-
};
|
|
3981
|
-
track.addEventListener("pointermove", onMove);
|
|
3982
|
-
track.addEventListener("pointerup", onUp);
|
|
3983
|
-
track.addEventListener("pointercancel", onUp);
|
|
4061
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3984
4062
|
},
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
const
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
return
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4063
|
+
onDragLeave: () => {
|
|
4064
|
+
setDragOver((cur) => cur && cur.col === col && cur.index === index ? null : cur);
|
|
4065
|
+
},
|
|
4066
|
+
onDrop: (e) => {
|
|
4067
|
+
e.preventDefault();
|
|
4068
|
+
handleDrop(col, index);
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
const renderCell = (col, index, slotId) => {
|
|
4072
|
+
const byId = col === "origin" ? originById : targetById;
|
|
4073
|
+
const track = slotId ? byId.get(slotId) : void 0;
|
|
4074
|
+
const locked = slotId !== null && creatingDbIds.has(slotId);
|
|
4075
|
+
const isDragging = dragging?.col === col && dragging.index === index;
|
|
4076
|
+
const isDragTarget = dragOver?.col === col && dragOver.index === index && !isDragging;
|
|
4077
|
+
const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
|
|
4078
|
+
const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
|
|
4079
|
+
if (slotId === null) {
|
|
4080
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4081
|
+
"div",
|
|
4082
|
+
{
|
|
4083
|
+
...cellDragProps(col, index, false),
|
|
4084
|
+
"data-testid": `${testIdPrefix}-${col}-gap-${index}`,
|
|
4085
|
+
className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
|
|
4086
|
+
children: [
|
|
4087
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
|
|
4088
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4089
|
+
"button",
|
|
4090
|
+
{
|
|
4091
|
+
type: "button",
|
|
4092
|
+
"data-testid": `${testIdPrefix}-${col}-remove-gap-${index}`,
|
|
4093
|
+
onClick: () => removeGap(col, index),
|
|
4094
|
+
title: "Remove gap",
|
|
4095
|
+
className: "text-[10px] text-sas-muted hover:text-sas-danger",
|
|
4096
|
+
children: "\u2715"
|
|
4097
|
+
}
|
|
4098
|
+
)
|
|
4099
|
+
]
|
|
4100
|
+
}
|
|
4101
|
+
);
|
|
4102
|
+
}
|
|
4103
|
+
const primary = track ? track.prompt?.trim() || track.name : slotId;
|
|
4104
|
+
const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
|
|
4105
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4009
4106
|
"div",
|
|
4010
4107
|
{
|
|
4011
|
-
|
|
4012
|
-
"data-testid":
|
|
4013
|
-
|
|
4014
|
-
className:
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4025
|
-
"div",
|
|
4026
|
-
{
|
|
4027
|
-
"aria-hidden": "true",
|
|
4028
|
-
className: "absolute top-0 bottom-0 w-px bg-sas-accent/40",
|
|
4029
|
-
style: { left: "50%" }
|
|
4030
|
-
}
|
|
4031
|
-
),
|
|
4032
|
-
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4033
|
-
"div",
|
|
4034
|
-
{
|
|
4035
|
-
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
4036
|
-
"aria-hidden": "true",
|
|
4037
|
-
className: t.isDownbeat ? "absolute bg-sas-accent" : "absolute bg-sas-muted/50",
|
|
4038
|
-
style: {
|
|
4039
|
-
left: `${(t.fraction * 100).toFixed(2)}%`,
|
|
4040
|
-
top: (SLIDER_HEIGHT_PX - (t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX)) / 2,
|
|
4041
|
-
width: 1,
|
|
4042
|
-
height: t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX
|
|
4043
|
-
}
|
|
4044
|
-
},
|
|
4045
|
-
t.i
|
|
4046
|
-
)),
|
|
4047
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4048
|
-
"div",
|
|
4108
|
+
...cellDragProps(col, index, locked),
|
|
4109
|
+
"data-testid": `${testIdPrefix}-${col}-cell-${slotId}`,
|
|
4110
|
+
"data-value": slotId,
|
|
4111
|
+
className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
|
|
4112
|
+
title: track ? track.dbId : "Track no longer available",
|
|
4113
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-start gap-1", children: [
|
|
4114
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
|
|
4115
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
4116
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
|
|
4117
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
|
|
4118
|
+
] }),
|
|
4119
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4120
|
+
"button",
|
|
4049
4121
|
{
|
|
4050
|
-
|
|
4051
|
-
"
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
pointerEvents: "none"
|
|
4058
|
-
}
|
|
4122
|
+
type: "button",
|
|
4123
|
+
"data-testid": `${testIdPrefix}-${col}-insert-gap-${index}`,
|
|
4124
|
+
onClick: () => insertGapAbove(col, index),
|
|
4125
|
+
disabled: locked,
|
|
4126
|
+
title: "Insert a gap above (make this a fade)",
|
|
4127
|
+
className: "text-[10px] text-sas-muted opacity-0 group-hover:opacity-100 hover:text-sas-accent disabled:opacity-30",
|
|
4128
|
+
children: "+gap"
|
|
4059
4129
|
}
|
|
4060
4130
|
)
|
|
4061
|
-
]
|
|
4062
|
-
}
|
|
4063
|
-
),
|
|
4064
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4065
|
-
"span",
|
|
4066
|
-
{
|
|
4067
|
-
"data-testid": "offset-scrubber-readout",
|
|
4068
|
-
className: "text-[10px] text-sas-muted/70 tabular-nums flex-shrink-0 min-w-[64px] text-right",
|
|
4069
|
-
children: formatOffset(draftOffset, sampleRate)
|
|
4070
|
-
}
|
|
4071
|
-
),
|
|
4072
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4073
|
-
"button",
|
|
4074
|
-
{
|
|
4075
|
-
type: "button",
|
|
4076
|
-
"data-testid": "offset-scrubber-reset",
|
|
4077
|
-
onClick: handleResetToZero,
|
|
4078
|
-
disabled: isDisabled || draftOffset === 0,
|
|
4079
|
-
className: `text-[10px] px-1 py-0.5 rounded-sm border transition-colors flex-shrink-0 ${isDisabled || draftOffset === 0 ? "border-sas-border text-sas-muted/30 cursor-not-allowed" : "border-sas-border text-sas-muted/70 hover:border-sas-accent hover:text-sas-accent"}`,
|
|
4080
|
-
title: "Reset offset to 0 (bar 1)",
|
|
4081
|
-
children: "\u2316"
|
|
4082
|
-
}
|
|
4083
|
-
),
|
|
4084
|
-
bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4085
|
-
"span",
|
|
4086
|
-
{
|
|
4087
|
-
"data-testid": "offset-bpm-mismatch",
|
|
4088
|
-
className: "text-[9px] px-1 py-0.5 rounded-sm bg-amber-500/15 text-amber-400 border border-amber-500/30 flex-shrink-0",
|
|
4089
|
-
title: `Detected ${detectedBpm.toFixed(1)} BPM \u2014 beats may not align with project ${projectBpm} BPM grid`,
|
|
4090
|
-
children: "BPM \u2260"
|
|
4131
|
+
] })
|
|
4091
4132
|
}
|
|
4092
|
-
)
|
|
4133
|
+
);
|
|
4134
|
+
};
|
|
4135
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
|
|
4136
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
|
|
4137
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
|
|
4138
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: fromLabel }),
|
|
4139
|
+
" \u2192",
|
|
4140
|
+
" ",
|
|
4141
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: toLabel }),
|
|
4142
|
+
familyLabel ? ` \xB7 ${familyLabel}` : "",
|
|
4143
|
+
" \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
|
|
4144
|
+
] }),
|
|
4145
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4146
|
+
creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
|
|
4147
|
+
creatingKeys.size,
|
|
4148
|
+
" creating\u2026"
|
|
4149
|
+
] }),
|
|
4150
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4151
|
+
"button",
|
|
4152
|
+
{
|
|
4153
|
+
type: "button",
|
|
4154
|
+
"data-testid": `${testIdPrefix}-create-all`,
|
|
4155
|
+
onClick: createAll,
|
|
4156
|
+
disabled: eligibleCount === 0,
|
|
4157
|
+
title: "Create every staged transition at once (runs several concurrently)",
|
|
4158
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
4159
|
+
children: [
|
|
4160
|
+
"Create all",
|
|
4161
|
+
eligibleCount > 0 ? ` (${eligibleCount})` : ""
|
|
4162
|
+
]
|
|
4163
|
+
}
|
|
4164
|
+
)
|
|
4165
|
+
] })
|
|
4166
|
+
] }),
|
|
4167
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
|
|
4168
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
|
|
4169
|
+
"Origin (",
|
|
4170
|
+
fromLabel,
|
|
4171
|
+
")"
|
|
4172
|
+
] }),
|
|
4173
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
|
|
4174
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
|
|
4175
|
+
"Target (",
|
|
4176
|
+
toLabel,
|
|
4177
|
+
")"
|
|
4178
|
+
] })
|
|
4179
|
+
] }),
|
|
4180
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
|
|
4181
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
4182
|
+
load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
|
|
4183
|
+
"No tracks to arrange in this panel for either scene. Add tracks to ",
|
|
4184
|
+
fromLabel,
|
|
4185
|
+
" or ",
|
|
4186
|
+
toLabel,
|
|
4187
|
+
" ",
|
|
4188
|
+
"first (or free one by deleting an existing crossfade/fade)."
|
|
4189
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
|
|
4190
|
+
const key = rowKey(row);
|
|
4191
|
+
const isCreatingThis = key !== null && creatingKeys.has(key);
|
|
4192
|
+
const errMsg = key !== null ? rowErrors[key] : void 0;
|
|
4193
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4194
|
+
"div",
|
|
4195
|
+
{
|
|
4196
|
+
"data-testid": `${testIdPrefix}-row-${i}`,
|
|
4197
|
+
className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
|
|
4198
|
+
children: [
|
|
4199
|
+
renderCell("origin", i, row.originId),
|
|
4200
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
|
|
4201
|
+
!row.type ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4202
|
+
"span",
|
|
4203
|
+
{
|
|
4204
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4205
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
|
|
4206
|
+
children: TYPE_LABEL[row.type]
|
|
4207
|
+
}
|
|
4208
|
+
) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
|
|
4209
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4210
|
+
"select",
|
|
4211
|
+
{
|
|
4212
|
+
"data-testid": `${testIdPrefix}-effect-${i}`,
|
|
4213
|
+
value: rowEffects[row.originId ?? row.targetId] ?? "fade",
|
|
4214
|
+
onChange: (e) => {
|
|
4215
|
+
const id = row.originId ?? row.targetId;
|
|
4216
|
+
if (id) setRowEffect(id, e.target.value);
|
|
4217
|
+
},
|
|
4218
|
+
className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
|
|
4219
|
+
children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
|
|
4220
|
+
}
|
|
4221
|
+
),
|
|
4222
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
|
|
4223
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4224
|
+
"span",
|
|
4225
|
+
{
|
|
4226
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4227
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
|
|
4228
|
+
children: TYPE_LABEL[row.type]
|
|
4229
|
+
}
|
|
4230
|
+
),
|
|
4231
|
+
isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4232
|
+
SorceryProgressBar,
|
|
4233
|
+
{
|
|
4234
|
+
isLoading: true,
|
|
4235
|
+
heightClass: "h-5",
|
|
4236
|
+
statusText: "CREATING",
|
|
4237
|
+
estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
|
|
4238
|
+
}
|
|
4239
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4240
|
+
"button",
|
|
4241
|
+
{
|
|
4242
|
+
type: "button",
|
|
4243
|
+
"data-testid": `${testIdPrefix}-create-${i}`,
|
|
4244
|
+
onClick: () => createRow(row),
|
|
4245
|
+
disabled: !row.type,
|
|
4246
|
+
className: `w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${row.type ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
4247
|
+
children: "Create"
|
|
4248
|
+
}
|
|
4249
|
+
),
|
|
4250
|
+
errMsg && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4251
|
+
"span",
|
|
4252
|
+
{
|
|
4253
|
+
"data-testid": `${testIdPrefix}-row-error-${i}`,
|
|
4254
|
+
className: "text-[10px] text-sas-danger text-center leading-tight",
|
|
4255
|
+
children: errMsg
|
|
4256
|
+
}
|
|
4257
|
+
)
|
|
4258
|
+
] }),
|
|
4259
|
+
renderCell("target", i, row.targetId)
|
|
4260
|
+
]
|
|
4261
|
+
},
|
|
4262
|
+
i
|
|
4263
|
+
);
|
|
4264
|
+
}) }))
|
|
4093
4265
|
] });
|
|
4094
4266
|
}
|
|
4095
|
-
function formatOffset(samples, sampleRate) {
|
|
4096
|
-
const sign = samples > 0 ? "+" : samples < 0 ? "-" : "";
|
|
4097
|
-
const abs = Math.abs(samples);
|
|
4098
|
-
const ms = Math.round(abs / sampleRate * 1e3);
|
|
4099
|
-
return `${sign}${abs} spl (${sign}${ms} ms)`;
|
|
4100
|
-
}
|
|
4101
4267
|
|
|
4102
|
-
// src/components/
|
|
4103
|
-
var
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
for (let c = 0; c < audioBuffer.numberOfChannels; c++) {
|
|
4112
|
-
const data = audioBuffer.getChannelData(c);
|
|
4113
|
-
for (let i = 0; i < data.length; i++) {
|
|
4114
|
-
const a = Math.abs(data[i]);
|
|
4115
|
-
if (a > peak) peak = a;
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
const peakDb = peak > 1e-6 ? 20 * Math.log10(peak) : -120;
|
|
4119
|
-
return {
|
|
4120
|
-
peakLinear: peak,
|
|
4121
|
-
peakDb,
|
|
4122
|
-
clipped: peak >= CLIP_THRESHOLD_LINEAR - 5e-3
|
|
4123
|
-
};
|
|
4124
|
-
} finally {
|
|
4125
|
-
await audioContext.close().catch(() => {
|
|
4126
|
-
});
|
|
4127
|
-
}
|
|
4268
|
+
// src/components/DownloadPackButton.tsx
|
|
4269
|
+
var import_react17 = require("react");
|
|
4270
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
4271
|
+
function formatSize(bytes) {
|
|
4272
|
+
if (!bytes || bytes <= 0) return "";
|
|
4273
|
+
const gb = bytes / 1024 ** 3;
|
|
4274
|
+
if (gb >= 1) return `${gb.toFixed(1)} GB`;
|
|
4275
|
+
const mb = bytes / 1024 ** 2;
|
|
4276
|
+
return `${Math.round(mb)} MB`;
|
|
4128
4277
|
}
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
}) {
|
|
4137
|
-
const
|
|
4138
|
-
const
|
|
4139
|
-
const
|
|
4140
|
-
|
|
4278
|
+
var DownloadPackButton = ({
|
|
4279
|
+
host,
|
|
4280
|
+
packId,
|
|
4281
|
+
displayName,
|
|
4282
|
+
sizeBytes,
|
|
4283
|
+
variant = "compact",
|
|
4284
|
+
onDownloadComplete
|
|
4285
|
+
}) => {
|
|
4286
|
+
const [status, setStatus] = (0, import_react17.useState)("idle");
|
|
4287
|
+
const [progress, setProgress] = (0, import_react17.useState)(0);
|
|
4288
|
+
const [errorMessage, setErrorMessage] = (0, import_react17.useState)(null);
|
|
4289
|
+
(0, import_react17.useEffect)(() => {
|
|
4290
|
+
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
4291
|
+
setStatus(p.status);
|
|
4292
|
+
setProgress(p.progress);
|
|
4293
|
+
if (p.status === "error") {
|
|
4294
|
+
setErrorMessage(p.message || "Download failed");
|
|
4295
|
+
} else if (p.status === "complete") {
|
|
4296
|
+
setErrorMessage(null);
|
|
4297
|
+
setTimeout(() => onDownloadComplete?.(), 250);
|
|
4298
|
+
} else {
|
|
4299
|
+
setErrorMessage(null);
|
|
4300
|
+
}
|
|
4301
|
+
});
|
|
4302
|
+
return unsub;
|
|
4303
|
+
}, [host, packId, onDownloadComplete]);
|
|
4304
|
+
const handleClick = (0, import_react17.useCallback)(async () => {
|
|
4305
|
+
if (status !== "idle" && status !== "error") return;
|
|
4306
|
+
try {
|
|
4307
|
+
setStatus("downloading");
|
|
4308
|
+
setProgress(0);
|
|
4309
|
+
setErrorMessage(null);
|
|
4310
|
+
const result = await host.startSamplePackDownload(packId);
|
|
4311
|
+
if (!result.success) {
|
|
4312
|
+
setStatus("error");
|
|
4313
|
+
setErrorMessage(result.error || "Download failed");
|
|
4314
|
+
}
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
console.error("[DownloadPackButton] start failed:", err);
|
|
4317
|
+
setStatus("error");
|
|
4318
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
4319
|
+
}
|
|
4320
|
+
}, [host, packId, status]);
|
|
4321
|
+
const isWorking = status === "downloading" || status === "verifying" || status === "extracting" || status === "installing";
|
|
4322
|
+
const isDisabled = isWorking || status === "complete";
|
|
4323
|
+
const buttonLabel = (() => {
|
|
4324
|
+
switch (status) {
|
|
4325
|
+
case "downloading":
|
|
4326
|
+
return `${progress}%`;
|
|
4327
|
+
case "verifying":
|
|
4328
|
+
return "Verifying...";
|
|
4329
|
+
case "extracting":
|
|
4330
|
+
return "Extracting...";
|
|
4331
|
+
case "installing":
|
|
4332
|
+
return "Installing...";
|
|
4333
|
+
case "complete":
|
|
4334
|
+
return "Done!";
|
|
4335
|
+
case "error":
|
|
4336
|
+
return "Retry";
|
|
4337
|
+
default:
|
|
4338
|
+
return variant === "large" ? `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ""}` : "Download";
|
|
4339
|
+
}
|
|
4340
|
+
})();
|
|
4341
|
+
const tooltip = (() => {
|
|
4342
|
+
if (status === "error") return errorMessage || "Download failed. Click to retry.";
|
|
4343
|
+
if (isWorking) return `${buttonLabel} \u2014 ${displayName}`;
|
|
4344
|
+
if (status === "complete") return "Installation complete";
|
|
4345
|
+
return `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ""}`;
|
|
4346
|
+
})();
|
|
4347
|
+
const baseClasses = variant === "large" ? "px-4 py-2 text-sm font-medium rounded border transition-colors" : "px-2 py-0.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors";
|
|
4348
|
+
let className;
|
|
4349
|
+
if (status === "error") {
|
|
4350
|
+
className = `${baseClasses} text-red-400 border-red-400/50 hover:text-red-300 hover:border-red-300`;
|
|
4351
|
+
} else if (status === "complete") {
|
|
4352
|
+
className = `${baseClasses} text-green-400 border-green-400/50`;
|
|
4353
|
+
} else if (isDisabled) {
|
|
4354
|
+
className = `${baseClasses} text-sas-accent border-sas-accent/50 cursor-wait`;
|
|
4355
|
+
} else {
|
|
4356
|
+
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
4357
|
+
}
|
|
4358
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
4359
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
4360
|
+
"button",
|
|
4361
|
+
{
|
|
4362
|
+
"data-testid": `download-pack-button-${packId}`,
|
|
4363
|
+
onClick: handleClick,
|
|
4364
|
+
disabled: isDisabled,
|
|
4365
|
+
className,
|
|
4366
|
+
title: tooltip,
|
|
4367
|
+
children: buttonLabel
|
|
4368
|
+
}
|
|
4369
|
+
),
|
|
4370
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
4371
|
+
] });
|
|
4372
|
+
};
|
|
4373
|
+
|
|
4374
|
+
// src/components/SamplePackCTACard.tsx
|
|
4375
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
4376
|
+
var SamplePackCTACard = ({
|
|
4377
|
+
host,
|
|
4378
|
+
pack,
|
|
4379
|
+
status,
|
|
4380
|
+
onDownloadComplete
|
|
4381
|
+
}) => {
|
|
4382
|
+
if (status === "checking") {
|
|
4383
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
4384
|
+
"div",
|
|
4385
|
+
{
|
|
4386
|
+
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
4387
|
+
className: "flex items-center justify-center py-16 text-sas-muted text-sm",
|
|
4388
|
+
children: "Checking sample library..."
|
|
4389
|
+
}
|
|
4390
|
+
);
|
|
4391
|
+
}
|
|
4392
|
+
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
4393
|
+
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
4394
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
4395
|
+
"div",
|
|
4396
|
+
{
|
|
4397
|
+
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
4398
|
+
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
4399
|
+
children: [
|
|
4400
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
4401
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
4402
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
4403
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
4404
|
+
DownloadPackButton,
|
|
4405
|
+
{
|
|
4406
|
+
host,
|
|
4407
|
+
packId: pack.packId,
|
|
4408
|
+
displayName: pack.displayName,
|
|
4409
|
+
sizeBytes: pack.sizeBytes,
|
|
4410
|
+
variant: "large",
|
|
4411
|
+
onDownloadComplete
|
|
4412
|
+
}
|
|
4413
|
+
)
|
|
4414
|
+
]
|
|
4415
|
+
}
|
|
4416
|
+
);
|
|
4417
|
+
};
|
|
4418
|
+
|
|
4419
|
+
// src/components/WaveformView.tsx
|
|
4420
|
+
var import_react18 = require("react");
|
|
4421
|
+
|
|
4422
|
+
// src/components/waveform.ts
|
|
4423
|
+
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
4424
|
+
const { length, numberOfChannels, sampleRate } = audioBuffer;
|
|
4425
|
+
const channels = [];
|
|
4426
|
+
for (let c = 0; c < numberOfChannels; c++) {
|
|
4427
|
+
channels.push(audioBuffer.getChannelData(c));
|
|
4428
|
+
}
|
|
4429
|
+
const totalForBinning = typeof targetSamples === "number" && targetSamples > length ? targetSamples : length;
|
|
4430
|
+
const samplesPerBin = Math.max(1, Math.floor(totalForBinning / bins));
|
|
4431
|
+
const out = new Float32Array(bins * 2);
|
|
4432
|
+
for (let i = 0; i < bins; i++) {
|
|
4433
|
+
const startIdx = i * samplesPerBin;
|
|
4434
|
+
const endIdx = Math.min(length, startIdx + samplesPerBin);
|
|
4435
|
+
if (startIdx >= length) {
|
|
4436
|
+
out[i * 2] = 0;
|
|
4437
|
+
out[i * 2 + 1] = 0;
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
let mn = Infinity;
|
|
4441
|
+
let mx = -Infinity;
|
|
4442
|
+
for (let j = startIdx; j < endIdx; j++) {
|
|
4443
|
+
let v = 0;
|
|
4444
|
+
for (let c = 0; c < numberOfChannels; c++) {
|
|
4445
|
+
v += channels[c][j];
|
|
4446
|
+
}
|
|
4447
|
+
v /= numberOfChannels;
|
|
4448
|
+
if (v < mn) mn = v;
|
|
4449
|
+
if (v > mx) mx = v;
|
|
4450
|
+
}
|
|
4451
|
+
if (!Number.isFinite(mn)) mn = 0;
|
|
4452
|
+
if (!Number.isFinite(mx)) mx = 0;
|
|
4453
|
+
out[i * 2] = mn;
|
|
4454
|
+
out[i * 2 + 1] = mx;
|
|
4455
|
+
}
|
|
4456
|
+
return { sampleRate, totalSamples: totalForBinning, peaks: out };
|
|
4457
|
+
}
|
|
4458
|
+
function drawWaveform(canvas, peaks, options = {}) {
|
|
4459
|
+
const dpr = window.devicePixelRatio || 1;
|
|
4460
|
+
const cssWidth = canvas.clientWidth;
|
|
4461
|
+
const cssHeight = canvas.clientHeight;
|
|
4462
|
+
if (cssWidth === 0 || cssHeight === 0) return;
|
|
4463
|
+
canvas.width = Math.floor(cssWidth * dpr);
|
|
4464
|
+
canvas.height = Math.floor(cssHeight * dpr);
|
|
4465
|
+
const ctx = canvas.getContext("2d");
|
|
4466
|
+
if (!ctx) return;
|
|
4467
|
+
ctx.scale(dpr, dpr);
|
|
4468
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
4469
|
+
ctx.fillStyle = options.fillStyle ?? "rgba(255, 255, 255, 0.4)";
|
|
4470
|
+
const bins = peaks.peaks.length / 2;
|
|
4471
|
+
const mid = cssHeight / 2;
|
|
4472
|
+
for (let x = 0; x < cssWidth; x++) {
|
|
4473
|
+
const binIdx = Math.floor(x / cssWidth * bins);
|
|
4474
|
+
const mn = peaks.peaks[binIdx * 2];
|
|
4475
|
+
const mx = peaks.peaks[binIdx * 2 + 1];
|
|
4476
|
+
const yTop = mid - mx * mid;
|
|
4477
|
+
const yBot = mid - mn * mid;
|
|
4478
|
+
ctx.fillRect(x, yTop, 1, Math.max(1, yBot - yTop));
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
// src/components/WaveformView.tsx
|
|
4483
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
4484
|
+
var WaveformView = ({
|
|
4485
|
+
host,
|
|
4486
|
+
filePath,
|
|
4487
|
+
bins = 256,
|
|
4488
|
+
className,
|
|
4489
|
+
fillStyle,
|
|
4490
|
+
targetSamples
|
|
4491
|
+
}) => {
|
|
4492
|
+
const canvasRef = (0, import_react18.useRef)(null);
|
|
4493
|
+
const [peaks, setPeaks] = (0, import_react18.useState)(null);
|
|
4494
|
+
(0, import_react18.useEffect)(() => {
|
|
4495
|
+
let cancelled = false;
|
|
4496
|
+
let audioContext = null;
|
|
4497
|
+
(async () => {
|
|
4498
|
+
try {
|
|
4499
|
+
const bytes = await host.getAudioFileBytes(filePath);
|
|
4500
|
+
if (cancelled) return;
|
|
4501
|
+
const ContextCtor = window.AudioContext ?? window.webkitAudioContext;
|
|
4502
|
+
audioContext = new ContextCtor();
|
|
4503
|
+
const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));
|
|
4504
|
+
if (cancelled) return;
|
|
4505
|
+
const computed = computePeaks(audioBuffer, bins, targetSamples);
|
|
4506
|
+
setPeaks(computed);
|
|
4507
|
+
} catch (err) {
|
|
4508
|
+
console.warn("[WaveformView] failed to decode", filePath, err);
|
|
4509
|
+
} finally {
|
|
4510
|
+
if (audioContext) {
|
|
4511
|
+
audioContext.close().catch(() => {
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
})();
|
|
4516
|
+
return () => {
|
|
4517
|
+
cancelled = true;
|
|
4518
|
+
};
|
|
4519
|
+
}, [host, filePath, bins, targetSamples]);
|
|
4520
|
+
(0, import_react18.useEffect)(() => {
|
|
4521
|
+
if (!peaks) return;
|
|
4522
|
+
const canvas = canvasRef.current;
|
|
4523
|
+
if (!canvas) return;
|
|
4524
|
+
drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : void 0);
|
|
4525
|
+
const observer = new ResizeObserver(() => {
|
|
4526
|
+
drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : void 0);
|
|
4527
|
+
});
|
|
4528
|
+
observer.observe(canvas);
|
|
4529
|
+
return () => observer.disconnect();
|
|
4530
|
+
}, [peaks, fillStyle]);
|
|
4531
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4532
|
+
"canvas",
|
|
4533
|
+
{
|
|
4534
|
+
ref: canvasRef,
|
|
4535
|
+
"data-testid": "waveform-view",
|
|
4536
|
+
className: className ?? "w-full h-10"
|
|
4537
|
+
}
|
|
4538
|
+
);
|
|
4539
|
+
};
|
|
4540
|
+
|
|
4541
|
+
// src/components/ScrollingWaveform.tsx
|
|
4542
|
+
var import_react19 = require("react");
|
|
4543
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
4544
|
+
var ScrollingWaveform = ({
|
|
4545
|
+
getPeakDb,
|
|
4546
|
+
active,
|
|
4547
|
+
columns = 256,
|
|
4548
|
+
className,
|
|
4549
|
+
fillStyle
|
|
4550
|
+
}) => {
|
|
4551
|
+
const canvasRef = (0, import_react19.useRef)(null);
|
|
4552
|
+
const ringRef = (0, import_react19.useRef)(new Float32Array(columns));
|
|
4553
|
+
const writeIdxRef = (0, import_react19.useRef)(0);
|
|
4554
|
+
const rafRef = (0, import_react19.useRef)(null);
|
|
4555
|
+
(0, import_react19.useEffect)(() => {
|
|
4556
|
+
if (ringRef.current.length !== columns) {
|
|
4557
|
+
const next = new Float32Array(columns);
|
|
4558
|
+
const prev = ringRef.current;
|
|
4559
|
+
const copyLen = Math.min(prev.length, columns);
|
|
4560
|
+
for (let i = 0; i < copyLen; i++) {
|
|
4561
|
+
next[i] = prev[i];
|
|
4562
|
+
}
|
|
4563
|
+
ringRef.current = next;
|
|
4564
|
+
writeIdxRef.current = writeIdxRef.current % columns;
|
|
4565
|
+
}
|
|
4566
|
+
}, [columns]);
|
|
4567
|
+
(0, import_react19.useEffect)(() => {
|
|
4568
|
+
if (!active) {
|
|
4569
|
+
if (rafRef.current !== null) {
|
|
4570
|
+
cancelAnimationFrame(rafRef.current);
|
|
4571
|
+
rafRef.current = null;
|
|
4572
|
+
}
|
|
4573
|
+
return;
|
|
4574
|
+
}
|
|
4575
|
+
const tick = () => {
|
|
4576
|
+
const peakDb = getPeakDb();
|
|
4577
|
+
const amp = peakDb <= -120 ? 0 : Math.max(0, Math.min(1, (peakDb + 60) / 60));
|
|
4578
|
+
const ring = ringRef.current;
|
|
4579
|
+
ring[writeIdxRef.current] = amp;
|
|
4580
|
+
writeIdxRef.current = (writeIdxRef.current + 1) % ring.length;
|
|
4581
|
+
const canvas = canvasRef.current;
|
|
4582
|
+
if (canvas) {
|
|
4583
|
+
const dpr = window.devicePixelRatio || 1;
|
|
4584
|
+
const cssW = canvas.clientWidth;
|
|
4585
|
+
const cssH = canvas.clientHeight;
|
|
4586
|
+
if (cssW > 0 && cssH > 0) {
|
|
4587
|
+
if (canvas.width !== Math.floor(cssW * dpr) || canvas.height !== Math.floor(cssH * dpr)) {
|
|
4588
|
+
canvas.width = Math.floor(cssW * dpr);
|
|
4589
|
+
canvas.height = Math.floor(cssH * dpr);
|
|
4590
|
+
}
|
|
4591
|
+
const ctx = canvas.getContext("2d");
|
|
4592
|
+
if (ctx) {
|
|
4593
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
4594
|
+
ctx.clearRect(0, 0, cssW, cssH);
|
|
4595
|
+
ctx.fillStyle = fillStyle ?? "#6af2c5";
|
|
4596
|
+
const mid = cssH / 2;
|
|
4597
|
+
const cols = ring.length;
|
|
4598
|
+
const colW = cssW / cols;
|
|
4599
|
+
const start = writeIdxRef.current;
|
|
4600
|
+
for (let x = 0; x < cols; x++) {
|
|
4601
|
+
const ringIdx = (start + x) % cols;
|
|
4602
|
+
const a = ring[ringIdx];
|
|
4603
|
+
const half = a * mid;
|
|
4604
|
+
ctx.fillRect(x * colW, mid - half, Math.max(1, colW), Math.max(1, half * 2));
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
4610
|
+
};
|
|
4611
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
4612
|
+
return () => {
|
|
4613
|
+
if (rafRef.current !== null) {
|
|
4614
|
+
cancelAnimationFrame(rafRef.current);
|
|
4615
|
+
rafRef.current = null;
|
|
4616
|
+
}
|
|
4617
|
+
};
|
|
4618
|
+
}, [active, getPeakDb, fillStyle]);
|
|
4619
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
4620
|
+
"canvas",
|
|
4621
|
+
{
|
|
4622
|
+
ref: canvasRef,
|
|
4623
|
+
"data-testid": "scrolling-waveform",
|
|
4624
|
+
className: className ?? "w-full h-12"
|
|
4625
|
+
}
|
|
4626
|
+
);
|
|
4627
|
+
};
|
|
4628
|
+
|
|
4629
|
+
// src/components/OffsetScrubber.tsx
|
|
4630
|
+
var import_react20 = require("react");
|
|
4631
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
4632
|
+
var SLIDER_HEIGHT_PX = 28;
|
|
4633
|
+
var TICK_HEIGHT_PX = 14;
|
|
4634
|
+
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
4635
|
+
var THUMB_WIDTH_PX = 4;
|
|
4636
|
+
function OffsetScrubber({
|
|
4637
|
+
cuePoints,
|
|
4638
|
+
offsetSamples,
|
|
4639
|
+
projectBpm,
|
|
4640
|
+
meter = 4,
|
|
4641
|
+
onChange,
|
|
4642
|
+
disabled = false
|
|
4643
|
+
}) {
|
|
4644
|
+
const trackRef = (0, import_react20.useRef)(null);
|
|
4645
|
+
const [draftOffset, setDraftOffset] = (0, import_react20.useState)(offsetSamples);
|
|
4646
|
+
const [isDragging, setIsDragging] = (0, import_react20.useState)(false);
|
|
4647
|
+
(0, import_react20.useEffect)(() => {
|
|
4648
|
+
if (!isDragging) setDraftOffset(offsetSamples);
|
|
4649
|
+
}, [offsetSamples, isDragging]);
|
|
4650
|
+
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
4651
|
+
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
4652
|
+
const beatsForRange = (0, import_react20.useMemo)(() => {
|
|
4653
|
+
return Math.round(60 / projectBpm * sampleRate);
|
|
4654
|
+
}, [projectBpm, sampleRate]);
|
|
4655
|
+
const rangeSamples = beatsForRange * meter;
|
|
4656
|
+
const sampleToFraction = (0, import_react20.useCallback)(
|
|
4657
|
+
(sample) => {
|
|
4658
|
+
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
4659
|
+
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
4660
|
+
},
|
|
4661
|
+
[rangeSamples]
|
|
4662
|
+
);
|
|
4663
|
+
const fractionToSample = (0, import_react20.useCallback)(
|
|
4664
|
+
(fraction) => {
|
|
4665
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
4666
|
+
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
4667
|
+
},
|
|
4668
|
+
[rangeSamples]
|
|
4669
|
+
);
|
|
4670
|
+
const snapTargets = (0, import_react20.useMemo)(() => {
|
|
4671
|
+
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
4672
|
+
const downbeat = cuePoints.beats[0];
|
|
4673
|
+
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
4674
|
+
const negatives = positives.slice(1).map((p) => -p);
|
|
4675
|
+
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
4676
|
+
}, [cuePoints]);
|
|
4677
|
+
const snapToBeat = (0, import_react20.useCallback)(
|
|
4678
|
+
(sample) => {
|
|
4679
|
+
if (snapTargets.length === 0) return sample;
|
|
4680
|
+
let best = snapTargets[0];
|
|
4681
|
+
let bestDist = Math.abs(sample - best);
|
|
4682
|
+
for (const t of snapTargets) {
|
|
4683
|
+
const d = Math.abs(sample - t);
|
|
4684
|
+
if (d < bestDist) {
|
|
4685
|
+
best = t;
|
|
4686
|
+
bestDist = d;
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
return best;
|
|
4690
|
+
},
|
|
4691
|
+
[snapTargets]
|
|
4692
|
+
);
|
|
4693
|
+
const handlePointerDown = (0, import_react20.useCallback)(
|
|
4694
|
+
(e) => {
|
|
4695
|
+
if (disabled || !cuePoints) return;
|
|
4696
|
+
e.preventDefault();
|
|
4697
|
+
const track = trackRef.current;
|
|
4698
|
+
if (!track) return;
|
|
4699
|
+
track.setPointerCapture(e.pointerId);
|
|
4700
|
+
setIsDragging(true);
|
|
4701
|
+
const updateFromEvent = (clientX, shiftHeld) => {
|
|
4702
|
+
const rect = track.getBoundingClientRect();
|
|
4703
|
+
const fraction = (clientX - rect.left) / rect.width;
|
|
4704
|
+
const raw = fractionToSample(fraction);
|
|
4705
|
+
return shiftHeld ? raw : snapToBeat(raw);
|
|
4706
|
+
};
|
|
4707
|
+
setDraftOffset(updateFromEvent(e.clientX, e.shiftKey));
|
|
4708
|
+
const onMove = (ev) => {
|
|
4709
|
+
setDraftOffset(updateFromEvent(ev.clientX, ev.shiftKey));
|
|
4710
|
+
};
|
|
4711
|
+
const onUp = (ev) => {
|
|
4712
|
+
const final = updateFromEvent(ev.clientX, ev.shiftKey);
|
|
4713
|
+
track.releasePointerCapture(e.pointerId);
|
|
4714
|
+
track.removeEventListener("pointermove", onMove);
|
|
4715
|
+
track.removeEventListener("pointerup", onUp);
|
|
4716
|
+
track.removeEventListener("pointercancel", onUp);
|
|
4717
|
+
setIsDragging(false);
|
|
4718
|
+
setDraftOffset(final);
|
|
4719
|
+
onChange(final);
|
|
4720
|
+
};
|
|
4721
|
+
track.addEventListener("pointermove", onMove);
|
|
4722
|
+
track.addEventListener("pointerup", onUp);
|
|
4723
|
+
track.addEventListener("pointercancel", onUp);
|
|
4724
|
+
},
|
|
4725
|
+
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
4726
|
+
);
|
|
4727
|
+
const handleResetToZero = (0, import_react20.useCallback)(() => {
|
|
4728
|
+
if (disabled) return;
|
|
4729
|
+
setDraftOffset(0);
|
|
4730
|
+
onChange(0);
|
|
4731
|
+
}, [disabled, onChange]);
|
|
4732
|
+
const thumbFraction = sampleToFraction(draftOffset);
|
|
4733
|
+
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
4734
|
+
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
4735
|
+
const ticks = (0, import_react20.useMemo)(() => {
|
|
4736
|
+
if (!cuePoints) return [];
|
|
4737
|
+
const downbeat = cuePoints.beats[0] ?? 0;
|
|
4738
|
+
return cuePoints.beats.map((b, i) => {
|
|
4739
|
+
const offsetCandidate = b - downbeat;
|
|
4740
|
+
const fraction = sampleToFraction(offsetCandidate);
|
|
4741
|
+
const isDownbeat = i === 0;
|
|
4742
|
+
return { i, fraction, isDownbeat };
|
|
4743
|
+
});
|
|
4744
|
+
}, [cuePoints, sampleToFraction]);
|
|
4745
|
+
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
4746
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
4747
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
4748
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
|
|
4749
|
+
"div",
|
|
4750
|
+
{
|
|
4751
|
+
ref: trackRef,
|
|
4752
|
+
"data-testid": "offset-scrubber-track",
|
|
4753
|
+
onPointerDown: handlePointerDown,
|
|
4754
|
+
className: `relative flex-1 min-w-0 rounded-sm select-none ${isDisabled ? "bg-sas-panel cursor-not-allowed opacity-40" : "bg-sas-bg cursor-pointer"}`,
|
|
4755
|
+
style: { height: SLIDER_HEIGHT_PX },
|
|
4756
|
+
title: isDisabled ? "Generate audio first to enable offset alignment" : "Drag to align beat 1. Hold Shift for free, no-snap movement.",
|
|
4757
|
+
role: "slider",
|
|
4758
|
+
"aria-label": "Audio offset alignment",
|
|
4759
|
+
"aria-valuemin": -rangeSamples,
|
|
4760
|
+
"aria-valuemax": rangeSamples,
|
|
4761
|
+
"aria-valuenow": draftOffset,
|
|
4762
|
+
"aria-disabled": isDisabled,
|
|
4763
|
+
children: [
|
|
4764
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4765
|
+
"div",
|
|
4766
|
+
{
|
|
4767
|
+
"aria-hidden": "true",
|
|
4768
|
+
className: "absolute top-0 bottom-0 w-px bg-sas-accent/40",
|
|
4769
|
+
style: { left: "50%" }
|
|
4770
|
+
}
|
|
4771
|
+
),
|
|
4772
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4773
|
+
"div",
|
|
4774
|
+
{
|
|
4775
|
+
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
4776
|
+
"aria-hidden": "true",
|
|
4777
|
+
className: t.isDownbeat ? "absolute bg-sas-accent" : "absolute bg-sas-muted/50",
|
|
4778
|
+
style: {
|
|
4779
|
+
left: `${(t.fraction * 100).toFixed(2)}%`,
|
|
4780
|
+
top: (SLIDER_HEIGHT_PX - (t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX)) / 2,
|
|
4781
|
+
width: 1,
|
|
4782
|
+
height: t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX
|
|
4783
|
+
}
|
|
4784
|
+
},
|
|
4785
|
+
t.i
|
|
4786
|
+
)),
|
|
4787
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4788
|
+
"div",
|
|
4789
|
+
{
|
|
4790
|
+
"data-testid": "offset-scrubber-thumb",
|
|
4791
|
+
"aria-hidden": "true",
|
|
4792
|
+
className: `absolute top-0 bottom-0 rounded-sm ${isDragging ? "bg-sas-accent" : "bg-sas-accent/80"}`,
|
|
4793
|
+
style: {
|
|
4794
|
+
left: thumbLeftPct,
|
|
4795
|
+
width: THUMB_WIDTH_PX,
|
|
4796
|
+
transform: "translateX(-50%)",
|
|
4797
|
+
pointerEvents: "none"
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
)
|
|
4801
|
+
]
|
|
4802
|
+
}
|
|
4803
|
+
),
|
|
4804
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4805
|
+
"span",
|
|
4806
|
+
{
|
|
4807
|
+
"data-testid": "offset-scrubber-readout",
|
|
4808
|
+
className: "text-[10px] text-sas-muted/70 tabular-nums flex-shrink-0 min-w-[64px] text-right",
|
|
4809
|
+
children: formatOffset(draftOffset, sampleRate)
|
|
4810
|
+
}
|
|
4811
|
+
),
|
|
4812
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4813
|
+
"button",
|
|
4814
|
+
{
|
|
4815
|
+
type: "button",
|
|
4816
|
+
"data-testid": "offset-scrubber-reset",
|
|
4817
|
+
onClick: handleResetToZero,
|
|
4818
|
+
disabled: isDisabled || draftOffset === 0,
|
|
4819
|
+
className: `text-[10px] px-1 py-0.5 rounded-sm border transition-colors flex-shrink-0 ${isDisabled || draftOffset === 0 ? "border-sas-border text-sas-muted/30 cursor-not-allowed" : "border-sas-border text-sas-muted/70 hover:border-sas-accent hover:text-sas-accent"}`,
|
|
4820
|
+
title: "Reset offset to 0 (bar 1)",
|
|
4821
|
+
children: "\u2316"
|
|
4822
|
+
}
|
|
4823
|
+
),
|
|
4824
|
+
bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4825
|
+
"span",
|
|
4826
|
+
{
|
|
4827
|
+
"data-testid": "offset-bpm-mismatch",
|
|
4828
|
+
className: "text-[9px] px-1 py-0.5 rounded-sm bg-amber-500/15 text-amber-400 border border-amber-500/30 flex-shrink-0",
|
|
4829
|
+
title: `Detected ${detectedBpm.toFixed(1)} BPM \u2014 beats may not align with project ${projectBpm} BPM grid`,
|
|
4830
|
+
children: "BPM \u2260"
|
|
4831
|
+
}
|
|
4832
|
+
)
|
|
4833
|
+
] });
|
|
4834
|
+
}
|
|
4835
|
+
function formatOffset(samples, sampleRate) {
|
|
4836
|
+
const sign = samples > 0 ? "+" : samples < 0 ? "-" : "";
|
|
4837
|
+
const abs = Math.abs(samples);
|
|
4838
|
+
const ms = Math.round(abs / sampleRate * 1e3);
|
|
4839
|
+
return `${sign}${abs} spl (${sign}${ms} ms)`;
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
// src/components/wavPeakAnalyzer.ts
|
|
4843
|
+
var CLIP_THRESHOLD_LINEAR = 0.891;
|
|
4844
|
+
async function analyzeWavPeak(host, filePath) {
|
|
4845
|
+
const bytes = await host.getAudioFileBytes(filePath);
|
|
4846
|
+
const ContextCtor = window.AudioContext ?? window.webkitAudioContext;
|
|
4847
|
+
const audioContext = new ContextCtor();
|
|
4848
|
+
try {
|
|
4849
|
+
const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));
|
|
4850
|
+
let peak = 0;
|
|
4851
|
+
for (let c = 0; c < audioBuffer.numberOfChannels; c++) {
|
|
4852
|
+
const data = audioBuffer.getChannelData(c);
|
|
4853
|
+
for (let i = 0; i < data.length; i++) {
|
|
4854
|
+
const a = Math.abs(data[i]);
|
|
4855
|
+
if (a > peak) peak = a;
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
const peakDb = peak > 1e-6 ? 20 * Math.log10(peak) : -120;
|
|
4859
|
+
return {
|
|
4860
|
+
peakLinear: peak,
|
|
4861
|
+
peakDb,
|
|
4862
|
+
clipped: peak >= CLIP_THRESHOLD_LINEAR - 5e-3
|
|
4863
|
+
};
|
|
4864
|
+
} finally {
|
|
4865
|
+
await audioContext.close().catch(() => {
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
// src/components/synthesizeCuePoints.ts
|
|
4871
|
+
function synthesizeCuePoints({
|
|
4872
|
+
bpm,
|
|
4873
|
+
sampleRate,
|
|
4874
|
+
bars,
|
|
4875
|
+
meter = 4
|
|
4876
|
+
}) {
|
|
4877
|
+
const safeBpm = bpm > 0 ? bpm : 120;
|
|
4878
|
+
const safeSampleRate = sampleRate > 0 ? sampleRate : 48e3;
|
|
4879
|
+
const samplesPerBeat = Math.round(60 / safeBpm * safeSampleRate);
|
|
4880
|
+
const totalBeats = Math.max(1, Math.round(bars * meter));
|
|
4141
4881
|
const beats = [];
|
|
4142
4882
|
for (let i = 0; i < totalBeats; i++) {
|
|
4143
4883
|
beats.push(i * samplesPerBeat);
|
|
4144
4884
|
}
|
|
4145
4885
|
return {
|
|
4146
|
-
schema: 1,
|
|
4147
|
-
sample_rate: safeSampleRate,
|
|
4148
|
-
detected_bpm: safeBpm,
|
|
4149
|
-
downbeat_sample: 0,
|
|
4150
|
-
beats,
|
|
4151
|
-
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4886
|
+
schema: 1,
|
|
4887
|
+
sample_rate: safeSampleRate,
|
|
4888
|
+
detected_bpm: safeBpm,
|
|
4889
|
+
downbeat_sample: 0,
|
|
4890
|
+
beats,
|
|
4891
|
+
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4892
|
+
};
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
// src/panel-core/useGeneratorPanelCore.tsx
|
|
4896
|
+
var import_react25 = require("react");
|
|
4897
|
+
|
|
4898
|
+
// src/hooks/useSceneState.ts
|
|
4899
|
+
var import_react21 = require("react");
|
|
4900
|
+
function useSceneState(activeSceneId, initialValue) {
|
|
4901
|
+
const [stateMap, setStateMap] = (0, import_react21.useState)(() => /* @__PURE__ */ new Map());
|
|
4902
|
+
const activeSceneIdRef = (0, import_react21.useRef)(activeSceneId);
|
|
4903
|
+
activeSceneIdRef.current = activeSceneId;
|
|
4904
|
+
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
4905
|
+
const setForCurrentScene = (0, import_react21.useCallback)((value) => {
|
|
4906
|
+
const sid = activeSceneIdRef.current;
|
|
4907
|
+
if (sid === null) return;
|
|
4908
|
+
setStateMap((prev) => {
|
|
4909
|
+
const current = prev.has(sid) ? prev.get(sid) : initialValue;
|
|
4910
|
+
const next = typeof value === "function" ? value(current) : value;
|
|
4911
|
+
const newMap = new Map(prev);
|
|
4912
|
+
newMap.set(sid, next);
|
|
4913
|
+
return newMap;
|
|
4914
|
+
});
|
|
4915
|
+
}, [initialValue]);
|
|
4916
|
+
const setForScene = (0, import_react21.useCallback)((sceneId, value) => {
|
|
4917
|
+
setStateMap((prev) => {
|
|
4918
|
+
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
4919
|
+
const next = typeof value === "function" ? value(current) : value;
|
|
4920
|
+
const newMap = new Map(prev);
|
|
4921
|
+
newMap.set(sceneId, next);
|
|
4922
|
+
return newMap;
|
|
4923
|
+
});
|
|
4924
|
+
}, [initialValue]);
|
|
4925
|
+
return [currentValue, setForCurrentScene, setForScene];
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
// src/hooks/useAnySolo.ts
|
|
4929
|
+
var import_react22 = require("react");
|
|
4930
|
+
function useAnySolo(host) {
|
|
4931
|
+
const [anySolo, setAnySolo] = (0, import_react22.useState)(false);
|
|
4932
|
+
(0, import_react22.useEffect)(() => {
|
|
4933
|
+
let active = true;
|
|
4934
|
+
const refresh = () => {
|
|
4935
|
+
host.isAnySoloActive().then((v) => {
|
|
4936
|
+
if (active) setAnySolo(v);
|
|
4937
|
+
}).catch(() => {
|
|
4938
|
+
});
|
|
4939
|
+
};
|
|
4940
|
+
refresh();
|
|
4941
|
+
const unsub = host.onTrackStateChange(() => refresh());
|
|
4942
|
+
return () => {
|
|
4943
|
+
active = false;
|
|
4944
|
+
unsub();
|
|
4945
|
+
};
|
|
4946
|
+
}, [host]);
|
|
4947
|
+
return anySolo;
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
// src/hooks/useSoundHistory.ts
|
|
4951
|
+
var import_react23 = require("react");
|
|
4952
|
+
var EMPTY = { entries: [], cursor: -1 };
|
|
4953
|
+
function sameDescriptor(a, b) {
|
|
4954
|
+
if (a === b) return true;
|
|
4955
|
+
try {
|
|
4956
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
4957
|
+
} catch {
|
|
4958
|
+
return false;
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
function useSoundHistory(applySound, opts = {}) {
|
|
4962
|
+
const max = Math.max(2, opts.max ?? 24);
|
|
4963
|
+
const applyRef = (0, import_react23.useRef)(applySound);
|
|
4964
|
+
applyRef.current = applySound;
|
|
4965
|
+
const onChangeRef = (0, import_react23.useRef)(opts.onChange);
|
|
4966
|
+
onChangeRef.current = opts.onChange;
|
|
4967
|
+
const dataRef = (0, import_react23.useRef)({});
|
|
4968
|
+
const [, setVersion] = (0, import_react23.useState)(0);
|
|
4969
|
+
const bump = (0, import_react23.useCallback)(() => setVersion((v) => v + 1), []);
|
|
4970
|
+
const commit = (0, import_react23.useCallback)(
|
|
4971
|
+
(trackId, next, notify) => {
|
|
4972
|
+
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
4973
|
+
bump();
|
|
4974
|
+
if (notify) onChangeRef.current?.(trackId, next);
|
|
4975
|
+
},
|
|
4976
|
+
[bump]
|
|
4977
|
+
);
|
|
4978
|
+
const record = (0, import_react23.useCallback)(
|
|
4979
|
+
(trackId, descriptor, label) => {
|
|
4980
|
+
const h = dataRef.current[trackId];
|
|
4981
|
+
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
4982
|
+
if (current && sameDescriptor(current.descriptor, descriptor)) return;
|
|
4983
|
+
const entries = [...h ? h.entries : [], { descriptor, label }];
|
|
4984
|
+
while (entries.length > max) {
|
|
4985
|
+
const victim = entries.findIndex((e) => !e.favorite);
|
|
4986
|
+
if (victim === -1) break;
|
|
4987
|
+
entries.splice(victim, 1);
|
|
4988
|
+
}
|
|
4989
|
+
commit(trackId, { entries, cursor: entries.length - 1 }, true);
|
|
4990
|
+
},
|
|
4991
|
+
[max, commit]
|
|
4992
|
+
);
|
|
4993
|
+
const restoreTo = (0, import_react23.useCallback)(
|
|
4994
|
+
async (trackId, index) => {
|
|
4995
|
+
const h = dataRef.current[trackId];
|
|
4996
|
+
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
4997
|
+
await applyRef.current(trackId, h.entries[index].descriptor);
|
|
4998
|
+
commit(trackId, { entries: h.entries, cursor: index }, true);
|
|
4999
|
+
return true;
|
|
5000
|
+
},
|
|
5001
|
+
[commit]
|
|
5002
|
+
);
|
|
5003
|
+
const undo = (0, import_react23.useCallback)(
|
|
5004
|
+
(trackId) => {
|
|
5005
|
+
const h = dataRef.current[trackId];
|
|
5006
|
+
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
5007
|
+
return restoreTo(trackId, h.cursor - 1);
|
|
5008
|
+
},
|
|
5009
|
+
[restoreTo]
|
|
5010
|
+
);
|
|
5011
|
+
const toggleFavorite = (0, import_react23.useCallback)(
|
|
5012
|
+
(trackId, index) => {
|
|
5013
|
+
const h = dataRef.current[trackId];
|
|
5014
|
+
if (!h || index < 0 || index >= h.entries.length) return;
|
|
5015
|
+
const entries = h.entries.map((e, i) => i === index ? { ...e, favorite: !e.favorite } : e);
|
|
5016
|
+
commit(trackId, { entries, cursor: h.cursor }, true);
|
|
5017
|
+
},
|
|
5018
|
+
[commit]
|
|
5019
|
+
);
|
|
5020
|
+
const restore = (0, import_react23.useCallback)(
|
|
5021
|
+
(trackId, state) => {
|
|
5022
|
+
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
5023
|
+
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
5024
|
+
const cursor = entries.length === 0 ? -1 : Math.min(Math.max(raw, 0), entries.length - 1);
|
|
5025
|
+
commit(trackId, { entries, cursor }, false);
|
|
5026
|
+
},
|
|
5027
|
+
[commit]
|
|
5028
|
+
);
|
|
5029
|
+
const list = (0, import_react23.useCallback)(
|
|
5030
|
+
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
5031
|
+
[]
|
|
5032
|
+
);
|
|
5033
|
+
const canUndo = (0, import_react23.useCallback)((trackId) => {
|
|
5034
|
+
const h = dataRef.current[trackId];
|
|
5035
|
+
return !!h && h.cursor > 0;
|
|
5036
|
+
}, []);
|
|
5037
|
+
const clear = (0, import_react23.useCallback)(
|
|
5038
|
+
(trackId) => {
|
|
5039
|
+
if (dataRef.current[trackId]) {
|
|
5040
|
+
const next = { ...dataRef.current };
|
|
5041
|
+
delete next[trackId];
|
|
5042
|
+
dataRef.current = next;
|
|
5043
|
+
bump();
|
|
5044
|
+
}
|
|
5045
|
+
onChangeRef.current?.(trackId, EMPTY);
|
|
5046
|
+
},
|
|
5047
|
+
[bump]
|
|
5048
|
+
);
|
|
5049
|
+
const reset = (0, import_react23.useCallback)(() => {
|
|
5050
|
+
dataRef.current = {};
|
|
5051
|
+
bump();
|
|
5052
|
+
}, [bump]);
|
|
5053
|
+
return (0, import_react23.useMemo)(
|
|
5054
|
+
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
5055
|
+
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
5056
|
+
);
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
// src/panel-core/track-state.ts
|
|
5060
|
+
function newTrackState(handle, overrides = {}) {
|
|
5061
|
+
return {
|
|
5062
|
+
handle,
|
|
5063
|
+
prompt: "",
|
|
5064
|
+
role: "",
|
|
5065
|
+
runtimeState: { id: handle.id, muted: false, solo: false, volume: 0.75, pan: 0 },
|
|
5066
|
+
fxDetailState: { ...EMPTY_FX_DETAIL_STATE },
|
|
5067
|
+
drawerOpen: false,
|
|
5068
|
+
drawerTab: "fx",
|
|
5069
|
+
editorStage: false,
|
|
5070
|
+
isGenerating: false,
|
|
5071
|
+
error: null,
|
|
5072
|
+
hasMidi: false,
|
|
5073
|
+
generationProgress: 0,
|
|
5074
|
+
editNotes: [],
|
|
5075
|
+
editBars: 4,
|
|
5076
|
+
editBpm: 120,
|
|
5077
|
+
instrumentPluginId: handle.instrumentPluginId ?? null,
|
|
5078
|
+
instrumentName: handle.instrumentName ?? null,
|
|
5079
|
+
instrumentMissing: false,
|
|
5080
|
+
shuffleHistory: /* @__PURE__ */ new Set(),
|
|
5081
|
+
...overrides
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
// src/panel-core/panel-helpers.ts
|
|
5086
|
+
function trackDataKey(dbId, suffix) {
|
|
5087
|
+
return `track:${dbId}:${suffix}`;
|
|
5088
|
+
}
|
|
5089
|
+
function pluginFxToToggleFx(sdkState) {
|
|
5090
|
+
const result = { ...EMPTY_FX_DETAIL_STATE };
|
|
5091
|
+
for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
|
|
5092
|
+
const sdkCat = sdkState[category];
|
|
5093
|
+
if (sdkCat) {
|
|
5094
|
+
result[category] = {
|
|
5095
|
+
enabled: sdkCat.enabled,
|
|
5096
|
+
presetIndex: sdkCat.presetIndex,
|
|
5097
|
+
dryWet: sdkCat.dryWet
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
return result;
|
|
5102
|
+
}
|
|
5103
|
+
function parseLLMNoteResponse(content) {
|
|
5104
|
+
try {
|
|
5105
|
+
let jsonStr = content.trim();
|
|
5106
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
5107
|
+
if (fenceMatch) {
|
|
5108
|
+
jsonStr = fenceMatch[1].trim();
|
|
5109
|
+
}
|
|
5110
|
+
const parsed = JSON.parse(jsonStr);
|
|
5111
|
+
if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
|
|
5112
|
+
return null;
|
|
5113
|
+
}
|
|
5114
|
+
const obj = parsed;
|
|
5115
|
+
if (!Array.isArray(obj.notes)) {
|
|
5116
|
+
return null;
|
|
5117
|
+
}
|
|
5118
|
+
const validNotes = [];
|
|
5119
|
+
for (const raw of obj.notes) {
|
|
5120
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
5121
|
+
const note = raw;
|
|
5122
|
+
const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
|
|
5123
|
+
const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
|
|
5124
|
+
const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
|
|
5125
|
+
const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
|
|
5126
|
+
if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
|
|
5127
|
+
validNotes.push({
|
|
5128
|
+
pitch: Math.round(pitch),
|
|
5129
|
+
startBeat,
|
|
5130
|
+
durationBeats,
|
|
5131
|
+
velocity: Math.round(velocity)
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
const role = typeof obj.role === "string" ? obj.role : void 0;
|
|
5136
|
+
return { notes: validNotes, role };
|
|
5137
|
+
} catch {
|
|
5138
|
+
return null;
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
|
|
5142
|
+
// src/panel-core/group-meta.ts
|
|
5143
|
+
function parseTrackGroups(sceneData, spec) {
|
|
5144
|
+
const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
|
|
5145
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5146
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
5147
|
+
const match = pattern.exec(key);
|
|
5148
|
+
if (!match) continue;
|
|
5149
|
+
const meta = spec.asMeta(val);
|
|
5150
|
+
if (!meta) continue;
|
|
5151
|
+
const groupId = spec.groupIdOf(meta);
|
|
5152
|
+
const list = groups.get(groupId) ?? [];
|
|
5153
|
+
list.push({ dbId: match[1], meta });
|
|
5154
|
+
groups.set(groupId, list);
|
|
5155
|
+
}
|
|
5156
|
+
const out = [];
|
|
5157
|
+
for (const [groupId, members] of groups) {
|
|
5158
|
+
if (spec.sortMembers) members.sort(spec.sortMembers);
|
|
5159
|
+
out.push({ groupId, members });
|
|
5160
|
+
}
|
|
5161
|
+
return out;
|
|
5162
|
+
}
|
|
5163
|
+
function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
|
|
5164
|
+
const byDbId = /* @__PURE__ */ new Map();
|
|
5165
|
+
for (const t of tracks) byDbId.set(getDbId(t), t);
|
|
5166
|
+
const resolved = [];
|
|
5167
|
+
const memberDbIds = /* @__PURE__ */ new Set();
|
|
5168
|
+
const staleMemberDbIds = [];
|
|
5169
|
+
for (const parsed of parsedGroups) {
|
|
5170
|
+
const live = { groupId: parsed.groupId, members: [] };
|
|
5171
|
+
for (const member of parsed.members) {
|
|
5172
|
+
const track = byDbId.get(member.dbId);
|
|
5173
|
+
if (track) live.members.push({ dbId: member.dbId, meta: member.meta, track });
|
|
5174
|
+
else staleMemberDbIds.push(member.dbId);
|
|
5175
|
+
}
|
|
5176
|
+
if (live.members.length === 0) continue;
|
|
5177
|
+
const complete = opts.isComplete ? opts.isComplete(live, parsed) : live.members.length === parsed.members.length;
|
|
5178
|
+
if (!complete) continue;
|
|
5179
|
+
resolved.push(live);
|
|
5180
|
+
for (const m of live.members) memberDbIds.add(m.dbId);
|
|
5181
|
+
}
|
|
5182
|
+
return { resolved, memberDbIds, staleMemberDbIds };
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
// src/panel-core/useTransitionOps.ts
|
|
5186
|
+
var import_react24 = require("react");
|
|
5187
|
+
function useTransitionOps({
|
|
5188
|
+
host,
|
|
5189
|
+
adapter,
|
|
5190
|
+
activeSceneId,
|
|
5191
|
+
isConnected,
|
|
5192
|
+
isAuthenticated,
|
|
5193
|
+
sceneContext,
|
|
5194
|
+
tracks,
|
|
5195
|
+
setTracks,
|
|
5196
|
+
loadTracks,
|
|
5197
|
+
setCrossfadePairsMeta,
|
|
5198
|
+
setFadesMeta,
|
|
5199
|
+
resolvedCrossfadePairs,
|
|
5200
|
+
resolvedFades
|
|
5201
|
+
}) {
|
|
5202
|
+
const { identity } = adapter;
|
|
5203
|
+
const appliedFadeAutomationRef = (0, import_react24.useRef)(/* @__PURE__ */ new Set());
|
|
5204
|
+
const applyCrossfadeAutomation = (0, import_react24.useCallback)(
|
|
5205
|
+
async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
|
|
5206
|
+
if (host.setTrackVolumeAutomation) {
|
|
5207
|
+
const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
|
|
5208
|
+
await host.setTrackVolumeAutomation(originTrackId, curves.origin).catch(() => {
|
|
5209
|
+
});
|
|
5210
|
+
await host.setTrackVolumeAutomation(targetTrackId, curves.target).catch(() => {
|
|
5211
|
+
});
|
|
5212
|
+
} else {
|
|
5213
|
+
await host.setTrackVolume(originTrackId, EQUAL_POWER_GAIN).catch(() => {
|
|
5214
|
+
});
|
|
5215
|
+
await host.setTrackVolume(targetTrackId, EQUAL_POWER_GAIN).catch(() => {
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
5218
|
+
},
|
|
5219
|
+
[host]
|
|
5220
|
+
);
|
|
5221
|
+
const applyFadeAutomation = (0, import_react24.useCallback)(
|
|
5222
|
+
async (trackId, direction, bars, bpm, sliderPos, gesture) => {
|
|
5223
|
+
if (!host.setTrackVolumeAutomation) return;
|
|
5224
|
+
const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
|
|
5225
|
+
await host.setTrackVolumeAutomation(trackId, points).catch(() => {
|
|
5226
|
+
});
|
|
5227
|
+
},
|
|
5228
|
+
[host]
|
|
5229
|
+
);
|
|
5230
|
+
const [isCreatingCrossfade, setIsCreatingCrossfade] = (0, import_react24.useState)(false);
|
|
5231
|
+
const handleCreateCrossfade = (0, import_react24.useCallback)(
|
|
5232
|
+
async (origin, target) => {
|
|
5233
|
+
const scene = activeSceneId;
|
|
5234
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
5235
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
5236
|
+
if (!scene) throw new Error("No active scene.");
|
|
5237
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
5238
|
+
if (!isAuthenticated) throw new Error("Please sign in to generate the bridge.");
|
|
5239
|
+
if (tracks.length + 2 > identity.maxTracks) {
|
|
5240
|
+
throw new Error("Not enough track slots for a crossfade.");
|
|
5241
|
+
}
|
|
5242
|
+
setIsCreatingCrossfade(true);
|
|
5243
|
+
const created = [];
|
|
5244
|
+
try {
|
|
5245
|
+
const role = target.role ?? origin.role ?? "";
|
|
5246
|
+
const mc = await host.getMusicalContext();
|
|
5247
|
+
const [originMidi, targetMidi, originKey, targetKey] = await Promise.all([
|
|
5248
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(origin.dbId) : Promise.resolve({ clips: [] }),
|
|
5249
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(target.dbId) : Promise.resolve({ clips: [] }),
|
|
5250
|
+
host.getSceneKey ? host.getSceneKey(fromSceneId) : Promise.resolve(null),
|
|
5251
|
+
host.getSceneKey ? host.getSceneKey(toSceneId) : Promise.resolve(null)
|
|
5252
|
+
]);
|
|
5253
|
+
const userPrompt = buildCrossfadeInpaintPrompt({
|
|
5254
|
+
role,
|
|
5255
|
+
bars: mc.bars,
|
|
5256
|
+
originName: origin.name,
|
|
5257
|
+
targetName: target.name,
|
|
5258
|
+
originKey: originKey ? `${originKey.key} ${originKey.mode}` : null,
|
|
5259
|
+
targetKey: targetKey ? `${targetKey.key} ${targetKey.mode}` : null,
|
|
5260
|
+
originNotes: originMidi.clips[0]?.notes ?? [],
|
|
5261
|
+
targetNotes: targetMidi.clips[0]?.notes ?? []
|
|
5262
|
+
});
|
|
5263
|
+
const llm = await host.generateWithLLM({
|
|
5264
|
+
system: adapter.buildSystemPrompt(host.getValidRoles()),
|
|
5265
|
+
user: userPrompt,
|
|
5266
|
+
responseFormat: "json"
|
|
5267
|
+
});
|
|
5268
|
+
const parsed = adapter.parseNotesResponse(llm.content);
|
|
5269
|
+
if (!parsed || parsed.notes.length === 0) {
|
|
5270
|
+
throw new Error("The bridge generator returned no notes.");
|
|
5271
|
+
}
|
|
5272
|
+
const notes = await host.postProcessMidi(parsed.notes, {
|
|
5273
|
+
quantize: true,
|
|
5274
|
+
removeOverlaps: true
|
|
5275
|
+
});
|
|
5276
|
+
const clip = {
|
|
5277
|
+
startTime: 0,
|
|
5278
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5279
|
+
tempo: mc.bpm,
|
|
5280
|
+
notes
|
|
5281
|
+
};
|
|
5282
|
+
const top = await host.createTrack({
|
|
5283
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-xf-o`,
|
|
5284
|
+
...adapter.createTrackOptions()
|
|
5285
|
+
});
|
|
5286
|
+
created.push(top);
|
|
5287
|
+
const bottom = await host.createTrack({
|
|
5288
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-xf-t`,
|
|
5289
|
+
...adapter.createTrackOptions()
|
|
5290
|
+
});
|
|
5291
|
+
created.push(bottom);
|
|
5292
|
+
if (role) {
|
|
5293
|
+
await host.setTrackRole(top.id, role).catch(() => {
|
|
5294
|
+
});
|
|
5295
|
+
await host.setTrackRole(bottom.id, role).catch(() => {
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
await host.writeMidiClip(top.id, clip);
|
|
5299
|
+
await host.writeMidiClip(bottom.id, clip);
|
|
5300
|
+
const copySound = async (newTrackId, sourceDbId) => {
|
|
5301
|
+
if (!host.getTrackSound) return "default";
|
|
5302
|
+
const snap = await host.getTrackSound(sourceDbId);
|
|
5303
|
+
if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) return "default";
|
|
5304
|
+
return adapter.sound.copySnapshot(newTrackId, snap);
|
|
5305
|
+
};
|
|
5306
|
+
const originLabel = await copySound(top.id, origin.dbId);
|
|
5307
|
+
const targetLabel = await copySound(bottom.id, target.dbId);
|
|
5308
|
+
await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
|
|
5309
|
+
const groupId = top.dbId;
|
|
5310
|
+
const originMeta = {
|
|
5311
|
+
groupId,
|
|
5312
|
+
slot: "origin",
|
|
5313
|
+
partnerDbId: bottom.dbId,
|
|
5314
|
+
sourceTrackDbId: origin.dbId,
|
|
5315
|
+
sourceSceneId: fromSceneId,
|
|
5316
|
+
sourceName: origin.name,
|
|
5317
|
+
soundLabel: originLabel,
|
|
5318
|
+
sliderPos: 0.5
|
|
5319
|
+
};
|
|
5320
|
+
const targetMeta = {
|
|
5321
|
+
groupId,
|
|
5322
|
+
slot: "target",
|
|
5323
|
+
partnerDbId: top.dbId,
|
|
5324
|
+
sourceTrackDbId: target.dbId,
|
|
5325
|
+
sourceSceneId: toSceneId,
|
|
5326
|
+
sourceName: target.name,
|
|
5327
|
+
soundLabel: targetLabel,
|
|
5328
|
+
sliderPos: 0.5
|
|
5329
|
+
};
|
|
5330
|
+
await host.setSceneData(scene, `track:${top.dbId}:crossfade`, originMeta);
|
|
5331
|
+
await host.setSceneData(scene, `track:${bottom.dbId}:crossfade`, targetMeta);
|
|
5332
|
+
await loadTracks(true);
|
|
5333
|
+
host.showToast("success", "Crossfade created", `${origin.name} \u2192 ${target.name}`);
|
|
5334
|
+
} catch (err) {
|
|
5335
|
+
for (const h of [...created].reverse()) {
|
|
5336
|
+
try {
|
|
5337
|
+
await host.deleteTrack(h.id);
|
|
5338
|
+
} catch {
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
5342
|
+
} finally {
|
|
5343
|
+
setIsCreatingCrossfade(false);
|
|
5344
|
+
}
|
|
5345
|
+
},
|
|
5346
|
+
[
|
|
5347
|
+
host,
|
|
5348
|
+
adapter,
|
|
5349
|
+
identity,
|
|
5350
|
+
activeSceneId,
|
|
5351
|
+
isConnected,
|
|
5352
|
+
isAuthenticated,
|
|
5353
|
+
tracks.length,
|
|
5354
|
+
sceneContext,
|
|
5355
|
+
applyCrossfadeAutomation,
|
|
5356
|
+
loadTracks
|
|
5357
|
+
]
|
|
5358
|
+
);
|
|
5359
|
+
const [isCreatingFade, setIsCreatingFade] = (0, import_react24.useState)(false);
|
|
5360
|
+
const handleCreateFade = (0, import_react24.useCallback)(
|
|
5361
|
+
async (selection, direction, gesture) => {
|
|
5362
|
+
const scene = activeSceneId;
|
|
5363
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
5364
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
5365
|
+
if (!scene) throw new Error("No active scene.");
|
|
5366
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
5367
|
+
if (!isAuthenticated) throw new Error("Please sign in to generate the fade.");
|
|
5368
|
+
if (tracks.length + 1 > identity.maxTracks) {
|
|
5369
|
+
throw new Error("Not enough track slots for a fade.");
|
|
5370
|
+
}
|
|
5371
|
+
setIsCreatingFade(true);
|
|
5372
|
+
const created = [];
|
|
5373
|
+
try {
|
|
5374
|
+
const role = selection.role ?? "";
|
|
5375
|
+
const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
|
|
5376
|
+
const mc = await host.getMusicalContext();
|
|
5377
|
+
const [srcMidi, srcKey] = await Promise.all([
|
|
5378
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(selection.dbId) : Promise.resolve({ clips: [] }),
|
|
5379
|
+
host.getSceneKey ? host.getSceneKey(sourceSceneId) : Promise.resolve(null)
|
|
5380
|
+
]);
|
|
5381
|
+
const srcNotes = srcMidi.clips[0]?.notes ?? [];
|
|
5382
|
+
const keyStr = srcKey ? `${srcKey.key} ${srcKey.mode}` : null;
|
|
5383
|
+
const userPrompt = buildCrossfadeInpaintPrompt({
|
|
5384
|
+
role,
|
|
5385
|
+
bars: mc.bars,
|
|
5386
|
+
originName: direction === "out" ? selection.name : "silence",
|
|
5387
|
+
targetName: direction === "in" ? selection.name : "silence",
|
|
5388
|
+
originKey: direction === "out" ? keyStr : null,
|
|
5389
|
+
targetKey: direction === "in" ? keyStr : null,
|
|
5390
|
+
originNotes: direction === "out" ? srcNotes : [],
|
|
5391
|
+
targetNotes: direction === "in" ? srcNotes : []
|
|
5392
|
+
});
|
|
5393
|
+
const llm = await host.generateWithLLM({
|
|
5394
|
+
system: adapter.buildSystemPrompt(host.getValidRoles()),
|
|
5395
|
+
user: userPrompt,
|
|
5396
|
+
responseFormat: "json"
|
|
5397
|
+
});
|
|
5398
|
+
const parsed = adapter.parseNotesResponse(llm.content);
|
|
5399
|
+
if (!parsed || parsed.notes.length === 0) {
|
|
5400
|
+
throw new Error("The fade generator returned no notes.");
|
|
5401
|
+
}
|
|
5402
|
+
const notes = await host.postProcessMidi(parsed.notes, {
|
|
5403
|
+
quantize: true,
|
|
5404
|
+
removeOverlaps: true
|
|
5405
|
+
});
|
|
5406
|
+
const clip = {
|
|
5407
|
+
startTime: 0,
|
|
5408
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5409
|
+
tempo: mc.bpm,
|
|
5410
|
+
notes
|
|
5411
|
+
};
|
|
5412
|
+
const track = await host.createTrack({
|
|
5413
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}`,
|
|
5414
|
+
...adapter.createTrackOptions()
|
|
5415
|
+
});
|
|
5416
|
+
created.push(track);
|
|
5417
|
+
if (role) await host.setTrackRole(track.id, role).catch(() => {
|
|
5418
|
+
});
|
|
5419
|
+
await host.writeMidiClip(track.id, clip);
|
|
5420
|
+
let soundLabel = "default";
|
|
5421
|
+
if (host.getTrackSound) {
|
|
5422
|
+
const snap = await host.getTrackSound(selection.dbId);
|
|
5423
|
+
if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
|
|
5424
|
+
soundLabel = await adapter.sound.copySnapshot(track.id, snap);
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
|
|
5428
|
+
appliedFadeAutomationRef.current.add(track.id);
|
|
5429
|
+
const meta = {
|
|
5430
|
+
direction,
|
|
5431
|
+
gesture,
|
|
5432
|
+
sourceTrackDbId: selection.dbId,
|
|
5433
|
+
sourceSceneId,
|
|
5434
|
+
sourceName: selection.name,
|
|
5435
|
+
soundLabel,
|
|
5436
|
+
sliderPos: 0.5
|
|
5437
|
+
};
|
|
5438
|
+
await host.setSceneData(scene, `track:${track.dbId}:fade`, meta);
|
|
5439
|
+
await loadTracks(true);
|
|
5440
|
+
host.showToast(
|
|
5441
|
+
"success",
|
|
5442
|
+
direction === "in" ? "Fade in created" : "Fade out created",
|
|
5443
|
+
selection.name
|
|
5444
|
+
);
|
|
5445
|
+
} catch (err) {
|
|
5446
|
+
for (const h of [...created].reverse()) {
|
|
5447
|
+
try {
|
|
5448
|
+
await host.deleteTrack(h.id);
|
|
5449
|
+
} catch {
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
5453
|
+
} finally {
|
|
5454
|
+
setIsCreatingFade(false);
|
|
5455
|
+
}
|
|
5456
|
+
},
|
|
5457
|
+
[
|
|
5458
|
+
host,
|
|
5459
|
+
adapter,
|
|
5460
|
+
identity,
|
|
5461
|
+
activeSceneId,
|
|
5462
|
+
isConnected,
|
|
5463
|
+
isAuthenticated,
|
|
5464
|
+
tracks.length,
|
|
5465
|
+
sceneContext,
|
|
5466
|
+
applyFadeAutomation,
|
|
5467
|
+
loadTracks
|
|
5468
|
+
]
|
|
5469
|
+
);
|
|
5470
|
+
const handleCrossfadeMute = (0, import_react24.useCallback)(
|
|
5471
|
+
(pair) => {
|
|
5472
|
+
const newMuted = !pair.origin.runtimeState.muted;
|
|
5473
|
+
for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
|
|
5474
|
+
setTracks(
|
|
5475
|
+
(prev) => prev.map(
|
|
5476
|
+
(t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
|
|
5477
|
+
)
|
|
5478
|
+
);
|
|
5479
|
+
host.setTrackMute(id, newMuted).catch(() => {
|
|
5480
|
+
});
|
|
5481
|
+
}
|
|
5482
|
+
},
|
|
5483
|
+
[host, setTracks]
|
|
5484
|
+
);
|
|
5485
|
+
const handleCrossfadeSolo = (0, import_react24.useCallback)(
|
|
5486
|
+
(pair) => {
|
|
5487
|
+
const newSolo = !pair.origin.runtimeState.solo;
|
|
5488
|
+
for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
|
|
5489
|
+
setTracks(
|
|
5490
|
+
(prev) => prev.map(
|
|
5491
|
+
(t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
|
|
5492
|
+
)
|
|
5493
|
+
);
|
|
5494
|
+
host.setTrackSolo(id, newSolo).catch(() => {
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
},
|
|
5498
|
+
[host, setTracks]
|
|
5499
|
+
);
|
|
5500
|
+
const handleCrossfadeDelete = (0, import_react24.useCallback)(
|
|
5501
|
+
async (pair) => {
|
|
5502
|
+
try {
|
|
5503
|
+
for (const member of [pair.origin, pair.target]) {
|
|
5504
|
+
await host.deleteTrack(member.handle.id);
|
|
5505
|
+
if (activeSceneId) {
|
|
5506
|
+
await host.deleteSceneData(activeSceneId, `track:${member.handle.dbId}:crossfade`);
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
setCrossfadePairsMeta((prev) => prev.filter((p) => p.groupId !== pair.groupId));
|
|
5510
|
+
setTracks(
|
|
5511
|
+
(prev) => prev.filter(
|
|
5512
|
+
(t) => t.handle.id !== pair.origin.handle.id && t.handle.id !== pair.target.handle.id
|
|
5513
|
+
)
|
|
5514
|
+
);
|
|
5515
|
+
host.showToast("success", "Crossfade removed");
|
|
5516
|
+
} catch (err) {
|
|
5517
|
+
host.showToast(
|
|
5518
|
+
"error",
|
|
5519
|
+
"Failed to delete crossfade",
|
|
5520
|
+
err instanceof Error ? err.message : String(err)
|
|
5521
|
+
);
|
|
5522
|
+
}
|
|
5523
|
+
},
|
|
5524
|
+
[host, activeSceneId, setCrossfadePairsMeta, setTracks]
|
|
5525
|
+
);
|
|
5526
|
+
const crossfadeSliderTimers = (0, import_react24.useRef)({});
|
|
5527
|
+
const handleCrossfadeSlider = (0, import_react24.useCallback)(
|
|
5528
|
+
(pair, pos) => {
|
|
5529
|
+
setCrossfadePairsMeta(
|
|
5530
|
+
(prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
|
|
5531
|
+
);
|
|
5532
|
+
if (crossfadeSliderTimers.current[pair.groupId]) {
|
|
5533
|
+
clearTimeout(crossfadeSliderTimers.current[pair.groupId]);
|
|
5534
|
+
}
|
|
5535
|
+
crossfadeSliderTimers.current[pair.groupId] = setTimeout(() => {
|
|
5536
|
+
void (async () => {
|
|
5537
|
+
const mc = await host.getMusicalContext();
|
|
5538
|
+
await applyCrossfadeAutomation(
|
|
5539
|
+
pair.origin.handle.id,
|
|
5540
|
+
pair.target.handle.id,
|
|
5541
|
+
mc.bars,
|
|
5542
|
+
mc.bpm,
|
|
5543
|
+
pos
|
|
5544
|
+
);
|
|
5545
|
+
if (activeSceneId) {
|
|
5546
|
+
const sceneData = await host.getAllSceneData(activeSceneId);
|
|
5547
|
+
for (const dbId of [pair.originDbId, pair.targetDbId]) {
|
|
5548
|
+
const meta = asCrossfadeMeta(sceneData[`track:${dbId}:crossfade`]);
|
|
5549
|
+
if (meta) {
|
|
5550
|
+
host.setSceneData(activeSceneId, `track:${dbId}:crossfade`, { ...meta, sliderPos: pos }).catch(() => {
|
|
5551
|
+
});
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
})();
|
|
5556
|
+
}, 200);
|
|
5557
|
+
},
|
|
5558
|
+
[host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
|
|
5559
|
+
);
|
|
5560
|
+
const handleFadeDelete = (0, import_react24.useCallback)(
|
|
5561
|
+
async (fade) => {
|
|
5562
|
+
try {
|
|
5563
|
+
await host.deleteTrack(fade.track.handle.id);
|
|
5564
|
+
if (activeSceneId) {
|
|
5565
|
+
await host.deleteSceneData(activeSceneId, `track:${fade.dbId}:fade`);
|
|
5566
|
+
}
|
|
5567
|
+
setFadesMeta((prev) => prev.filter((f) => f.dbId !== fade.dbId));
|
|
5568
|
+
setTracks((prev) => prev.filter((t) => t.handle.id !== fade.track.handle.id));
|
|
5569
|
+
host.showToast("success", "Fade removed");
|
|
5570
|
+
} catch (err) {
|
|
5571
|
+
host.showToast(
|
|
5572
|
+
"error",
|
|
5573
|
+
"Failed to delete fade",
|
|
5574
|
+
err instanceof Error ? err.message : String(err)
|
|
5575
|
+
);
|
|
5576
|
+
}
|
|
5577
|
+
},
|
|
5578
|
+
[host, activeSceneId, setFadesMeta, setTracks]
|
|
5579
|
+
);
|
|
5580
|
+
const fadeSliderTimers = (0, import_react24.useRef)({});
|
|
5581
|
+
const handleFadeSlider = (0, import_react24.useCallback)(
|
|
5582
|
+
(fade, pos) => {
|
|
5583
|
+
setFadesMeta(
|
|
5584
|
+
(prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
|
|
5585
|
+
);
|
|
5586
|
+
if (fadeSliderTimers.current[fade.dbId]) clearTimeout(fadeSliderTimers.current[fade.dbId]);
|
|
5587
|
+
fadeSliderTimers.current[fade.dbId] = setTimeout(() => {
|
|
5588
|
+
void (async () => {
|
|
5589
|
+
const mc = await host.getMusicalContext();
|
|
5590
|
+
await applyFadeAutomation(
|
|
5591
|
+
fade.track.handle.id,
|
|
5592
|
+
fade.meta.direction,
|
|
5593
|
+
mc.bars,
|
|
5594
|
+
mc.bpm,
|
|
5595
|
+
pos,
|
|
5596
|
+
fade.meta.gesture
|
|
5597
|
+
);
|
|
5598
|
+
if (activeSceneId) {
|
|
5599
|
+
const sceneData = await host.getAllSceneData(activeSceneId);
|
|
5600
|
+
const meta = asFadeMeta(sceneData[`track:${fade.dbId}:fade`]);
|
|
5601
|
+
if (meta) {
|
|
5602
|
+
host.setSceneData(activeSceneId, `track:${fade.dbId}:fade`, { ...meta, sliderPos: pos }).catch(() => {
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
})();
|
|
5607
|
+
}, 200);
|
|
5608
|
+
},
|
|
5609
|
+
[host, activeSceneId, applyFadeAutomation, setFadesMeta]
|
|
5610
|
+
);
|
|
5611
|
+
const lastResyncKeyRef = (0, import_react24.useRef)("");
|
|
5612
|
+
(0, import_react24.useEffect)(() => {
|
|
5613
|
+
if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
|
|
5614
|
+
return;
|
|
5615
|
+
}
|
|
5616
|
+
const resyncKey = [
|
|
5617
|
+
...resolvedCrossfadePairs.map(
|
|
5618
|
+
(p) => `${p.origin.handle.dbId}<${p.originSourceDbId}|${p.target.handle.dbId}<${p.targetSourceDbId}`
|
|
5619
|
+
),
|
|
5620
|
+
...resolvedFades.map((f) => `${f.track.handle.dbId}<${f.meta.sourceTrackDbId}`)
|
|
5621
|
+
].join(",");
|
|
5622
|
+
if (resyncKey === lastResyncKeyRef.current) return;
|
|
5623
|
+
lastResyncKeyRef.current = resyncKey;
|
|
5624
|
+
let cancelled = false;
|
|
5625
|
+
const reapplyIfDrifted = async (layerTrackId, layerDbId, sourceDbId) => {
|
|
5626
|
+
if (!host.getTrackSound || cancelled) return;
|
|
5627
|
+
const [sourceSnap, layerSnap] = await Promise.all([
|
|
5628
|
+
host.getTrackSound(sourceDbId),
|
|
5629
|
+
host.getTrackSound(layerDbId)
|
|
5630
|
+
]);
|
|
5631
|
+
if (cancelled || !sourceSnap || sourceSnap.kind !== adapter.sound.acceptedSnapshotKind) {
|
|
5632
|
+
return;
|
|
5633
|
+
}
|
|
5634
|
+
if (soundIdentity(sourceSnap) === soundIdentity(layerSnap)) return;
|
|
5635
|
+
try {
|
|
5636
|
+
await adapter.sound.copySnapshot(layerTrackId, sourceSnap);
|
|
5637
|
+
} catch {
|
|
5638
|
+
}
|
|
5639
|
+
};
|
|
5640
|
+
void (async () => {
|
|
5641
|
+
for (const pair of resolvedCrossfadePairs) {
|
|
5642
|
+
await reapplyIfDrifted(pair.origin.handle.id, pair.origin.handle.dbId, pair.originSourceDbId);
|
|
5643
|
+
await reapplyIfDrifted(pair.target.handle.id, pair.target.handle.dbId, pair.targetSourceDbId);
|
|
5644
|
+
}
|
|
5645
|
+
for (const fade of resolvedFades) {
|
|
5646
|
+
await reapplyIfDrifted(fade.track.handle.id, fade.track.handle.dbId, fade.meta.sourceTrackDbId);
|
|
5647
|
+
}
|
|
5648
|
+
})();
|
|
5649
|
+
return () => {
|
|
5650
|
+
cancelled = true;
|
|
5651
|
+
};
|
|
5652
|
+
}, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
|
|
5653
|
+
(0, import_react24.useEffect)(() => {
|
|
5654
|
+
if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
|
|
5655
|
+
void (async () => {
|
|
5656
|
+
const mc = await host.getMusicalContext();
|
|
5657
|
+
for (const fade of resolvedFades) {
|
|
5658
|
+
const id = fade.track.handle.id;
|
|
5659
|
+
if (appliedFadeAutomationRef.current.has(id)) continue;
|
|
5660
|
+
appliedFadeAutomationRef.current.add(id);
|
|
5661
|
+
await applyFadeAutomation(
|
|
5662
|
+
id,
|
|
5663
|
+
fade.meta.direction,
|
|
5664
|
+
mc.bars,
|
|
5665
|
+
mc.bpm,
|
|
5666
|
+
fade.meta.sliderPos,
|
|
5667
|
+
fade.meta.gesture
|
|
5668
|
+
);
|
|
5669
|
+
}
|
|
5670
|
+
})();
|
|
5671
|
+
}, [resolvedFades, host, applyFadeAutomation]);
|
|
5672
|
+
return {
|
|
5673
|
+
isCreatingCrossfade,
|
|
5674
|
+
isCreatingFade,
|
|
5675
|
+
handleCreateCrossfade,
|
|
5676
|
+
handleCreateFade,
|
|
5677
|
+
handleCrossfadeMute,
|
|
5678
|
+
handleCrossfadeSolo,
|
|
5679
|
+
handleCrossfadeDelete,
|
|
5680
|
+
handleCrossfadeSlider,
|
|
5681
|
+
handleFadeDelete,
|
|
5682
|
+
handleFadeSlider
|
|
4152
5683
|
};
|
|
4153
5684
|
}
|
|
4154
5685
|
|
|
4155
|
-
// src/
|
|
4156
|
-
var
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
const
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
5686
|
+
// src/panel-core/useGeneratorPanelCore.tsx
|
|
5687
|
+
var import_jsx_runtime23 = require("react/jsx-runtime");
|
|
5688
|
+
var EMPTY_PLACEHOLDERS = [];
|
|
5689
|
+
function useGeneratorPanelCore({
|
|
5690
|
+
ui,
|
|
5691
|
+
adapter
|
|
5692
|
+
}) {
|
|
5693
|
+
const {
|
|
5694
|
+
host,
|
|
5695
|
+
activeSceneId,
|
|
5696
|
+
isAuthenticated,
|
|
5697
|
+
isConnected,
|
|
5698
|
+
onHeaderContent,
|
|
5699
|
+
onLoading,
|
|
5700
|
+
sceneContext,
|
|
5701
|
+
onOpenContract,
|
|
5702
|
+
onExpandSelf,
|
|
5703
|
+
isExpanded
|
|
5704
|
+
} = ui;
|
|
5705
|
+
const { identity, features } = adapter;
|
|
5706
|
+
const logTag = identity.logTag;
|
|
5707
|
+
const adapterRef = (0, import_react25.useRef)(adapter);
|
|
5708
|
+
(0, import_react25.useEffect)(() => {
|
|
5709
|
+
if (adapterRef.current !== adapter) {
|
|
5710
|
+
adapterRef.current = adapter;
|
|
5711
|
+
console.warn(
|
|
5712
|
+
`[${logTag}] GeneratorPanelAdapter identity changed between renders \u2014 wrap it in useMemo(() => createAdapter(host), [host]) to avoid load loops.`
|
|
5713
|
+
);
|
|
5714
|
+
}
|
|
5715
|
+
}, [adapter, logTag]);
|
|
5716
|
+
const supportsMeters = typeof host.getTrackLevels === "function";
|
|
5717
|
+
const trackLevels = useTrackLevels(host, isExpanded);
|
|
5718
|
+
const [tracks, setTracks] = (0, import_react25.useState)([]);
|
|
5719
|
+
const [isLoadingTracks, setIsLoadingTracks] = (0, import_react25.useState)(false);
|
|
5720
|
+
const [importOpen, setImportOpen] = (0, import_react25.useState)(false);
|
|
5721
|
+
const [soundImportTarget, setSoundImportTarget] = (0, import_react25.useState)(null);
|
|
5722
|
+
const [designerView, setDesignerView] = (0, import_react25.useState)(false);
|
|
5723
|
+
const [transitionSourceTotal, setTransitionSourceTotal] = (0, import_react25.useState)(0);
|
|
5724
|
+
const [crossfadePairsMeta, setCrossfadePairsMeta] = (0, import_react25.useState)([]);
|
|
5725
|
+
const [fadesMeta, setFadesMeta] = (0, import_react25.useState)([]);
|
|
5726
|
+
const [genericGroupMetas, setGenericGroupMetas] = (0, import_react25.useState)({});
|
|
5727
|
+
const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
|
|
5728
|
+
const [placeholders, , setPlaceholdersForScene] = useSceneState(
|
|
5729
|
+
activeSceneId,
|
|
5730
|
+
EMPTY_PLACEHOLDERS
|
|
5731
|
+
);
|
|
5732
|
+
const saveTimeoutRefs = (0, import_react25.useRef)({});
|
|
5733
|
+
const editLoadStartedRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
5734
|
+
const [availableInstruments, setAvailableInstruments] = (0, import_react25.useState)([]);
|
|
5735
|
+
const [instrumentsLoading, setInstrumentsLoading] = (0, import_react25.useState)(false);
|
|
5736
|
+
const engineToDbIdRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
5737
|
+
const tracksLoadedForSceneRef = (0, import_react25.useRef)(null);
|
|
5738
|
+
const persistSoundHistory = (0, import_react25.useCallback)(
|
|
5739
|
+
(trackId, state) => {
|
|
5740
|
+
if (!activeSceneId) return;
|
|
5741
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
5742
|
+
host.setSceneData(activeSceneId, trackDataKey(dbId, "soundHistory"), state).catch(() => {
|
|
5743
|
+
});
|
|
5744
|
+
},
|
|
5745
|
+
[host, activeSceneId]
|
|
5746
|
+
);
|
|
5747
|
+
const soundHistory = useSoundHistory(adapter.sound.applySound, {
|
|
5748
|
+
max: adapter.sound.historyMax,
|
|
5749
|
+
onChange: persistSoundHistory
|
|
5750
|
+
});
|
|
5751
|
+
const anySolo = useAnySolo(host);
|
|
5752
|
+
const reorder = useTrackReorder({
|
|
5753
|
+
host,
|
|
5754
|
+
items: tracks,
|
|
5755
|
+
setItems: setTracks,
|
|
5756
|
+
getId: (t) => t.handle.dbId
|
|
5757
|
+
});
|
|
5758
|
+
const loadTracks = (0, import_react25.useCallback)(
|
|
5759
|
+
async (incremental = false) => {
|
|
5760
|
+
const sceneAtStart = activeSceneId;
|
|
5761
|
+
if (!sceneAtStart) {
|
|
5762
|
+
setTracks([]);
|
|
5763
|
+
setCrossfadePairsMeta([]);
|
|
5764
|
+
setFadesMeta([]);
|
|
5765
|
+
setGenericGroupMetas({});
|
|
5766
|
+
tracksLoadedForSceneRef.current = null;
|
|
5767
|
+
setIsLoadingTracks(false);
|
|
5768
|
+
return;
|
|
5769
|
+
}
|
|
5770
|
+
if (!incremental && tracksLoadedForSceneRef.current !== sceneAtStart) {
|
|
5771
|
+
setTracks([]);
|
|
5772
|
+
}
|
|
5773
|
+
tracksLoadedForSceneRef.current = sceneAtStart;
|
|
5774
|
+
if (!incremental) soundHistory.reset();
|
|
5775
|
+
const isStale = () => tracksLoadedForSceneRef.current !== sceneAtStart;
|
|
5776
|
+
if (!incremental) setIsLoadingTracks(true);
|
|
5777
|
+
try {
|
|
5778
|
+
await host.adoptSceneTracks();
|
|
5779
|
+
if (isStale()) return;
|
|
5780
|
+
const handles = await host.getPluginTracks();
|
|
5781
|
+
if (isStale()) return;
|
|
5782
|
+
const sceneData = await host.getAllSceneData(sceneAtStart);
|
|
5783
|
+
if (isStale()) return;
|
|
5784
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
5785
|
+
for (const h of handles) {
|
|
5786
|
+
idMap.set(h.id, h.dbId);
|
|
5787
|
+
}
|
|
5788
|
+
engineToDbIdRef.current = idMap;
|
|
5789
|
+
const trackStates = [];
|
|
5790
|
+
for (const handle of handles) {
|
|
5791
|
+
let runtimeState = {
|
|
5792
|
+
id: handle.id,
|
|
5793
|
+
muted: false,
|
|
5794
|
+
solo: false,
|
|
5795
|
+
volume: 0.75,
|
|
5796
|
+
pan: 0
|
|
5797
|
+
};
|
|
5798
|
+
let hasMidi = false;
|
|
5799
|
+
try {
|
|
5800
|
+
const info = await host.getTrackInfo(handle.id);
|
|
5801
|
+
runtimeState = {
|
|
5802
|
+
id: handle.id,
|
|
5803
|
+
muted: info.muted,
|
|
5804
|
+
solo: info.soloed,
|
|
5805
|
+
volume: info.volume,
|
|
5806
|
+
pan: info.pan
|
|
5807
|
+
};
|
|
5808
|
+
hasMidi = info.hasMidi;
|
|
5809
|
+
} catch {
|
|
5810
|
+
}
|
|
5811
|
+
let fxDetailState = newTrackState(handle).fxDetailState;
|
|
5812
|
+
try {
|
|
5813
|
+
const fxState = await host.getTrackFxState(handle.id);
|
|
5814
|
+
fxDetailState = pluginFxToToggleFx(fxState);
|
|
5815
|
+
} catch {
|
|
5816
|
+
}
|
|
5817
|
+
const promptKey = trackDataKey(handle.dbId, "prompt");
|
|
5818
|
+
let prompt = typeof sceneData[promptKey] === "string" ? sceneData[promptKey] : "";
|
|
5819
|
+
if (!prompt && handle.prompt) {
|
|
5820
|
+
prompt = handle.prompt;
|
|
5821
|
+
host.setSceneData(sceneAtStart, promptKey, prompt).catch(() => {
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
if (!hasMidi && handle.role) {
|
|
5825
|
+
hasMidi = true;
|
|
5826
|
+
}
|
|
5827
|
+
let instrumentMissing = false;
|
|
5828
|
+
if (handle.instrumentPluginId) {
|
|
5829
|
+
try {
|
|
5830
|
+
const instrDescriptor = await host.getTrackInstrument(handle.id);
|
|
5831
|
+
if (instrDescriptor?.missing) {
|
|
5832
|
+
instrumentMissing = true;
|
|
5833
|
+
}
|
|
5834
|
+
} catch {
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
trackStates.push(
|
|
5838
|
+
newTrackState(handle, {
|
|
5839
|
+
prompt,
|
|
5840
|
+
role: handle.role ?? "",
|
|
5841
|
+
runtimeState,
|
|
5842
|
+
fxDetailState,
|
|
5843
|
+
hasMidi,
|
|
5844
|
+
instrumentMissing
|
|
5845
|
+
})
|
|
5846
|
+
);
|
|
5847
|
+
}
|
|
5848
|
+
if (isStale()) return;
|
|
5849
|
+
setTracks((prev) => {
|
|
5850
|
+
const prevByDbId = new Map(prev.map((p) => [p.handle.dbId, p]));
|
|
5851
|
+
return trackStates.map((ts) => {
|
|
5852
|
+
const carry = prevByDbId.get(ts.handle.dbId);
|
|
5853
|
+
return carry ? { ...ts, editNotes: carry.editNotes, editBars: carry.editBars, editBpm: carry.editBpm } : ts;
|
|
5854
|
+
});
|
|
5855
|
+
});
|
|
5856
|
+
for (const ts of trackStates) {
|
|
5857
|
+
const persisted = sceneData[trackDataKey(ts.handle.dbId, "soundHistory")];
|
|
5858
|
+
if (persisted && typeof persisted === "object") {
|
|
5859
|
+
soundHistory.restore(ts.handle.id, persisted);
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
if (!isStale()) {
|
|
5863
|
+
setCrossfadePairsMeta(parseCrossfadePairs(sceneData));
|
|
5864
|
+
setFadesMeta(parseFades(sceneData));
|
|
5865
|
+
if (adapter.groupExtensions && adapter.groupExtensions.length > 0) {
|
|
5866
|
+
const map = {};
|
|
5867
|
+
for (const ext of adapter.groupExtensions) {
|
|
5868
|
+
map[ext.metaKey] = parseTrackGroups(sceneData, ext);
|
|
5869
|
+
}
|
|
5870
|
+
setGenericGroupMetas(map);
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
} catch (error) {
|
|
5874
|
+
console.error(`[${logTag}] Failed to load tracks:`, error);
|
|
5875
|
+
} finally {
|
|
5876
|
+
if (tracksLoadedForSceneRef.current === sceneAtStart) {
|
|
5877
|
+
setIsLoadingTracks(false);
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
},
|
|
5881
|
+
[host, activeSceneId, soundHistory, adapter, logTag]
|
|
5882
|
+
);
|
|
5883
|
+
(0, import_react25.useEffect)(() => {
|
|
5884
|
+
loadTracks();
|
|
5885
|
+
}, [loadTracks]);
|
|
5886
|
+
(0, import_react25.useEffect)(() => {
|
|
5887
|
+
const map = /* @__PURE__ */ new Map();
|
|
5888
|
+
for (const t of tracks) {
|
|
5889
|
+
map.set(t.handle.id, t.handle.dbId);
|
|
5890
|
+
}
|
|
5891
|
+
engineToDbIdRef.current = map;
|
|
5892
|
+
}, [tracks]);
|
|
5893
|
+
const loadedCompletedIdsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
5894
|
+
(0, import_react25.useEffect)(() => {
|
|
5895
|
+
if (placeholders.length === 0) {
|
|
5896
|
+
loadedCompletedIdsRef.current.clear();
|
|
5897
|
+
return;
|
|
5898
|
+
}
|
|
5899
|
+
const newCompleted = placeholders.filter(
|
|
5900
|
+
(ph) => ph.status === "completed" && !loadedCompletedIdsRef.current.has(ph.id)
|
|
5901
|
+
);
|
|
5902
|
+
if (newCompleted.length > 0) {
|
|
5903
|
+
for (const ph of newCompleted) {
|
|
5904
|
+
loadedCompletedIdsRef.current.add(ph.id);
|
|
5905
|
+
}
|
|
5906
|
+
console.log(
|
|
5907
|
+
`[${logTag}] ${newCompleted.length} track(s) completed, reloading. IDs:`,
|
|
5908
|
+
newCompleted.map((ph) => ph.id)
|
|
5909
|
+
);
|
|
5910
|
+
loadTracks(true);
|
|
5911
|
+
}
|
|
5912
|
+
}, [placeholders, loadTracks, logTag]);
|
|
5913
|
+
const adoptAndLoad = (0, import_react25.useCallback)(() => {
|
|
5914
|
+
loadTracks(true);
|
|
5915
|
+
}, [loadTracks]);
|
|
5916
|
+
(0, import_react25.useEffect)(() => {
|
|
5917
|
+
const unsub = host.onEngineReady(() => {
|
|
5918
|
+
adoptAndLoad();
|
|
4171
5919
|
});
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
5920
|
+
return unsub;
|
|
5921
|
+
}, [host, adoptAndLoad]);
|
|
5922
|
+
(0, import_react25.useEffect)(() => {
|
|
5923
|
+
if (typeof host.onAfterAgentMutation !== "function") return;
|
|
5924
|
+
let timer = null;
|
|
5925
|
+
const unsub = host.onAfterAgentMutation(() => {
|
|
5926
|
+
if (timer) clearTimeout(timer);
|
|
5927
|
+
timer = setTimeout(() => {
|
|
5928
|
+
timer = null;
|
|
5929
|
+
loadTracks(true);
|
|
5930
|
+
}, 500);
|
|
4180
5931
|
});
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
5932
|
+
return () => {
|
|
5933
|
+
unsub?.();
|
|
5934
|
+
if (timer) clearTimeout(timer);
|
|
5935
|
+
};
|
|
5936
|
+
}, [host, loadTracks]);
|
|
5937
|
+
(0, import_react25.useEffect)(() => {
|
|
5938
|
+
const unsub = host.onTrackStateChange((trackId, state) => {
|
|
5939
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
|
|
5940
|
+
});
|
|
5941
|
+
return unsub;
|
|
5942
|
+
}, [host]);
|
|
5943
|
+
(0, import_react25.useEffect)(() => {
|
|
5944
|
+
if (!features.bulkComposePlaceholders) return;
|
|
5945
|
+
console.log(`[${logTag}] Subscribing to composeProgress`);
|
|
5946
|
+
const unsub = host.onComposeProgress((event) => {
|
|
5947
|
+
const targetScene = event.sceneId;
|
|
5948
|
+
if (!targetScene) return;
|
|
5949
|
+
console.log(
|
|
5950
|
+
`[${logTag}] composeProgress event:`,
|
|
5951
|
+
event.phase,
|
|
5952
|
+
"sceneId:",
|
|
5953
|
+
targetScene,
|
|
5954
|
+
"placeholders:",
|
|
5955
|
+
event.placeholders?.length ?? "none"
|
|
5956
|
+
);
|
|
5957
|
+
switch (event.phase) {
|
|
5958
|
+
case "planning":
|
|
5959
|
+
setIsComposingForScene(targetScene, true);
|
|
5960
|
+
setPlaceholdersForScene(targetScene, []);
|
|
5961
|
+
break;
|
|
5962
|
+
case "generating":
|
|
5963
|
+
setIsComposingForScene(targetScene, false);
|
|
5964
|
+
if (event.placeholders) {
|
|
5965
|
+
setPlaceholdersForScene(targetScene, event.placeholders);
|
|
5966
|
+
}
|
|
5967
|
+
break;
|
|
5968
|
+
case "complete":
|
|
5969
|
+
case "error":
|
|
5970
|
+
setIsComposingForScene(targetScene, false);
|
|
5971
|
+
setPlaceholdersForScene(targetScene, EMPTY_PLACEHOLDERS);
|
|
5972
|
+
break;
|
|
5973
|
+
}
|
|
5974
|
+
});
|
|
5975
|
+
return unsub;
|
|
5976
|
+
}, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
|
|
5977
|
+
(0, import_react25.useEffect)(() => {
|
|
5978
|
+
const refs = saveTimeoutRefs;
|
|
5979
|
+
return () => {
|
|
5980
|
+
for (const timeout of Object.values(refs.current)) {
|
|
5981
|
+
clearTimeout(timeout);
|
|
5982
|
+
}
|
|
5983
|
+
};
|
|
5984
|
+
}, []);
|
|
5985
|
+
const isAddingTrackRef = (0, import_react25.useRef)(false);
|
|
5986
|
+
const [isAddingTrack, setIsAddingTrack] = (0, import_react25.useState)(false);
|
|
5987
|
+
const handleAddTrack = (0, import_react25.useCallback)(async () => {
|
|
5988
|
+
if (isAddingTrackRef.current) return;
|
|
5989
|
+
if (!activeSceneId) {
|
|
5990
|
+
host.showToast("warning", "Select SCENE");
|
|
5991
|
+
return;
|
|
5992
|
+
}
|
|
5993
|
+
if (!isConnected) {
|
|
5994
|
+
host.showToast("warning", "Systems not connected");
|
|
5995
|
+
return;
|
|
5996
|
+
}
|
|
5997
|
+
if (!isAuthenticated) {
|
|
5998
|
+
host.showToast("warning", "Sign In Required", "Please sign in to add tracks");
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
if (tracks.length >= identity.maxTracks) return;
|
|
6002
|
+
isAddingTrackRef.current = true;
|
|
6003
|
+
setIsAddingTrack(true);
|
|
6004
|
+
try {
|
|
6005
|
+
const handle = await host.createTrack({
|
|
6006
|
+
name: `${identity.trackNamePrefix}-${Date.now()}`,
|
|
6007
|
+
...adapter.createTrackOptions()
|
|
6008
|
+
});
|
|
6009
|
+
setTracks((prev) => [...prev, newTrackState(handle)]);
|
|
6010
|
+
onExpandSelf?.();
|
|
6011
|
+
setTimeout(() => {
|
|
6012
|
+
const inputs = document.querySelectorAll(
|
|
6013
|
+
`[data-testid="${identity.familyKey}-section"] [data-testid="sdk-prompt-input"]`
|
|
6014
|
+
);
|
|
6015
|
+
if (inputs.length > 0) {
|
|
6016
|
+
inputs[inputs.length - 1].focus();
|
|
6017
|
+
}
|
|
6018
|
+
}, 350);
|
|
6019
|
+
} catch (error) {
|
|
6020
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
6021
|
+
host.showToast("error", "Failed to create track", msg);
|
|
6022
|
+
} finally {
|
|
6023
|
+
isAddingTrackRef.current = false;
|
|
6024
|
+
setIsAddingTrack(false);
|
|
6025
|
+
}
|
|
6026
|
+
}, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
|
|
6027
|
+
const handlePortTrack = (0, import_react25.useCallback)(
|
|
6028
|
+
async (sel) => {
|
|
6029
|
+
if (!activeSceneId) {
|
|
6030
|
+
host.showToast("warning", "Select SCENE");
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
if (!isConnected) {
|
|
6034
|
+
host.showToast("warning", "Systems not connected");
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
6037
|
+
if (tracks.length >= identity.maxTracks) {
|
|
6038
|
+
host.showToast("warning", "Track limit reached");
|
|
6039
|
+
return;
|
|
6040
|
+
}
|
|
6041
|
+
if (!host.readImportableTrackMidi) return;
|
|
6042
|
+
let handle = null;
|
|
6043
|
+
try {
|
|
6044
|
+
handle = await host.createTrack({
|
|
6045
|
+
name: `${identity.trackNamePrefix}-${Date.now()}`,
|
|
6046
|
+
...adapter.createTrackOptions()
|
|
6047
|
+
});
|
|
6048
|
+
if (sel.role) {
|
|
6049
|
+
try {
|
|
6050
|
+
await host.setTrackRole(handle.id, sel.role);
|
|
6051
|
+
} catch {
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
const midi = await host.readImportableTrackMidi(sel.sourceTrackDbId);
|
|
6055
|
+
const notes = midi.clips[0]?.notes ?? [];
|
|
6056
|
+
if (notes.length > 0) {
|
|
6057
|
+
const mc = await host.getMusicalContext();
|
|
6058
|
+
await host.writeMidiClip(handle.id, {
|
|
6059
|
+
startTime: 0,
|
|
6060
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
6061
|
+
tempo: mc.bpm,
|
|
6062
|
+
notes
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6065
|
+
await adapter.applyPortedTrackSound(handle, sel.role);
|
|
6066
|
+
host.showToast(
|
|
6067
|
+
"success",
|
|
6068
|
+
`Imported to ${identity.familyKey}`,
|
|
6069
|
+
notes.length ? `${sel.trackName} \u2192 ${identity.familyKey}` : `${sel.trackName} (no MIDI yet)`
|
|
6070
|
+
);
|
|
6071
|
+
await loadTracks(true);
|
|
6072
|
+
} catch (err) {
|
|
6073
|
+
if (handle) {
|
|
6074
|
+
try {
|
|
6075
|
+
await host.deleteTrack(handle.id);
|
|
6076
|
+
} catch {
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
|
|
6080
|
+
}
|
|
6081
|
+
},
|
|
6082
|
+
[host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
|
|
6083
|
+
);
|
|
6084
|
+
const handleSoundImportPick = (0, import_react25.useCallback)(
|
|
6085
|
+
async (sel) => {
|
|
6086
|
+
const target = soundImportTarget;
|
|
6087
|
+
if (!target || !host.getTrackSound) {
|
|
6088
|
+
setSoundImportTarget(null);
|
|
6089
|
+
return;
|
|
6090
|
+
}
|
|
6091
|
+
const noun = adapter.sound.importNoun;
|
|
6092
|
+
const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
|
|
6093
|
+
try {
|
|
6094
|
+
const snap = await host.getTrackSound(sel.sourceTrackDbId);
|
|
6095
|
+
if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
|
|
6096
|
+
host.showToast(
|
|
6097
|
+
"error",
|
|
6098
|
+
`No ${noun} to import`,
|
|
6099
|
+
`${sel.trackName} has no ${identity.familyKey} ${noun}.`
|
|
6100
|
+
);
|
|
6101
|
+
return;
|
|
6102
|
+
}
|
|
6103
|
+
const descriptor = adapter.sound.descriptorFromSnapshot(snap);
|
|
6104
|
+
await adapter.sound.applySound(target.handle.id, descriptor);
|
|
6105
|
+
soundHistory.record(target.handle.id, descriptor, snap.label);
|
|
6106
|
+
host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
|
|
6107
|
+
} catch (err) {
|
|
6108
|
+
host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
|
|
6109
|
+
} finally {
|
|
6110
|
+
setSoundImportTarget(null);
|
|
6111
|
+
}
|
|
6112
|
+
},
|
|
6113
|
+
[soundImportTarget, host, adapter, identity.familyKey, soundHistory]
|
|
6114
|
+
);
|
|
6115
|
+
const [isExportingMidi, setIsExportingMidi] = (0, import_react25.useState)(false);
|
|
6116
|
+
const handleExportMidi = (0, import_react25.useCallback)(async () => {
|
|
6117
|
+
if (isExportingMidi) return;
|
|
6118
|
+
setIsExportingMidi(true);
|
|
6119
|
+
try {
|
|
6120
|
+
const result = await host.exportTracksAsMidiBundle({
|
|
6121
|
+
defaultName: identity.exportDefaultName ?? "midi-tracks"
|
|
4195
6122
|
});
|
|
6123
|
+
if (result.success) {
|
|
6124
|
+
const filename = result.filePath.split("/").pop() || result.filePath;
|
|
6125
|
+
const skippedNote = result.skippedCount > 0 ? ` (${result.skippedCount} empty track${result.skippedCount === 1 ? "" : "s"} skipped)` : "";
|
|
6126
|
+
host.showToast(
|
|
6127
|
+
"success",
|
|
6128
|
+
"MIDI exported",
|
|
6129
|
+
`${result.trackCount} track${result.trackCount === 1 ? "" : "s"} \u2192 ${filename}${skippedNote}`
|
|
6130
|
+
);
|
|
6131
|
+
} else if (!("canceled" in result && result.canceled)) {
|
|
6132
|
+
const errMsg = "error" in result ? result.error : "Unknown error";
|
|
6133
|
+
host.showToast("error", "Export failed", errMsg);
|
|
6134
|
+
}
|
|
6135
|
+
} catch (error) {
|
|
6136
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
6137
|
+
host.showToast("error", "Export failed", msg);
|
|
6138
|
+
} finally {
|
|
6139
|
+
setIsExportingMidi(false);
|
|
6140
|
+
}
|
|
6141
|
+
}, [host, identity.exportDefaultName, isExportingMidi]);
|
|
6142
|
+
const isBulkActive = !!(isComposing || placeholders.length > 0);
|
|
6143
|
+
const needsContract = !sceneContext?.hasContract;
|
|
6144
|
+
const xfFromId = sceneContext?.transitionFromSceneId ?? null;
|
|
6145
|
+
const xfToId = sceneContext?.transitionToSceneId ?? null;
|
|
6146
|
+
const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
|
|
6147
|
+
(0, import_react25.useEffect)(() => {
|
|
6148
|
+
if (!canCrossfade) setDesignerView(false);
|
|
6149
|
+
}, [canCrossfade]);
|
|
6150
|
+
(0, import_react25.useEffect)(() => {
|
|
6151
|
+
if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
|
|
6152
|
+
setTransitionSourceTotal(0);
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6155
|
+
let cancelled = false;
|
|
6156
|
+
void Promise.all([host.listSceneFamilyTracks(xfFromId), host.listSceneFamilyTracks(xfToId)]).then(([a, b]) => {
|
|
6157
|
+
if (!cancelled) setTransitionSourceTotal(a.length + b.length);
|
|
6158
|
+
}).catch(() => {
|
|
6159
|
+
if (!cancelled) setTransitionSourceTotal(0);
|
|
6160
|
+
});
|
|
6161
|
+
return () => {
|
|
6162
|
+
cancelled = true;
|
|
4196
6163
|
};
|
|
4197
|
-
|
|
4198
|
-
|
|
6164
|
+
}, [canCrossfade, xfFromId, xfToId, host]);
|
|
6165
|
+
const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
|
|
6166
|
+
(0, import_react25.useEffect)(() => {
|
|
6167
|
+
if (!onHeaderContent) return;
|
|
6168
|
+
const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
|
|
6169
|
+
onHeaderContent(
|
|
6170
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex gap-1 items-center", children: [
|
|
6171
|
+
features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
6172
|
+
"button",
|
|
6173
|
+
{
|
|
6174
|
+
"data-testid": `import-from-scene-${identity.familyKey}-button`,
|
|
6175
|
+
onClick: (e) => {
|
|
6176
|
+
e.stopPropagation();
|
|
6177
|
+
onExpandSelf?.();
|
|
6178
|
+
setImportOpen(true);
|
|
6179
|
+
},
|
|
6180
|
+
disabled: !activeSceneId || needsContract,
|
|
6181
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${!activeSceneId || needsContract ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
6182
|
+
children: identity.importTrackLabel ?? "Import Track"
|
|
6183
|
+
}
|
|
6184
|
+
),
|
|
6185
|
+
(!canCrossfade || !designerView) && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
6186
|
+
"button",
|
|
6187
|
+
{
|
|
6188
|
+
"data-testid": `add-${identity.familyKey}-track-button`,
|
|
6189
|
+
onClick: (e) => {
|
|
6190
|
+
e.stopPropagation();
|
|
6191
|
+
if (needsContract) {
|
|
6192
|
+
onOpenContract?.();
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
handleAddTrack();
|
|
6196
|
+
},
|
|
6197
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${addDisabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-accent/10 border-sas-accent/30 text-sas-accent hover:bg-sas-accent/20"}`,
|
|
6198
|
+
children: identity.addTrackLabel ?? "Add Track"
|
|
6199
|
+
}
|
|
6200
|
+
),
|
|
6201
|
+
canCrossfade && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
|
|
6202
|
+
"button",
|
|
6203
|
+
{
|
|
6204
|
+
"data-testid": `${identity.familyKey}-view-toggle`,
|
|
6205
|
+
onClick: (e) => {
|
|
6206
|
+
e.stopPropagation();
|
|
6207
|
+
if (!designerView) {
|
|
6208
|
+
if (needsContract) {
|
|
6209
|
+
onOpenContract?.();
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
onExpandSelf?.();
|
|
6213
|
+
}
|
|
6214
|
+
setDesignerView((v) => !v);
|
|
6215
|
+
},
|
|
6216
|
+
disabled: !designerView && needsContract,
|
|
6217
|
+
title: designerView ? "Back to the track list" : "Open the transition designer",
|
|
6218
|
+
className: "relative overflow-hidden px-2 py-0.5 text-[10px] font-medium rounded-sm border border-sas-accent/40 text-sas-accent transition-colors hover:border-sas-accent disabled:opacity-50",
|
|
6219
|
+
children: [
|
|
6220
|
+
transitionSourceTotal > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
6221
|
+
"span",
|
|
6222
|
+
{
|
|
6223
|
+
className: "absolute inset-y-0 left-0 bg-sas-accent/25",
|
|
6224
|
+
style: { width: `${Math.min(100, transitionDone / transitionSourceTotal * 100)}%` },
|
|
6225
|
+
"aria-hidden": true
|
|
6226
|
+
}
|
|
6227
|
+
),
|
|
6228
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "relative", children: [
|
|
6229
|
+
"\u21C4 ",
|
|
6230
|
+
designerView ? "Transition" : "Tracks",
|
|
6231
|
+
transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
|
|
6232
|
+
] })
|
|
6233
|
+
]
|
|
6234
|
+
}
|
|
6235
|
+
)
|
|
6236
|
+
] })
|
|
6237
|
+
);
|
|
4199
6238
|
return () => {
|
|
4200
|
-
|
|
4201
|
-
unsub();
|
|
6239
|
+
onHeaderContent(null);
|
|
4202
6240
|
};
|
|
4203
|
-
}, [
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
6241
|
+
}, [
|
|
6242
|
+
onHeaderContent,
|
|
6243
|
+
needsContract,
|
|
6244
|
+
isConnected,
|
|
6245
|
+
activeSceneId,
|
|
6246
|
+
tracks.length,
|
|
6247
|
+
isAddingTrack,
|
|
6248
|
+
handleAddTrack,
|
|
6249
|
+
onOpenContract,
|
|
6250
|
+
host,
|
|
6251
|
+
canCrossfade,
|
|
6252
|
+
designerView,
|
|
6253
|
+
transitionDone,
|
|
6254
|
+
transitionSourceTotal,
|
|
6255
|
+
onExpandSelf,
|
|
6256
|
+
identity,
|
|
6257
|
+
features.importTracks
|
|
6258
|
+
]);
|
|
6259
|
+
(0, import_react25.useEffect)(() => {
|
|
6260
|
+
if (!onLoading) return;
|
|
6261
|
+
const anyGenerating = tracks.some((t) => t.isGenerating);
|
|
6262
|
+
onLoading(isLoadingTracks || anyGenerating || isBulkActive);
|
|
6263
|
+
return () => {
|
|
6264
|
+
onLoading(false);
|
|
6265
|
+
};
|
|
6266
|
+
}, [onLoading, isLoadingTracks, tracks, isBulkActive]);
|
|
6267
|
+
const handleDeleteTrack = (0, import_react25.useCallback)(
|
|
6268
|
+
async (trackId) => {
|
|
6269
|
+
try {
|
|
6270
|
+
await host.deleteTrack(trackId);
|
|
6271
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
6272
|
+
if (activeSceneId) {
|
|
6273
|
+
await host.deleteSceneData(activeSceneId, trackDataKey(dbId, "prompt"));
|
|
6274
|
+
}
|
|
6275
|
+
setTracks((prev) => prev.filter((t) => t.handle.id !== trackId));
|
|
6276
|
+
} catch (error) {
|
|
6277
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
6278
|
+
host.showToast("error", "Failed to delete track", msg);
|
|
6279
|
+
}
|
|
4232
6280
|
},
|
|
4233
|
-
[
|
|
6281
|
+
[host, activeSceneId]
|
|
6282
|
+
);
|
|
6283
|
+
const handlePromptChange = (0, import_react25.useCallback)(
|
|
6284
|
+
(trackId, prompt) => {
|
|
6285
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
|
|
6286
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
6287
|
+
if (saveTimeoutRefs.current[trackId]) {
|
|
6288
|
+
clearTimeout(saveTimeoutRefs.current[trackId]);
|
|
6289
|
+
}
|
|
6290
|
+
saveTimeoutRefs.current[trackId] = setTimeout(() => {
|
|
6291
|
+
if (activeSceneId) {
|
|
6292
|
+
host.setSceneData(activeSceneId, trackDataKey(dbId, "prompt"), prompt).catch(() => {
|
|
6293
|
+
});
|
|
6294
|
+
}
|
|
6295
|
+
}, 500);
|
|
6296
|
+
},
|
|
6297
|
+
[host, activeSceneId]
|
|
6298
|
+
);
|
|
6299
|
+
const resolvedGenericGroups = (0, import_react25.useMemo)(() => {
|
|
6300
|
+
const out = {};
|
|
6301
|
+
for (const ext of adapter.groupExtensions ?? []) {
|
|
6302
|
+
out[ext.metaKey] = resolveTrackGroups(
|
|
6303
|
+
genericGroupMetas[ext.metaKey] ?? [],
|
|
6304
|
+
tracks,
|
|
6305
|
+
(t) => t.handle.dbId,
|
|
6306
|
+
{
|
|
6307
|
+
isComplete: ext.isComplete
|
|
6308
|
+
}
|
|
6309
|
+
);
|
|
6310
|
+
}
|
|
6311
|
+
return out;
|
|
6312
|
+
}, [adapter, genericGroupMetas, tracks]);
|
|
6313
|
+
const genericGroupMemberDbIds = (0, import_react25.useMemo)(() => {
|
|
6314
|
+
const s = /* @__PURE__ */ new Set();
|
|
6315
|
+
for (const r of Object.values(resolvedGenericGroups)) {
|
|
6316
|
+
for (const dbId of r.memberDbIds) s.add(dbId);
|
|
6317
|
+
}
|
|
6318
|
+
return s;
|
|
6319
|
+
}, [resolvedGenericGroups]);
|
|
6320
|
+
const engineToDbId = (0, import_react25.useCallback)(
|
|
6321
|
+
(trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
|
|
6322
|
+
[]
|
|
6323
|
+
);
|
|
6324
|
+
const updateTrack = (0, import_react25.useCallback)(
|
|
6325
|
+
(trackId, patch) => {
|
|
6326
|
+
setTracks(
|
|
6327
|
+
(prev) => prev.map(
|
|
6328
|
+
(t) => t.handle.id === trackId ? typeof patch === "function" ? patch(t) : { ...t, ...patch } : t
|
|
6329
|
+
)
|
|
6330
|
+
);
|
|
6331
|
+
},
|
|
6332
|
+
[]
|
|
6333
|
+
);
|
|
6334
|
+
const markEditLoaded = (0, import_react25.useCallback)((trackId) => {
|
|
6335
|
+
editLoadStartedRef.current.add(trackId);
|
|
6336
|
+
}, []);
|
|
6337
|
+
const tracksRef = (0, import_react25.useRef)(tracks);
|
|
6338
|
+
(0, import_react25.useEffect)(() => {
|
|
6339
|
+
tracksRef.current = tracks;
|
|
6340
|
+
}, [tracks]);
|
|
6341
|
+
const resolvedGenericGroupsRef = (0, import_react25.useRef)(resolvedGenericGroups);
|
|
6342
|
+
(0, import_react25.useEffect)(() => {
|
|
6343
|
+
resolvedGenericGroupsRef.current = resolvedGenericGroups;
|
|
6344
|
+
}, [resolvedGenericGroups]);
|
|
6345
|
+
const makeServices = (0, import_react25.useCallback)(() => {
|
|
6346
|
+
return {
|
|
6347
|
+
host,
|
|
6348
|
+
activeSceneId,
|
|
6349
|
+
tracks: tracksRef.current,
|
|
6350
|
+
updateTrack,
|
|
6351
|
+
setTracks,
|
|
6352
|
+
reloadTracks: loadTracks,
|
|
6353
|
+
soundHistory,
|
|
6354
|
+
engineToDbId,
|
|
6355
|
+
trackDataKey,
|
|
6356
|
+
markEditLoaded,
|
|
6357
|
+
createFamilyTrack: (nameSuffix = "") => host.createTrack({
|
|
6358
|
+
name: `${identity.trackNamePrefix}-${Date.now()}${nameSuffix}`,
|
|
6359
|
+
...adapter.createTrackOptions()
|
|
6360
|
+
}),
|
|
6361
|
+
resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
|
|
6362
|
+
};
|
|
6363
|
+
}, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
|
|
6364
|
+
const handleGenerate = (0, import_react25.useCallback)(
|
|
6365
|
+
async (trackId) => {
|
|
6366
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6367
|
+
if (!track || !track.prompt.trim()) return;
|
|
6368
|
+
if (!isAuthenticated) {
|
|
6369
|
+
host.showToast("warning", "Sign In Required", "Please sign in to generate MIDI");
|
|
6370
|
+
return;
|
|
6371
|
+
}
|
|
6372
|
+
setTracks(
|
|
6373
|
+
(prev) => prev.map(
|
|
6374
|
+
(t) => t.handle.id === trackId ? { ...t, isGenerating: true, error: null, generationProgress: 0 } : t
|
|
6375
|
+
)
|
|
6376
|
+
);
|
|
6377
|
+
try {
|
|
6378
|
+
await adapter.generation.generate(track, makeServices());
|
|
6379
|
+
} catch (error) {
|
|
6380
|
+
const msg = error instanceof Error ? error.message : "Generation failed";
|
|
6381
|
+
setTracks(
|
|
6382
|
+
(prev) => prev.map(
|
|
6383
|
+
(t) => t.handle.id === trackId ? { ...t, isGenerating: false, error: msg, generationProgress: 0 } : t
|
|
6384
|
+
)
|
|
6385
|
+
);
|
|
6386
|
+
host.showToast("error", "Generation failed", msg);
|
|
6387
|
+
}
|
|
6388
|
+
},
|
|
6389
|
+
[host, adapter, tracks, isAuthenticated, makeServices]
|
|
6390
|
+
);
|
|
6391
|
+
const handleMuteToggle = (0, import_react25.useCallback)(
|
|
6392
|
+
(trackId) => {
|
|
6393
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6394
|
+
if (!track) return;
|
|
6395
|
+
const newMuted = !track.runtimeState.muted;
|
|
6396
|
+
setTracks(
|
|
6397
|
+
(prev) => prev.map(
|
|
6398
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
|
|
6399
|
+
)
|
|
6400
|
+
);
|
|
6401
|
+
host.setTrackMute(trackId, newMuted).catch(() => {
|
|
6402
|
+
setTracks(
|
|
6403
|
+
(prev) => prev.map(
|
|
6404
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: !newMuted } } : t
|
|
6405
|
+
)
|
|
6406
|
+
);
|
|
6407
|
+
});
|
|
6408
|
+
},
|
|
6409
|
+
[host, tracks]
|
|
6410
|
+
);
|
|
6411
|
+
const handleSoloToggle = (0, import_react25.useCallback)(
|
|
6412
|
+
(trackId) => {
|
|
6413
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6414
|
+
if (!track) return;
|
|
6415
|
+
const newSolo = !track.runtimeState.solo;
|
|
6416
|
+
setTracks(
|
|
6417
|
+
(prev) => prev.map(
|
|
6418
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
|
|
6419
|
+
)
|
|
6420
|
+
);
|
|
6421
|
+
host.setTrackSolo(trackId, newSolo).catch(() => {
|
|
6422
|
+
setTracks(
|
|
6423
|
+
(prev) => prev.map(
|
|
6424
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: !newSolo } } : t
|
|
6425
|
+
)
|
|
6426
|
+
);
|
|
6427
|
+
});
|
|
6428
|
+
},
|
|
6429
|
+
[host, tracks]
|
|
6430
|
+
);
|
|
6431
|
+
const handleVolumeChange = (0, import_react25.useCallback)(
|
|
6432
|
+
(trackId, volume) => {
|
|
6433
|
+
setTracks(
|
|
6434
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
|
|
6435
|
+
);
|
|
6436
|
+
host.setTrackVolume(trackId, volume).catch(() => {
|
|
6437
|
+
});
|
|
6438
|
+
},
|
|
6439
|
+
[host]
|
|
6440
|
+
);
|
|
6441
|
+
const handlePanChange = (0, import_react25.useCallback)(
|
|
6442
|
+
(trackId, pan) => {
|
|
6443
|
+
setTracks(
|
|
6444
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
|
|
6445
|
+
);
|
|
6446
|
+
host.setTrackPan(trackId, pan).catch(() => {
|
|
6447
|
+
});
|
|
6448
|
+
},
|
|
6449
|
+
[host]
|
|
6450
|
+
);
|
|
6451
|
+
const handleShuffle = (0, import_react25.useCallback)(
|
|
6452
|
+
async (trackId) => {
|
|
6453
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6454
|
+
if (!track) return;
|
|
6455
|
+
if (soundHistory.list(trackId).entries.length === 0) {
|
|
6456
|
+
try {
|
|
6457
|
+
const cap = await adapter.sound.captureSoundDescriptor(trackId);
|
|
6458
|
+
if (cap) soundHistory.record(trackId, cap.descriptor, adapter.sound.previousSoundLabel);
|
|
6459
|
+
} catch {
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6462
|
+
try {
|
|
6463
|
+
let result;
|
|
6464
|
+
let nextHistory;
|
|
6465
|
+
try {
|
|
6466
|
+
result = await adapter.shuffle.shuffle(track, Array.from(track.shuffleHistory));
|
|
6467
|
+
nextHistory = new Set(track.shuffleHistory);
|
|
6468
|
+
} catch (firstErr) {
|
|
6469
|
+
if (adapter.shuffle.isExhaustedError(firstErr)) {
|
|
6470
|
+
nextHistory = /* @__PURE__ */ new Set();
|
|
6471
|
+
result = await adapter.shuffle.shuffle(track, []);
|
|
6472
|
+
} else {
|
|
6473
|
+
throw firstErr;
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
nextHistory.add(result.appliedName);
|
|
6477
|
+
setTracks(
|
|
6478
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, shuffleHistory: nextHistory } : t)
|
|
6479
|
+
);
|
|
6480
|
+
try {
|
|
6481
|
+
const cap = await adapter.sound.captureSoundDescriptor(trackId);
|
|
6482
|
+
if (cap) soundHistory.record(trackId, cap.descriptor, result.appliedName);
|
|
6483
|
+
} catch {
|
|
6484
|
+
}
|
|
6485
|
+
console.log(`[${logTag}] Sound shuffled: ${result.appliedName} (history ${nextHistory.size})`);
|
|
6486
|
+
} catch (error) {
|
|
6487
|
+
const msg = error instanceof Error ? error.message : "Shuffle failed";
|
|
6488
|
+
host.showToast("error", "Shuffle failed", msg);
|
|
6489
|
+
}
|
|
6490
|
+
},
|
|
6491
|
+
[host, adapter, tracks, soundHistory, logTag]
|
|
6492
|
+
);
|
|
6493
|
+
const handleCopy = (0, import_react25.useCallback)(
|
|
6494
|
+
async (trackId) => {
|
|
6495
|
+
try {
|
|
6496
|
+
const newHandle = await host.duplicateTrack(trackId);
|
|
6497
|
+
await loadTracks();
|
|
6498
|
+
host.showToast("success", "Track duplicated", newHandle.name);
|
|
6499
|
+
} catch (error) {
|
|
6500
|
+
const msg = error instanceof Error ? error.message : "Copy failed";
|
|
6501
|
+
host.showToast("error", "Copy failed", msg);
|
|
6502
|
+
}
|
|
6503
|
+
},
|
|
6504
|
+
[host, loadTracks]
|
|
6505
|
+
);
|
|
6506
|
+
const handleFxToggle = (0, import_react25.useCallback)(
|
|
6507
|
+
(trackId, category, enabled) => {
|
|
6508
|
+
setTracks(
|
|
6509
|
+
(prev) => prev.map(
|
|
6510
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], enabled } } } : t
|
|
6511
|
+
)
|
|
6512
|
+
);
|
|
6513
|
+
host.toggleTrackFx(trackId, category, enabled).catch(() => {
|
|
6514
|
+
setTracks(
|
|
6515
|
+
(prev) => prev.map(
|
|
6516
|
+
(t) => t.handle.id === trackId ? {
|
|
6517
|
+
...t,
|
|
6518
|
+
fxDetailState: {
|
|
6519
|
+
...t.fxDetailState,
|
|
6520
|
+
[category]: { ...t.fxDetailState[category], enabled: !enabled }
|
|
6521
|
+
}
|
|
6522
|
+
} : t
|
|
6523
|
+
)
|
|
6524
|
+
);
|
|
6525
|
+
});
|
|
6526
|
+
},
|
|
6527
|
+
[host]
|
|
6528
|
+
);
|
|
6529
|
+
const handleFxPresetChange = (0, import_react25.useCallback)(
|
|
6530
|
+
(trackId, category, presetIndex) => {
|
|
6531
|
+
setTracks(
|
|
6532
|
+
(prev) => prev.map(
|
|
6533
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], presetIndex } } } : t
|
|
6534
|
+
)
|
|
6535
|
+
);
|
|
6536
|
+
host.setTrackFxPreset(trackId, category, presetIndex).then((result) => {
|
|
6537
|
+
if (result.dryWet !== void 0) {
|
|
6538
|
+
setTracks(
|
|
6539
|
+
(prev) => prev.map(
|
|
6540
|
+
(t) => t.handle.id === trackId ? {
|
|
6541
|
+
...t,
|
|
6542
|
+
fxDetailState: {
|
|
6543
|
+
...t.fxDetailState,
|
|
6544
|
+
[category]: { ...t.fxDetailState[category], dryWet: result.dryWet }
|
|
6545
|
+
}
|
|
6546
|
+
} : t
|
|
6547
|
+
)
|
|
6548
|
+
);
|
|
6549
|
+
}
|
|
6550
|
+
}).catch(() => {
|
|
6551
|
+
});
|
|
6552
|
+
},
|
|
6553
|
+
[host]
|
|
6554
|
+
);
|
|
6555
|
+
const handleFxDryWetChange = (0, import_react25.useCallback)(
|
|
6556
|
+
(trackId, category, value) => {
|
|
6557
|
+
setTracks(
|
|
6558
|
+
(prev) => prev.map(
|
|
6559
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], dryWet: value } } } : t
|
|
6560
|
+
)
|
|
6561
|
+
);
|
|
6562
|
+
host.setTrackFxDryWet(trackId, category, value).catch(() => {
|
|
6563
|
+
});
|
|
6564
|
+
},
|
|
6565
|
+
[host]
|
|
4234
6566
|
);
|
|
4235
|
-
const
|
|
4236
|
-
(trackId
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
6567
|
+
const toggleFxDrawer = (0, import_react25.useCallback)(
|
|
6568
|
+
(trackId) => {
|
|
6569
|
+
setTracks(
|
|
6570
|
+
(prev) => prev.map((t) => {
|
|
6571
|
+
if (t.handle.id !== trackId) return t;
|
|
6572
|
+
const onFx = t.drawerOpen && t.drawerTab === "fx";
|
|
6573
|
+
return { ...t, drawerOpen: !onFx, drawerTab: "fx", editorStage: false };
|
|
6574
|
+
})
|
|
6575
|
+
);
|
|
6576
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6577
|
+
const wasOnFx = !!track && track.drawerOpen && track.drawerTab === "fx";
|
|
6578
|
+
if (track && !wasOnFx) {
|
|
6579
|
+
host.getTrackFxState(trackId).then((fxState) => {
|
|
6580
|
+
setTracks(
|
|
6581
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
|
|
6582
|
+
);
|
|
6583
|
+
}).catch(() => {
|
|
6584
|
+
});
|
|
4245
6585
|
}
|
|
4246
|
-
commit(trackId, { entries, cursor: entries.length - 1 }, true);
|
|
4247
6586
|
},
|
|
4248
|
-
[
|
|
6587
|
+
[host, tracks]
|
|
4249
6588
|
);
|
|
4250
|
-
const
|
|
4251
|
-
async (trackId
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
6589
|
+
const loadEditNotes = (0, import_react25.useCallback)(
|
|
6590
|
+
async (trackId) => {
|
|
6591
|
+
try {
|
|
6592
|
+
const mc = await host.getMusicalContext();
|
|
6593
|
+
let notes = [];
|
|
6594
|
+
if (typeof host.readMidiNotes === "function") {
|
|
6595
|
+
const result = await host.readMidiNotes(trackId);
|
|
6596
|
+
notes = result.clips[0]?.notes ?? [];
|
|
6597
|
+
}
|
|
6598
|
+
setTracks(
|
|
6599
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes, editBars: mc.bars, editBpm: mc.bpm } : t)
|
|
6600
|
+
);
|
|
6601
|
+
} catch (err) {
|
|
6602
|
+
console.warn(`[${logTag}] Failed to load MIDI for editing:`, err);
|
|
6603
|
+
}
|
|
4257
6604
|
},
|
|
4258
|
-
[
|
|
6605
|
+
[host, logTag]
|
|
4259
6606
|
);
|
|
4260
|
-
const
|
|
4261
|
-
(trackId) => {
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
6607
|
+
const handleNotesChange = (0, import_react25.useCallback)(
|
|
6608
|
+
(trackId, notes) => {
|
|
6609
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
|
|
6610
|
+
const key = `edit:${trackId}`;
|
|
6611
|
+
if (saveTimeoutRefs.current[key]) {
|
|
6612
|
+
clearTimeout(saveTimeoutRefs.current[key]);
|
|
6613
|
+
}
|
|
6614
|
+
saveTimeoutRefs.current[key] = setTimeout(() => {
|
|
6615
|
+
void (async () => {
|
|
6616
|
+
try {
|
|
6617
|
+
if (notes.length === 0) {
|
|
6618
|
+
await host.clearMidi(trackId);
|
|
6619
|
+
} else {
|
|
6620
|
+
const mc = await host.getMusicalContext();
|
|
6621
|
+
await host.writeMidiClip(trackId, {
|
|
6622
|
+
startTime: 0,
|
|
6623
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
6624
|
+
tempo: mc.bpm,
|
|
6625
|
+
notes
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6628
|
+
} catch (err) {
|
|
6629
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6630
|
+
host.showToast("error", "Failed to save edit", msg);
|
|
6631
|
+
}
|
|
6632
|
+
})();
|
|
6633
|
+
}, 300);
|
|
4265
6634
|
},
|
|
4266
|
-
[
|
|
6635
|
+
[host]
|
|
4267
6636
|
);
|
|
4268
|
-
const
|
|
4269
|
-
(trackId,
|
|
4270
|
-
|
|
4271
|
-
if (
|
|
4272
|
-
|
|
4273
|
-
|
|
6637
|
+
const handleTabChange = (0, import_react25.useCallback)(
|
|
6638
|
+
(trackId, tab) => {
|
|
6639
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
|
|
6640
|
+
if (tab === "fx") {
|
|
6641
|
+
host.getTrackFxState(trackId).then((fxState) => {
|
|
6642
|
+
setTracks(
|
|
6643
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
|
|
6644
|
+
);
|
|
6645
|
+
}).catch(() => {
|
|
6646
|
+
});
|
|
6647
|
+
} else if (tab === "pick" && availableInstruments.length === 0 && !instrumentsLoading) {
|
|
6648
|
+
setInstrumentsLoading(true);
|
|
6649
|
+
host.getAvailableInstruments().then((instruments) => {
|
|
6650
|
+
setAvailableInstruments(instruments);
|
|
6651
|
+
}).catch(() => {
|
|
6652
|
+
}).finally(() => {
|
|
6653
|
+
setInstrumentsLoading(false);
|
|
6654
|
+
});
|
|
6655
|
+
} else if (tab === "edit" && !editLoadStartedRef.current.has(trackId)) {
|
|
6656
|
+
editLoadStartedRef.current.add(trackId);
|
|
6657
|
+
void loadEditNotes(trackId);
|
|
6658
|
+
}
|
|
4274
6659
|
},
|
|
4275
|
-
[
|
|
6660
|
+
[host, availableInstruments.length, instrumentsLoading, loadEditNotes]
|
|
4276
6661
|
);
|
|
4277
|
-
const
|
|
4278
|
-
(
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
6662
|
+
const handleProgressChange = (0, import_react25.useCallback)((trackId, pct) => {
|
|
6663
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
|
|
6664
|
+
}, []);
|
|
6665
|
+
const handleToggleDrawer = (0, import_react25.useCallback)((trackId) => {
|
|
6666
|
+
setTracks(
|
|
6667
|
+
(prev) => prev.map((t) => {
|
|
6668
|
+
if (t.handle.id !== trackId) return t;
|
|
6669
|
+
const onSound = t.drawerOpen && t.drawerTab !== "fx";
|
|
6670
|
+
return { ...t, drawerOpen: !onSound, drawerTab: "history", editorStage: false };
|
|
6671
|
+
})
|
|
6672
|
+
);
|
|
6673
|
+
}, []);
|
|
6674
|
+
const handleInstrumentSelect = (0, import_react25.useCallback)(
|
|
6675
|
+
async (trackId, pluginId) => {
|
|
6676
|
+
const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
|
|
6677
|
+
if (isDefaultInstrument) {
|
|
6678
|
+
setTracks(
|
|
6679
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: false, editorStage: false } : t)
|
|
6680
|
+
);
|
|
6681
|
+
try {
|
|
6682
|
+
await host.setTrackInstrument(trackId, pluginId);
|
|
6683
|
+
const descriptor = await host.getTrackInstrument(trackId);
|
|
6684
|
+
setTracks(
|
|
6685
|
+
(prev) => prev.map(
|
|
6686
|
+
(t) => t.handle.id === trackId ? {
|
|
6687
|
+
...t,
|
|
6688
|
+
instrumentPluginId: descriptor?.pluginId ?? null,
|
|
6689
|
+
instrumentName: descriptor?.name ?? null,
|
|
6690
|
+
instrumentMissing: descriptor?.missing ?? false
|
|
6691
|
+
} : t
|
|
6692
|
+
)
|
|
6693
|
+
);
|
|
6694
|
+
} catch (err) {
|
|
6695
|
+
const msg = err instanceof Error ? err.message : "Failed to load instrument";
|
|
6696
|
+
host.showToast("error", "Instrument load failed", msg);
|
|
6697
|
+
}
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6700
|
+
setTracks(
|
|
6701
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerTab: "pick", editorStage: true } : t)
|
|
6702
|
+
);
|
|
6703
|
+
try {
|
|
6704
|
+
await host.setTrackInstrument(trackId, pluginId);
|
|
6705
|
+
const descriptor = await host.getTrackInstrument(trackId);
|
|
6706
|
+
setTracks(
|
|
6707
|
+
(prev) => prev.map(
|
|
6708
|
+
(t) => t.handle.id === trackId ? {
|
|
6709
|
+
...t,
|
|
6710
|
+
instrumentPluginId: descriptor?.pluginId ?? null,
|
|
6711
|
+
instrumentName: descriptor?.name ?? null,
|
|
6712
|
+
instrumentMissing: descriptor?.missing ?? false
|
|
6713
|
+
} : t
|
|
6714
|
+
)
|
|
6715
|
+
);
|
|
6716
|
+
} catch (err) {
|
|
6717
|
+
const msg = err instanceof Error ? err.message : "Failed to load instrument";
|
|
6718
|
+
console.error(`[${logTag}] Failed to set instrument:`, err);
|
|
6719
|
+
host.showToast("error", "Instrument load failed", msg);
|
|
6720
|
+
setTracks(
|
|
6721
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
|
|
6722
|
+
);
|
|
6723
|
+
}
|
|
4283
6724
|
},
|
|
4284
|
-
[
|
|
6725
|
+
[host, identity.defaultInstrumentPluginId, logTag]
|
|
4285
6726
|
);
|
|
4286
|
-
const
|
|
4287
|
-
(trackId) =>
|
|
4288
|
-
|
|
6727
|
+
const handleShowEditor = (0, import_react25.useCallback)(
|
|
6728
|
+
async (trackId) => {
|
|
6729
|
+
try {
|
|
6730
|
+
await host.showInstrumentEditor(trackId);
|
|
6731
|
+
} catch (err) {
|
|
6732
|
+
const msg = err instanceof Error ? err.message : "Failed to open editor";
|
|
6733
|
+
host.showToast("error", "Editor failed", msg);
|
|
6734
|
+
}
|
|
6735
|
+
},
|
|
6736
|
+
[host]
|
|
4289
6737
|
);
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
|
|
6738
|
+
const handleBackToInstruments = (0, import_react25.useCallback)((trackId) => {
|
|
6739
|
+
setTracks(
|
|
6740
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
|
|
6741
|
+
);
|
|
4293
6742
|
}, []);
|
|
4294
|
-
const
|
|
4295
|
-
(
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
6743
|
+
const handleRefreshInstruments = (0, import_react25.useCallback)(() => {
|
|
6744
|
+
setInstrumentsLoading(true);
|
|
6745
|
+
host.getAvailableInstruments().then((instruments) => {
|
|
6746
|
+
setAvailableInstruments(instruments);
|
|
6747
|
+
}).catch(() => {
|
|
6748
|
+
}).finally(() => {
|
|
6749
|
+
setInstrumentsLoading(false);
|
|
6750
|
+
});
|
|
6751
|
+
}, [host]);
|
|
6752
|
+
const onAuditionNote = (0, import_react25.useCallback)(
|
|
6753
|
+
(trackId, pitch, velocity, ms) => {
|
|
6754
|
+
void host.auditionNote(trackId, pitch, velocity, ms);
|
|
6755
|
+
},
|
|
6756
|
+
[host]
|
|
6757
|
+
);
|
|
6758
|
+
const { resolvedCrossfadePairs, crossfadeMemberDbIds } = (0, import_react25.useMemo)(() => {
|
|
6759
|
+
const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
|
|
6760
|
+
const pairs = [];
|
|
6761
|
+
const members = /* @__PURE__ */ new Set();
|
|
6762
|
+
for (const p of crossfadePairsMeta) {
|
|
6763
|
+
const origin = byDbId.get(p.originDbId);
|
|
6764
|
+
const target = byDbId.get(p.targetDbId);
|
|
6765
|
+
if (origin && target) {
|
|
6766
|
+
pairs.push({ ...p, origin, target });
|
|
6767
|
+
members.add(p.originDbId);
|
|
6768
|
+
members.add(p.targetDbId);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
|
|
6772
|
+
}, [tracks, crossfadePairsMeta]);
|
|
6773
|
+
const { resolvedFades, fadeMemberDbIds } = (0, import_react25.useMemo)(() => {
|
|
6774
|
+
const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
|
|
6775
|
+
const list = [];
|
|
6776
|
+
const members = /* @__PURE__ */ new Set();
|
|
6777
|
+
for (const f of fadesMeta) {
|
|
6778
|
+
const track = byDbId.get(f.dbId);
|
|
6779
|
+
if (track) {
|
|
6780
|
+
list.push({ ...f, track });
|
|
6781
|
+
members.add(f.dbId);
|
|
6782
|
+
}
|
|
6783
|
+
}
|
|
6784
|
+
return { resolvedFades: list, fadeMemberDbIds: members };
|
|
6785
|
+
}, [tracks, fadesMeta]);
|
|
6786
|
+
const transition = useTransitionOps({
|
|
6787
|
+
host,
|
|
6788
|
+
adapter,
|
|
6789
|
+
activeSceneId,
|
|
6790
|
+
isConnected,
|
|
6791
|
+
isAuthenticated,
|
|
6792
|
+
sceneContext,
|
|
6793
|
+
tracks,
|
|
6794
|
+
setTracks,
|
|
6795
|
+
loadTracks,
|
|
6796
|
+
setCrossfadePairsMeta,
|
|
6797
|
+
setFadesMeta,
|
|
6798
|
+
resolvedCrossfadePairs,
|
|
6799
|
+
resolvedFades
|
|
6800
|
+
});
|
|
6801
|
+
const setGroupMute = (0, import_react25.useCallback)(
|
|
6802
|
+
(trackIds, muted) => {
|
|
6803
|
+
for (const id of trackIds) {
|
|
6804
|
+
setTracks(
|
|
6805
|
+
(prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted } } : t)
|
|
6806
|
+
);
|
|
6807
|
+
host.setTrackMute(id, muted).catch(() => {
|
|
6808
|
+
});
|
|
4301
6809
|
}
|
|
4302
|
-
onChangeRef.current?.(trackId, EMPTY);
|
|
4303
6810
|
},
|
|
4304
|
-
[
|
|
6811
|
+
[host]
|
|
4305
6812
|
);
|
|
4306
|
-
const
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
6813
|
+
const setGroupSolo = (0, import_react25.useCallback)(
|
|
6814
|
+
(trackIds, solo) => {
|
|
6815
|
+
for (const id of trackIds) {
|
|
6816
|
+
setTracks(
|
|
6817
|
+
(prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo } } : t)
|
|
6818
|
+
);
|
|
6819
|
+
host.setTrackSolo(id, solo).catch(() => {
|
|
6820
|
+
});
|
|
6821
|
+
}
|
|
6822
|
+
},
|
|
6823
|
+
[host]
|
|
6824
|
+
);
|
|
6825
|
+
const deleteGroup = (0, import_react25.useCallback)(
|
|
6826
|
+
async (members, cleanupKeySuffixes) => {
|
|
6827
|
+
for (const member of members) {
|
|
6828
|
+
try {
|
|
6829
|
+
await host.deleteTrack(member.engineId);
|
|
6830
|
+
} catch {
|
|
6831
|
+
}
|
|
6832
|
+
if (activeSceneId) {
|
|
6833
|
+
for (const suffix of cleanupKeySuffixes) {
|
|
6834
|
+
await host.deleteSceneData(activeSceneId, trackDataKey(member.dbId, suffix)).catch(() => {
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
const gone = new Set(members.map((m) => m.engineId));
|
|
6840
|
+
setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
|
|
6841
|
+
await loadTracks(true);
|
|
6842
|
+
},
|
|
6843
|
+
[host, activeSceneId, loadTracks]
|
|
6844
|
+
);
|
|
6845
|
+
const handlers = (0, import_react25.useMemo)(
|
|
6846
|
+
() => ({
|
|
6847
|
+
promptChange: handlePromptChange,
|
|
6848
|
+
generate: (trackId) => {
|
|
6849
|
+
void handleGenerate(trackId);
|
|
6850
|
+
},
|
|
6851
|
+
shuffle: (trackId) => {
|
|
6852
|
+
void handleShuffle(trackId);
|
|
6853
|
+
},
|
|
6854
|
+
copy: (trackId) => {
|
|
6855
|
+
void handleCopy(trackId);
|
|
6856
|
+
},
|
|
6857
|
+
delete: (trackId) => {
|
|
6858
|
+
void handleDeleteTrack(trackId);
|
|
6859
|
+
},
|
|
6860
|
+
muteToggle: handleMuteToggle,
|
|
6861
|
+
soloToggle: handleSoloToggle,
|
|
6862
|
+
volumeChange: handleVolumeChange,
|
|
6863
|
+
panChange: handlePanChange,
|
|
6864
|
+
tabChange: handleTabChange,
|
|
6865
|
+
toggleDrawer: handleToggleDrawer,
|
|
6866
|
+
toggleFxDrawer,
|
|
6867
|
+
notesChange: handleNotesChange,
|
|
6868
|
+
progressChange: handleProgressChange
|
|
6869
|
+
}),
|
|
6870
|
+
[
|
|
6871
|
+
handlePromptChange,
|
|
6872
|
+
handleGenerate,
|
|
6873
|
+
handleShuffle,
|
|
6874
|
+
handleCopy,
|
|
6875
|
+
handleDeleteTrack,
|
|
6876
|
+
handleMuteToggle,
|
|
6877
|
+
handleSoloToggle,
|
|
6878
|
+
handleVolumeChange,
|
|
6879
|
+
handlePanChange,
|
|
6880
|
+
handleTabChange,
|
|
6881
|
+
handleToggleDrawer,
|
|
6882
|
+
toggleFxDrawer,
|
|
6883
|
+
handleNotesChange,
|
|
6884
|
+
handleProgressChange
|
|
6885
|
+
]
|
|
4313
6886
|
);
|
|
6887
|
+
return {
|
|
6888
|
+
ui,
|
|
6889
|
+
adapter,
|
|
6890
|
+
tracks,
|
|
6891
|
+
setTracks,
|
|
6892
|
+
isLoadingTracks,
|
|
6893
|
+
loadTracks,
|
|
6894
|
+
engineToDbId,
|
|
6895
|
+
supportsMeters,
|
|
6896
|
+
trackLevels,
|
|
6897
|
+
anySolo,
|
|
6898
|
+
reorder,
|
|
6899
|
+
soundHistory,
|
|
6900
|
+
isComposing,
|
|
6901
|
+
placeholders,
|
|
6902
|
+
isAddingTrack,
|
|
6903
|
+
isExportingMidi,
|
|
6904
|
+
designerView,
|
|
6905
|
+
canCrossfade,
|
|
6906
|
+
needsContract,
|
|
6907
|
+
xfFromId,
|
|
6908
|
+
xfToId,
|
|
6909
|
+
importOpen,
|
|
6910
|
+
setImportOpen,
|
|
6911
|
+
soundImportTarget,
|
|
6912
|
+
setSoundImportTarget,
|
|
6913
|
+
handleSoundImportPick,
|
|
6914
|
+
handlePortTrack,
|
|
6915
|
+
transition,
|
|
6916
|
+
crossfadePairsMeta,
|
|
6917
|
+
fadesMeta,
|
|
6918
|
+
resolvedCrossfadePairs,
|
|
6919
|
+
crossfadeMemberDbIds,
|
|
6920
|
+
resolvedFades,
|
|
6921
|
+
fadeMemberDbIds,
|
|
6922
|
+
resolvedGenericGroups,
|
|
6923
|
+
genericGroupMemberDbIds,
|
|
6924
|
+
availableInstruments,
|
|
6925
|
+
instrumentsLoading,
|
|
6926
|
+
handlers,
|
|
6927
|
+
handleGenerate,
|
|
6928
|
+
handleShuffle,
|
|
6929
|
+
handleAddTrack,
|
|
6930
|
+
handleDeleteTrack,
|
|
6931
|
+
handleExportMidi,
|
|
6932
|
+
handlePromptChange,
|
|
6933
|
+
handleMuteToggle,
|
|
6934
|
+
handleSoloToggle,
|
|
6935
|
+
handleVolumeChange,
|
|
6936
|
+
handlePanChange,
|
|
6937
|
+
handleTabChange,
|
|
6938
|
+
handleToggleDrawer,
|
|
6939
|
+
toggleFxDrawer,
|
|
6940
|
+
handleNotesChange,
|
|
6941
|
+
handleProgressChange,
|
|
6942
|
+
handleCopy,
|
|
6943
|
+
handleFxToggle,
|
|
6944
|
+
handleFxPresetChange,
|
|
6945
|
+
handleFxDryWetChange,
|
|
6946
|
+
handleInstrumentSelect,
|
|
6947
|
+
handleShowEditor,
|
|
6948
|
+
handleBackToInstruments,
|
|
6949
|
+
handleRefreshInstruments,
|
|
6950
|
+
onAuditionNote,
|
|
6951
|
+
makeServices,
|
|
6952
|
+
setGroupMute,
|
|
6953
|
+
setGroupSolo,
|
|
6954
|
+
deleteGroup
|
|
6955
|
+
};
|
|
4314
6956
|
}
|
|
4315
6957
|
|
|
4316
|
-
// src/
|
|
4317
|
-
var
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
6958
|
+
// src/panel-core/GeneratorPanelShell.tsx
|
|
6959
|
+
var import_react26 = __toESM(require("react"));
|
|
6960
|
+
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
6961
|
+
function GeneratorPanelShell({ core, slots }) {
|
|
6962
|
+
const {
|
|
6963
|
+
ui,
|
|
6964
|
+
adapter,
|
|
6965
|
+
tracks,
|
|
6966
|
+
isLoadingTracks,
|
|
6967
|
+
supportsMeters,
|
|
6968
|
+
trackLevels,
|
|
6969
|
+
anySolo,
|
|
6970
|
+
reorder,
|
|
6971
|
+
soundHistory,
|
|
6972
|
+
isComposing,
|
|
6973
|
+
placeholders,
|
|
6974
|
+
designerView,
|
|
6975
|
+
canCrossfade,
|
|
6976
|
+
xfFromId,
|
|
6977
|
+
xfToId,
|
|
6978
|
+
importOpen,
|
|
6979
|
+
setImportOpen,
|
|
6980
|
+
soundImportTarget,
|
|
6981
|
+
setSoundImportTarget,
|
|
6982
|
+
handleSoundImportPick,
|
|
6983
|
+
handlePortTrack,
|
|
6984
|
+
transition,
|
|
6985
|
+
crossfadePairsMeta,
|
|
6986
|
+
fadesMeta,
|
|
6987
|
+
resolvedCrossfadePairs,
|
|
6988
|
+
crossfadeMemberDbIds,
|
|
6989
|
+
resolvedFades,
|
|
6990
|
+
fadeMemberDbIds,
|
|
6991
|
+
resolvedGenericGroups,
|
|
6992
|
+
genericGroupMemberDbIds,
|
|
6993
|
+
availableInstruments,
|
|
6994
|
+
instrumentsLoading,
|
|
6995
|
+
handlers,
|
|
6996
|
+
isExportingMidi,
|
|
6997
|
+
handleExportMidi,
|
|
6998
|
+
handleFxToggle,
|
|
6999
|
+
handleFxPresetChange,
|
|
7000
|
+
handleFxDryWetChange,
|
|
7001
|
+
handleInstrumentSelect,
|
|
7002
|
+
handleShowEditor,
|
|
7003
|
+
handleBackToInstruments,
|
|
7004
|
+
handleRefreshInstruments,
|
|
7005
|
+
onAuditionNote,
|
|
7006
|
+
loadTracks,
|
|
7007
|
+
makeServices,
|
|
7008
|
+
setGroupMute,
|
|
7009
|
+
setGroupSolo,
|
|
7010
|
+
deleteGroup
|
|
7011
|
+
} = core;
|
|
7012
|
+
const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
|
|
7013
|
+
const { identity, features } = adapter;
|
|
7014
|
+
const buildRowProps = (0, import_react26.useCallback)(
|
|
7015
|
+
(track, drag) => {
|
|
7016
|
+
const id = track.handle.id;
|
|
7017
|
+
const pickerProps = features.instrumentPicker ? {
|
|
7018
|
+
instrumentName: track.instrumentName,
|
|
7019
|
+
instrumentMissing: track.instrumentMissing,
|
|
7020
|
+
onToggleDrawer: () => handlers.toggleDrawer(id),
|
|
7021
|
+
availableInstruments,
|
|
7022
|
+
currentInstrumentPluginId: track.instrumentPluginId,
|
|
7023
|
+
onInstrumentSelect: (pluginId) => handleInstrumentSelect(id, pluginId),
|
|
7024
|
+
instrumentsLoading,
|
|
7025
|
+
onRefreshInstruments: handleRefreshInstruments,
|
|
7026
|
+
editorStage: track.editorStage,
|
|
7027
|
+
onShowEditor: () => handleShowEditor(id),
|
|
7028
|
+
onBackToInstruments: () => handleBackToInstruments(id)
|
|
7029
|
+
} : {};
|
|
7030
|
+
const importSoundProps = features.importTracks ? {
|
|
7031
|
+
onImportSound: () => setSoundImportTarget(track),
|
|
7032
|
+
importSoundLabel: adapter.sound.importSoundLabel
|
|
7033
|
+
} : {};
|
|
7034
|
+
const props = {
|
|
7035
|
+
...drag ? { drag } : {},
|
|
7036
|
+
track: { id, name: track.handle.name, role: track.role },
|
|
7037
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7038
|
+
prompt: track.prompt,
|
|
7039
|
+
runtimeState: {
|
|
7040
|
+
muted: track.runtimeState.muted,
|
|
7041
|
+
solo: track.runtimeState.solo,
|
|
7042
|
+
volume: track.runtimeState.volume,
|
|
7043
|
+
pan: track.runtimeState.pan
|
|
7044
|
+
},
|
|
7045
|
+
soloedOut: anySolo && !track.runtimeState.solo,
|
|
7046
|
+
fxDetailState: track.fxDetailState,
|
|
7047
|
+
drawerOpen: track.drawerOpen,
|
|
7048
|
+
drawerTab: track.drawerTab,
|
|
7049
|
+
onTabChange: (tab) => handlers.tabChange(id, tab),
|
|
7050
|
+
isGenerating: track.isGenerating,
|
|
7051
|
+
isAuthenticated,
|
|
7052
|
+
error: track.error,
|
|
7053
|
+
hasMidi: track.hasMidi,
|
|
7054
|
+
generationProgress: track.generationProgress,
|
|
7055
|
+
estimatedGenerationMs: identity.estimatedGenerationMs,
|
|
7056
|
+
onPromptChange: (prompt) => handlers.promptChange(id, prompt),
|
|
7057
|
+
onGenerate: () => handlers.generate(id),
|
|
7058
|
+
onShuffle: () => handlers.shuffle(id),
|
|
7059
|
+
onCopy: () => handlers.copy(id),
|
|
7060
|
+
onDelete: () => handlers.delete(id),
|
|
7061
|
+
onMuteToggle: () => handlers.muteToggle(id),
|
|
7062
|
+
onSoloToggle: () => handlers.soloToggle(id),
|
|
7063
|
+
onVolumeChange: (vol) => handlers.volumeChange(id, vol),
|
|
7064
|
+
onPanChange: (pan) => handlers.panChange(id, pan),
|
|
7065
|
+
onFxToggle: (cat, enabled) => handleFxToggle(id, cat, enabled),
|
|
7066
|
+
onFxPresetChange: (cat, idx) => handleFxPresetChange(id, cat, idx),
|
|
7067
|
+
onFxDryWetChange: (cat, val) => handleFxDryWetChange(id, cat, val),
|
|
7068
|
+
onToggleFxDrawer: () => handlers.toggleFxDrawer(id),
|
|
7069
|
+
onProgressChange: (pct) => handlers.progressChange(id, pct),
|
|
7070
|
+
accentColor: identity.accentColor,
|
|
7071
|
+
...pickerProps,
|
|
7072
|
+
soundHistory: soundHistory.list(id).entries,
|
|
7073
|
+
soundHistoryCursor: soundHistory.list(id).cursor,
|
|
7074
|
+
onRestoreSound: (i) => {
|
|
7075
|
+
void soundHistory.restoreTo(id, i);
|
|
7076
|
+
},
|
|
7077
|
+
onToggleFavorite: (i) => soundHistory.toggleFavorite(id, i),
|
|
7078
|
+
...importSoundProps,
|
|
7079
|
+
editNotes: track.editNotes,
|
|
7080
|
+
onNotesChange: (notes) => handlers.notesChange(id, notes),
|
|
7081
|
+
editBars: track.editBars,
|
|
7082
|
+
editBpm: track.editBpm,
|
|
7083
|
+
editSnap: 0.25,
|
|
7084
|
+
onAuditionNote: (pitch, vel, ms) => onAuditionNote(id, pitch, vel, ms)
|
|
7085
|
+
};
|
|
7086
|
+
return adapter.mapTrackRowProps ? adapter.mapTrackRowProps(track, props) : props;
|
|
7087
|
+
},
|
|
7088
|
+
[
|
|
7089
|
+
features.instrumentPicker,
|
|
7090
|
+
features.importTracks,
|
|
7091
|
+
adapter,
|
|
7092
|
+
supportsMeters,
|
|
7093
|
+
trackLevels,
|
|
7094
|
+
anySolo,
|
|
7095
|
+
isAuthenticated,
|
|
7096
|
+
identity,
|
|
7097
|
+
handlers,
|
|
7098
|
+
availableInstruments,
|
|
7099
|
+
instrumentsLoading,
|
|
7100
|
+
handleInstrumentSelect,
|
|
7101
|
+
handleRefreshInstruments,
|
|
7102
|
+
handleShowEditor,
|
|
7103
|
+
handleBackToInstruments,
|
|
7104
|
+
setSoundImportTarget,
|
|
7105
|
+
soundHistory,
|
|
7106
|
+
handleFxToggle,
|
|
7107
|
+
handleFxPresetChange,
|
|
7108
|
+
handleFxDryWetChange,
|
|
7109
|
+
onAuditionNote
|
|
7110
|
+
]
|
|
7111
|
+
);
|
|
7112
|
+
if (!activeSceneId) {
|
|
7113
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7114
|
+
"div",
|
|
7115
|
+
{
|
|
7116
|
+
"data-testid": `no-scene-placeholder-${identity.familyKey}`,
|
|
7117
|
+
className: "flex items-center justify-center py-8",
|
|
7118
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7119
|
+
"button",
|
|
7120
|
+
{
|
|
7121
|
+
onClick: () => onSelectScene?.(),
|
|
7122
|
+
className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
|
|
7123
|
+
children: "Select a Scene"
|
|
7124
|
+
}
|
|
7125
|
+
)
|
|
7126
|
+
}
|
|
7127
|
+
);
|
|
4322
7128
|
}
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
const [dragOverIndex, setDragOverIndex] = (0, import_react22.useState)(null);
|
|
4336
|
-
const fromRef = (0, import_react22.useRef)(null);
|
|
4337
|
-
const itemsRef = (0, import_react22.useRef)(items);
|
|
4338
|
-
itemsRef.current = items;
|
|
4339
|
-
const dragPropsFor = (0, import_react22.useCallback)(
|
|
4340
|
-
(index) => ({
|
|
4341
|
-
handleProps: {
|
|
4342
|
-
draggable: true,
|
|
4343
|
-
onDragStart: (e) => {
|
|
4344
|
-
fromRef.current = index;
|
|
4345
|
-
setDraggingIndex(index);
|
|
4346
|
-
if (e.dataTransfer) {
|
|
4347
|
-
e.dataTransfer.effectAllowed = "move";
|
|
4348
|
-
try {
|
|
4349
|
-
e.dataTransfer.setData("text/plain", String(index));
|
|
4350
|
-
} catch {
|
|
4351
|
-
}
|
|
7129
|
+
if (!sceneContext?.hasContract) {
|
|
7130
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7131
|
+
"div",
|
|
7132
|
+
{
|
|
7133
|
+
"data-testid": `no-contract-placeholder-${identity.familyKey}`,
|
|
7134
|
+
className: "flex items-center justify-center py-8",
|
|
7135
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7136
|
+
"button",
|
|
7137
|
+
{
|
|
7138
|
+
onClick: () => onOpenContract?.(),
|
|
7139
|
+
className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
|
|
7140
|
+
children: "Generate a Contract"
|
|
4352
7141
|
}
|
|
7142
|
+
)
|
|
7143
|
+
}
|
|
7144
|
+
);
|
|
7145
|
+
}
|
|
7146
|
+
if (features.bulkComposePlaceholders && isComposing) {
|
|
7147
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
|
|
7148
|
+
}
|
|
7149
|
+
const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
|
|
7150
|
+
if (activePlaceholders.length > 0) {
|
|
7151
|
+
const tracksByDbId = /* @__PURE__ */ new Map();
|
|
7152
|
+
for (const t of tracks) {
|
|
7153
|
+
tracksByDbId.set(t.handle.dbId, t);
|
|
7154
|
+
if (t.handle.id !== t.handle.dbId) {
|
|
7155
|
+
tracksByDbId.set(t.handle.id, t);
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
|
|
7159
|
+
const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
|
|
7160
|
+
if (loadedTrack) {
|
|
7161
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
|
|
7162
|
+
}
|
|
7163
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7164
|
+
"div",
|
|
7165
|
+
{
|
|
7166
|
+
"data-testid": "bulk-placeholder-track",
|
|
7167
|
+
className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
|
|
7168
|
+
style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
|
|
7169
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
|
|
4353
7170
|
},
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
7171
|
+
ph.id
|
|
7172
|
+
);
|
|
7173
|
+
}) });
|
|
7174
|
+
}
|
|
7175
|
+
const groupCtx = {
|
|
7176
|
+
services: makeServices(),
|
|
7177
|
+
anySolo,
|
|
7178
|
+
supportsMeters,
|
|
7179
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7180
|
+
handlers,
|
|
7181
|
+
renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
|
|
7182
|
+
setGroupMute,
|
|
7183
|
+
setGroupSolo,
|
|
7184
|
+
deleteGroup
|
|
7185
|
+
};
|
|
7186
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
|
|
7187
|
+
features.importTracks && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7188
|
+
ImportTrackModal,
|
|
7189
|
+
{
|
|
7190
|
+
host,
|
|
7191
|
+
open: importOpen,
|
|
7192
|
+
onClose: () => setImportOpen(false),
|
|
7193
|
+
onImported: () => {
|
|
7194
|
+
void loadTracks(true);
|
|
4365
7195
|
},
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
7196
|
+
onPortTrack: host.readImportableTrackMidi ? handlePortTrack : void 0,
|
|
7197
|
+
testIdPrefix: `${identity.familyKey}-import`
|
|
7198
|
+
}
|
|
7199
|
+
),
|
|
7200
|
+
features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7201
|
+
ImportTrackModal,
|
|
7202
|
+
{
|
|
7203
|
+
host,
|
|
7204
|
+
mode: "sound",
|
|
7205
|
+
open: !!soundImportTarget,
|
|
7206
|
+
title: adapter.sound.importSoundLabel,
|
|
7207
|
+
onClose: () => setSoundImportTarget(null),
|
|
7208
|
+
onImported: () => {
|
|
4371
7209
|
},
|
|
4372
|
-
|
|
4373
|
-
|
|
7210
|
+
onPick: handleSoundImportPick,
|
|
7211
|
+
testIdPrefix: `${identity.familyKey}-sound-import`
|
|
7212
|
+
}
|
|
7213
|
+
),
|
|
7214
|
+
slots?.modals,
|
|
7215
|
+
canCrossfade && xfFromId && xfToId && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7216
|
+
TransitionDesigner,
|
|
7217
|
+
{
|
|
7218
|
+
host,
|
|
7219
|
+
fromSceneId: xfFromId,
|
|
7220
|
+
toSceneId: xfToId,
|
|
7221
|
+
transitionSceneId: activeSceneId ?? "",
|
|
7222
|
+
excludeSourceDbIds: [
|
|
7223
|
+
...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
|
|
7224
|
+
...fadesMeta.map((f) => f.meta.sourceTrackDbId)
|
|
7225
|
+
],
|
|
7226
|
+
onCreateCrossfade: transition.handleCreateCrossfade,
|
|
7227
|
+
onCreateFade: transition.handleCreateFade,
|
|
7228
|
+
familyLabel: identity.familyLabel,
|
|
7229
|
+
testIdPrefix: `${identity.familyKey}-transition-designer`
|
|
7230
|
+
}
|
|
7231
|
+
) }),
|
|
7232
|
+
!(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
|
|
7233
|
+
slots?.beforeRows,
|
|
7234
|
+
resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7235
|
+
CrossfadeTrackRow,
|
|
7236
|
+
{
|
|
7237
|
+
accentColor: identity.transitionAccentColor,
|
|
7238
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7239
|
+
sliderPos: pair.sliderPos,
|
|
7240
|
+
origin: {
|
|
7241
|
+
trackId: pair.origin.handle.id,
|
|
7242
|
+
name: pair.origin.handle.name,
|
|
7243
|
+
role: pair.origin.role,
|
|
7244
|
+
sourceName: pair.originSourceName,
|
|
7245
|
+
soundLabel: pair.originSoundLabel,
|
|
7246
|
+
runtimeState: pair.origin.runtimeState
|
|
7247
|
+
},
|
|
7248
|
+
target: {
|
|
7249
|
+
trackId: pair.target.handle.id,
|
|
7250
|
+
name: pair.target.handle.name,
|
|
7251
|
+
role: pair.target.role,
|
|
7252
|
+
sourceName: pair.targetSourceName,
|
|
7253
|
+
soundLabel: pair.targetSoundLabel,
|
|
7254
|
+
runtimeState: pair.target.runtimeState
|
|
7255
|
+
},
|
|
7256
|
+
onMuteToggle: () => transition.handleCrossfadeMute(pair),
|
|
7257
|
+
onSoloToggle: () => transition.handleCrossfadeSolo(pair),
|
|
7258
|
+
onVolumeChange: (slot, vol) => handlers.volumeChange(
|
|
7259
|
+
slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
|
|
7260
|
+
vol
|
|
7261
|
+
),
|
|
7262
|
+
onPanChange: (slot, pan) => handlers.panChange(
|
|
7263
|
+
slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
|
|
7264
|
+
pan
|
|
7265
|
+
),
|
|
7266
|
+
onSliderChange: (pos) => transition.handleCrossfadeSlider(pair, pos),
|
|
7267
|
+
onDelete: () => transition.handleCrossfadeDelete(pair)
|
|
4374
7268
|
},
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
7269
|
+
pair.groupId
|
|
7270
|
+
)),
|
|
7271
|
+
resolvedFades.map((fade) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7272
|
+
FadeTrackRow,
|
|
7273
|
+
{
|
|
7274
|
+
accentColor: identity.transitionAccentColor,
|
|
7275
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7276
|
+
direction: fade.meta.direction,
|
|
7277
|
+
gesture: fade.meta.gesture,
|
|
7278
|
+
sliderPos: fade.meta.sliderPos,
|
|
7279
|
+
layer: {
|
|
7280
|
+
trackId: fade.track.handle.id,
|
|
7281
|
+
name: fade.track.handle.name,
|
|
7282
|
+
role: fade.track.role,
|
|
7283
|
+
sourceName: fade.meta.sourceName,
|
|
7284
|
+
soundLabel: fade.meta.soundLabel,
|
|
7285
|
+
runtimeState: fade.track.runtimeState
|
|
7286
|
+
},
|
|
7287
|
+
onMuteToggle: () => handlers.muteToggle(fade.track.handle.id),
|
|
7288
|
+
onSoloToggle: () => handlers.soloToggle(fade.track.handle.id),
|
|
7289
|
+
onVolumeChange: (vol) => handlers.volumeChange(fade.track.handle.id, vol),
|
|
7290
|
+
onPanChange: (pan) => handlers.panChange(fade.track.handle.id, pan),
|
|
7291
|
+
onSliderChange: (pos) => transition.handleFadeSlider(fade, pos),
|
|
7292
|
+
onDelete: () => transition.handleFadeDelete(fade)
|
|
7293
|
+
},
|
|
7294
|
+
fade.dbId
|
|
7295
|
+
)),
|
|
7296
|
+
(adapter.groupExtensions ?? []).flatMap(
|
|
7297
|
+
(ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react26.default.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
|
|
7298
|
+
),
|
|
7299
|
+
tracks.map((track, index) => {
|
|
7300
|
+
if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
|
|
7301
|
+
return null;
|
|
4390
7302
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
}),
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
7303
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
|
|
7304
|
+
}),
|
|
7305
|
+
slots?.afterRows
|
|
7306
|
+
] })),
|
|
7307
|
+
features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
|
|
7308
|
+
const hasAnyMidi = tracks.some((t) => t.hasMidi);
|
|
7309
|
+
const exportDisabled = isExportingMidi || !hasAnyMidi;
|
|
7310
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "pt-2", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
7311
|
+
"button",
|
|
7312
|
+
{
|
|
7313
|
+
"data-testid": "export-midi-tracks-button",
|
|
7314
|
+
onClick: handleExportMidi,
|
|
7315
|
+
disabled: exportDisabled,
|
|
7316
|
+
title: isExportingMidi ? "Exporting..." : !hasAnyMidi ? "Generate MIDI on at least one track first" : "Export all tracks as a ZIP of .mid files",
|
|
7317
|
+
className: `w-full px-2 py-1.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors ${exportDisabled ? "text-sas-muted/40 border-transparent hover:border-sas-accent cursor-not-allowed" : "text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent"}`,
|
|
7318
|
+
children: isExportingMidi ? "Exporting..." : "Export Tracks"
|
|
7319
|
+
}
|
|
7320
|
+
) });
|
|
7321
|
+
})()
|
|
7322
|
+
] });
|
|
7323
|
+
}
|
|
7324
|
+
|
|
7325
|
+
// src/panel-core/surge-sound-adapter.ts
|
|
7326
|
+
async function getInstrument(host, trackId) {
|
|
7327
|
+
try {
|
|
7328
|
+
const plugins = await host.getTrackPlugins(trackId);
|
|
7329
|
+
const instrument = plugins.find(
|
|
7330
|
+
(p) => !p.name.includes("Volume") && !p.name.includes("Pan") && !p.name.includes("Level")
|
|
7331
|
+
);
|
|
7332
|
+
if (!instrument) return null;
|
|
7333
|
+
return { index: instrument.index, isRaw: !instrument.name.includes("Surge") };
|
|
7334
|
+
} catch {
|
|
7335
|
+
return null;
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7338
|
+
function createSurgeSoundAdapter(host, overrides = {}) {
|
|
7339
|
+
const applySound = async (trackId, descriptor) => {
|
|
7340
|
+
const { state, stateType } = descriptor;
|
|
7341
|
+
const inst = await getInstrument(host, trackId);
|
|
7342
|
+
if (!inst) return;
|
|
7343
|
+
if (stateType === "raw") await host.setRawPluginState(trackId, inst.index, state);
|
|
7344
|
+
else await host.setPluginState(trackId, inst.index, state);
|
|
7345
|
+
};
|
|
7346
|
+
return {
|
|
7347
|
+
applySound,
|
|
7348
|
+
captureSoundDescriptor: async (trackId) => {
|
|
7349
|
+
const inst = await getInstrument(host, trackId);
|
|
7350
|
+
if (!inst) return null;
|
|
7351
|
+
const state = inst.isRaw ? await host.getRawPluginState(trackId, inst.index) : await host.getPluginState(trackId, inst.index);
|
|
7352
|
+
return { descriptor: { state, stateType: inst.isRaw ? "raw" : "valuetree" } };
|
|
7353
|
+
},
|
|
7354
|
+
copySnapshot: async (trackId, snap) => {
|
|
7355
|
+
if (snap.kind !== "preset") return "default";
|
|
7356
|
+
await applySound(trackId, { state: snap.state, stateType: snap.stateType });
|
|
7357
|
+
await host.persistTrackPresetState?.(trackId, {
|
|
7358
|
+
state: snap.state,
|
|
7359
|
+
stateType: snap.stateType ?? "valuetree",
|
|
7360
|
+
name: snap.label
|
|
7361
|
+
}).catch(() => {
|
|
7362
|
+
});
|
|
7363
|
+
return snap.label;
|
|
7364
|
+
},
|
|
7365
|
+
descriptorFromSnapshot: (snap) => {
|
|
7366
|
+
const preset = snap;
|
|
7367
|
+
return { state: preset.state, stateType: preset.stateType };
|
|
7368
|
+
},
|
|
7369
|
+
acceptedSnapshotKind: "preset",
|
|
7370
|
+
historyMax: overrides.historyMax ?? 12,
|
|
7371
|
+
importSoundLabel: overrides.importSoundLabel ?? "Import Preset",
|
|
7372
|
+
importNoun: "preset",
|
|
7373
|
+
previousSoundLabel: "Previous preset"
|
|
7374
|
+
};
|
|
4398
7375
|
}
|
|
4399
7376
|
|
|
4400
7377
|
// src/constants/sdk-version.ts
|
|
4401
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
7378
|
+
var PLUGIN_SDK_VERSION = "2.35.0";
|
|
4402
7379
|
|
|
4403
7380
|
// src/utils/format-concurrent-tracks.ts
|
|
4404
7381
|
function formatConcurrentTracks(ctx) {
|
|
@@ -4542,6 +7519,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4542
7519
|
}
|
|
4543
7520
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4544
7521
|
0 && (module.exports = {
|
|
7522
|
+
AUDIO_EFFECTS,
|
|
7523
|
+
AUDIO_EFFECT_LABEL,
|
|
4545
7524
|
ConfirmDialog,
|
|
4546
7525
|
CrossfadeModal,
|
|
4547
7526
|
CrossfadeTrackRow,
|
|
@@ -4563,6 +7542,7 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4563
7542
|
FadeTrackRow,
|
|
4564
7543
|
FxToggleBar,
|
|
4565
7544
|
GUTTER_W,
|
|
7545
|
+
GeneratorPanelShell,
|
|
4566
7546
|
ImportTrackModal,
|
|
4567
7547
|
InstrumentDrawer,
|
|
4568
7548
|
LevelMeter,
|
|
@@ -4580,44 +7560,68 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4580
7560
|
ScrollingWaveform,
|
|
4581
7561
|
SorceryProgressBar,
|
|
4582
7562
|
TEXTURAL_ROLES,
|
|
7563
|
+
TRANSITION_DESIGNER_DRAFT_KEY,
|
|
4583
7564
|
TrackDrawer,
|
|
4584
7565
|
TrackMeterStrip,
|
|
4585
7566
|
TrackRow,
|
|
7567
|
+
TransitionDesigner,
|
|
4586
7568
|
VolumeSlider,
|
|
4587
7569
|
WaveformView,
|
|
4588
7570
|
analyzeWavPeak,
|
|
7571
|
+
asAudioEffect,
|
|
4589
7572
|
asCrossfadeMeta,
|
|
4590
7573
|
asFadeMeta,
|
|
7574
|
+
asTransitionDesignerDraft,
|
|
4591
7575
|
buildCrossfadeInpaintPrompt,
|
|
4592
7576
|
buildCrossfadeVolumeCurves,
|
|
4593
7577
|
buildFadeVolumeCurve,
|
|
7578
|
+
buildRowSlots,
|
|
4594
7579
|
calculateTimeBasedTarget,
|
|
4595
7580
|
cellToPx,
|
|
4596
7581
|
centerScrollTop,
|
|
4597
7582
|
computePeaks,
|
|
7583
|
+
createSurgeSoundAdapter,
|
|
7584
|
+
dbIdsFromKeys,
|
|
4598
7585
|
dbToSlider,
|
|
4599
7586
|
defaultFadeGesture,
|
|
4600
7587
|
drawWaveform,
|
|
4601
7588
|
formatConcurrentTracks,
|
|
7589
|
+
hashString,
|
|
4602
7590
|
moveItem,
|
|
7591
|
+
newTrackState,
|
|
7592
|
+
normalizeSlots,
|
|
7593
|
+
padPair,
|
|
7594
|
+
padSlots,
|
|
4603
7595
|
parseCrossfadePairs,
|
|
4604
7596
|
parseFades,
|
|
7597
|
+
parseLLMNoteResponse,
|
|
7598
|
+
parseTrackGroups,
|
|
4605
7599
|
pickTopKWeighted,
|
|
4606
7600
|
pitchToName,
|
|
7601
|
+
pluginFxToToggleFx,
|
|
4607
7602
|
pxToCell,
|
|
7603
|
+
reconcileSlots,
|
|
4608
7604
|
resizeNoteDuration,
|
|
7605
|
+
resolveTrackGroups,
|
|
7606
|
+
rowKey,
|
|
7607
|
+
rowType,
|
|
4609
7608
|
scorePromptMatch,
|
|
4610
7609
|
sliderToDb,
|
|
7610
|
+
slotsEqual,
|
|
7611
|
+
soundIdentity,
|
|
4611
7612
|
synthesizeCuePoints,
|
|
4612
7613
|
tokenizePrompt,
|
|
7614
|
+
trackDataKey,
|
|
4613
7615
|
transposeNotes,
|
|
4614
7616
|
useAnySolo,
|
|
7617
|
+
useGeneratorPanelCore,
|
|
4615
7618
|
useSceneState,
|
|
4616
7619
|
useSoundHistory,
|
|
4617
7620
|
useTrackLevel,
|
|
4618
7621
|
useTrackLevels,
|
|
4619
7622
|
useTrackMeter,
|
|
4620
7623
|
useTrackReorder,
|
|
7624
|
+
useTransitionOps,
|
|
4621
7625
|
useTransportPlaying
|
|
4622
7626
|
});
|
|
4623
7627
|
//# sourceMappingURL=index.js.map
|