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