@signalsandsorcery/plugin-sdk 2.26.1 → 2.28.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 +215 -2
- package/dist/index.d.ts +215 -2
- package/dist/index.js +668 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +661 -173
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,6 +47,8 @@ __export(index_exports, {
|
|
|
47
47
|
FX_DISPLAY_LABELS: () => FX_DISPLAY_LABELS,
|
|
48
48
|
FX_ENGINE_PLUGIN_NAMES: () => FX_ENGINE_PLUGIN_NAMES,
|
|
49
49
|
FX_PRESET_CONFIGS: () => FX_PRESET_CONFIGS,
|
|
50
|
+
FadeModal: () => FadeModal,
|
|
51
|
+
FadeTrackRow: () => FadeTrackRow,
|
|
50
52
|
FxToggleBar: () => FxToggleBar,
|
|
51
53
|
GUTTER_W: () => GUTTER_W,
|
|
52
54
|
ImportTrackModal: () => ImportTrackModal,
|
|
@@ -65,6 +67,7 @@ __export(index_exports, {
|
|
|
65
67
|
SamplePackCTACard: () => SamplePackCTACard,
|
|
66
68
|
ScrollingWaveform: () => ScrollingWaveform,
|
|
67
69
|
SorceryProgressBar: () => SorceryProgressBar,
|
|
70
|
+
TEXTURAL_ROLES: () => TEXTURAL_ROLES,
|
|
68
71
|
TrackDrawer: () => TrackDrawer,
|
|
69
72
|
TrackMeterStrip: () => TrackMeterStrip,
|
|
70
73
|
TrackRow: () => TrackRow,
|
|
@@ -72,17 +75,21 @@ __export(index_exports, {
|
|
|
72
75
|
WaveformView: () => WaveformView,
|
|
73
76
|
analyzeWavPeak: () => analyzeWavPeak,
|
|
74
77
|
asCrossfadeMeta: () => asCrossfadeMeta,
|
|
78
|
+
asFadeMeta: () => asFadeMeta,
|
|
75
79
|
buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
|
|
76
80
|
buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
|
|
81
|
+
buildFadeVolumeCurve: () => buildFadeVolumeCurve,
|
|
77
82
|
calculateTimeBasedTarget: () => calculateTimeBasedTarget,
|
|
78
83
|
cellToPx: () => cellToPx,
|
|
79
84
|
centerScrollTop: () => centerScrollTop,
|
|
80
85
|
computePeaks: () => computePeaks,
|
|
81
86
|
dbToSlider: () => dbToSlider,
|
|
87
|
+
defaultFadeGesture: () => defaultFadeGesture,
|
|
82
88
|
drawWaveform: () => drawWaveform,
|
|
83
89
|
formatConcurrentTracks: () => formatConcurrentTracks,
|
|
84
90
|
moveItem: () => moveItem,
|
|
85
91
|
parseCrossfadePairs: () => parseCrossfadePairs,
|
|
92
|
+
parseFades: () => parseFades,
|
|
86
93
|
pickTopKWeighted: () => pickTopKWeighted,
|
|
87
94
|
pitchToName: () => pitchToName,
|
|
88
95
|
pxToCell: () => pxToCell,
|
|
@@ -2675,9 +2682,448 @@ function buildCrossfadeInpaintPrompt(input) {
|
|
|
2675
2682
|
return lines.join("\n");
|
|
2676
2683
|
}
|
|
2677
2684
|
|
|
2678
|
-
// src/
|
|
2679
|
-
|
|
2685
|
+
// src/fade-meta.ts
|
|
2686
|
+
function asFadeMeta(val) {
|
|
2687
|
+
if (!val || typeof val !== "object") return null;
|
|
2688
|
+
const m = val;
|
|
2689
|
+
if (m.direction !== "in" && m.direction !== "out") return null;
|
|
2690
|
+
if (m.gesture !== "volume" && m.gesture !== "build") return null;
|
|
2691
|
+
return {
|
|
2692
|
+
direction: m.direction,
|
|
2693
|
+
gesture: m.gesture,
|
|
2694
|
+
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2695
|
+
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2696
|
+
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
2697
|
+
soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
|
|
2698
|
+
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function parseFades(sceneData) {
|
|
2702
|
+
const out = [];
|
|
2703
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
2704
|
+
const match = /^track:(.+):fade$/.exec(key);
|
|
2705
|
+
if (!match) continue;
|
|
2706
|
+
const meta = asFadeMeta(val);
|
|
2707
|
+
if (!meta) continue;
|
|
2708
|
+
out.push({ dbId: match[1], meta });
|
|
2709
|
+
}
|
|
2710
|
+
return out;
|
|
2711
|
+
}
|
|
2712
|
+
function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
|
|
2713
|
+
const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
|
|
2714
|
+
if (gesture === "build") {
|
|
2715
|
+
return [
|
|
2716
|
+
{ time: 0, db: 0 },
|
|
2717
|
+
{ time: Math.round(durationSeconds * 1e3) / 1e3, db: 0 }
|
|
2718
|
+
];
|
|
2719
|
+
}
|
|
2720
|
+
const s = Math.min(0.98, Math.max(0.02, sliderPos));
|
|
2721
|
+
const round = (n) => Math.round(n * 1e3) / 1e3;
|
|
2722
|
+
const points = [];
|
|
2723
|
+
for (let i = 0; i <= steps; i++) {
|
|
2724
|
+
const x = i / steps;
|
|
2725
|
+
const time = round(x * durationSeconds);
|
|
2726
|
+
const theta = x <= s ? x / s * (Math.PI / 4) : Math.PI / 4 + (x - s) / (1 - s) * (Math.PI / 4);
|
|
2727
|
+
const gain = direction === "out" ? Math.cos(theta) : Math.sin(theta);
|
|
2728
|
+
points.push({ time, db: Math.round(gainToDb(gain) * 100) / 100 });
|
|
2729
|
+
}
|
|
2730
|
+
return points;
|
|
2731
|
+
}
|
|
2732
|
+
var TEXTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2733
|
+
"pads",
|
|
2734
|
+
"pad",
|
|
2735
|
+
"strings",
|
|
2736
|
+
"atmospheres",
|
|
2737
|
+
"atmosphere",
|
|
2738
|
+
"atmos",
|
|
2739
|
+
"drones",
|
|
2740
|
+
"drone",
|
|
2741
|
+
"soundscapes",
|
|
2742
|
+
"soundscape"
|
|
2743
|
+
]);
|
|
2744
|
+
function defaultFadeGesture(role) {
|
|
2745
|
+
if (!role) return "build";
|
|
2746
|
+
const norm = role.toLowerCase().replace(/[\s_-]+/g, " ").trim();
|
|
2747
|
+
if (TEXTURAL_ROLES.has(norm)) return "volume";
|
|
2748
|
+
for (const token of norm.split(" ")) {
|
|
2749
|
+
if (TEXTURAL_ROLES.has(token)) return "volume";
|
|
2750
|
+
}
|
|
2751
|
+
return "build";
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// src/components/FadeTrackRow.tsx
|
|
2755
|
+
var import_react11 = __toESM(require("react"));
|
|
2680
2756
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
2757
|
+
function FadeCaption({
|
|
2758
|
+
layer,
|
|
2759
|
+
direction,
|
|
2760
|
+
gesture
|
|
2761
|
+
}) {
|
|
2762
|
+
const tag = direction === "in" ? "Fade in" : "Fade out";
|
|
2763
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
|
|
2764
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
|
|
2765
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
|
|
2766
|
+
layer.soundLabel && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
|
|
2767
|
+
"\xB7 ",
|
|
2768
|
+
layer.soundLabel
|
|
2769
|
+
] }),
|
|
2770
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
|
|
2771
|
+
"\xB7 ",
|
|
2772
|
+
gesture
|
|
2773
|
+
] })
|
|
2774
|
+
] });
|
|
2775
|
+
}
|
|
2776
|
+
function FadeTrackRow({
|
|
2777
|
+
layer,
|
|
2778
|
+
direction,
|
|
2779
|
+
gesture,
|
|
2780
|
+
sliderPos = 0.5,
|
|
2781
|
+
onMuteToggle,
|
|
2782
|
+
onSoloToggle,
|
|
2783
|
+
onVolumeChange,
|
|
2784
|
+
onPanChange,
|
|
2785
|
+
onDelete,
|
|
2786
|
+
onSliderChange,
|
|
2787
|
+
levels,
|
|
2788
|
+
accentColor = "#9333EA"
|
|
2789
|
+
}) {
|
|
2790
|
+
const [confirmDelete, setConfirmDelete] = import_react11.default.useState(false);
|
|
2791
|
+
const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
|
|
2792
|
+
const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
|
|
2793
|
+
const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
|
|
2794
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2795
|
+
"div",
|
|
2796
|
+
{
|
|
2797
|
+
"data-testid": "fade-track-row",
|
|
2798
|
+
className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
|
|
2799
|
+
style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
|
|
2800
|
+
children: [
|
|
2801
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
|
|
2802
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2803
|
+
"span",
|
|
2804
|
+
{
|
|
2805
|
+
"data-testid": "fade-direction-badge",
|
|
2806
|
+
className: "text-[10px] font-bold uppercase tracking-wide",
|
|
2807
|
+
style: { color: accentColor },
|
|
2808
|
+
children: badge
|
|
2809
|
+
}
|
|
2810
|
+
),
|
|
2811
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2812
|
+
"button",
|
|
2813
|
+
{
|
|
2814
|
+
"data-testid": "fade-delete-button",
|
|
2815
|
+
onClick: () => setConfirmDelete(true),
|
|
2816
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
|
|
2817
|
+
title: "Delete fade",
|
|
2818
|
+
"aria-label": "Delete fade",
|
|
2819
|
+
children: "x"
|
|
2820
|
+
}
|
|
2821
|
+
)
|
|
2822
|
+
] }),
|
|
2823
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2824
|
+
TrackRow,
|
|
2825
|
+
{
|
|
2826
|
+
track: { id: layer.trackId, name: "", role: layer.role },
|
|
2827
|
+
runtimeState: layer.runtimeState,
|
|
2828
|
+
fxDetailState: EMPTY_FX_DETAIL_STATE,
|
|
2829
|
+
drawerOpen: false,
|
|
2830
|
+
drawerTab: "fx",
|
|
2831
|
+
levels,
|
|
2832
|
+
accentColor,
|
|
2833
|
+
contentSlot: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(FadeCaption, { layer, direction, gesture }),
|
|
2834
|
+
onMuteToggle,
|
|
2835
|
+
onSoloToggle,
|
|
2836
|
+
onVolumeChange,
|
|
2837
|
+
onPanChange
|
|
2838
|
+
}
|
|
2839
|
+
),
|
|
2840
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
|
|
2841
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2842
|
+
"span",
|
|
2843
|
+
{
|
|
2844
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
|
|
2845
|
+
title: leftLabel,
|
|
2846
|
+
children: leftLabel
|
|
2847
|
+
}
|
|
2848
|
+
),
|
|
2849
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2850
|
+
"input",
|
|
2851
|
+
{
|
|
2852
|
+
type: "range",
|
|
2853
|
+
"data-testid": "fade-slider",
|
|
2854
|
+
min: 0,
|
|
2855
|
+
max: 1,
|
|
2856
|
+
step: 0.01,
|
|
2857
|
+
value: sliderPos,
|
|
2858
|
+
disabled: !onSliderChange,
|
|
2859
|
+
onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
|
|
2860
|
+
style: { accentColor },
|
|
2861
|
+
className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
|
|
2862
|
+
"aria-label": "Fade position"
|
|
2863
|
+
}
|
|
2864
|
+
),
|
|
2865
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2866
|
+
"span",
|
|
2867
|
+
{
|
|
2868
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
|
|
2869
|
+
title: rightLabel,
|
|
2870
|
+
children: rightLabel
|
|
2871
|
+
}
|
|
2872
|
+
)
|
|
2873
|
+
] }),
|
|
2874
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2875
|
+
ConfirmDialog,
|
|
2876
|
+
{
|
|
2877
|
+
open: confirmDelete,
|
|
2878
|
+
title: "Delete fade?",
|
|
2879
|
+
message: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
|
|
2880
|
+
confirmLabel: "Delete",
|
|
2881
|
+
onConfirm: () => {
|
|
2882
|
+
setConfirmDelete(false);
|
|
2883
|
+
onDelete();
|
|
2884
|
+
},
|
|
2885
|
+
onCancel: () => setConfirmDelete(false),
|
|
2886
|
+
testIdPrefix: "fade-delete-confirm"
|
|
2887
|
+
}
|
|
2888
|
+
)
|
|
2889
|
+
]
|
|
2890
|
+
}
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// src/components/FadeModal.tsx
|
|
2895
|
+
var import_react12 = require("react");
|
|
2896
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2897
|
+
function shortId(dbId) {
|
|
2898
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
2899
|
+
}
|
|
2900
|
+
var normRole = (r) => (r ?? "").toLowerCase().trim();
|
|
2901
|
+
function computeOrphans(from, to, excludeSet) {
|
|
2902
|
+
const bucket = (list) => {
|
|
2903
|
+
const m = /* @__PURE__ */ new Map();
|
|
2904
|
+
for (const t of list) {
|
|
2905
|
+
const k = normRole(t.role);
|
|
2906
|
+
const arr = m.get(k);
|
|
2907
|
+
if (arr) arr.push(t);
|
|
2908
|
+
else m.set(k, [t]);
|
|
2909
|
+
}
|
|
2910
|
+
return m;
|
|
2911
|
+
};
|
|
2912
|
+
const fromByRole = bucket(from);
|
|
2913
|
+
const toByRole = bucket(to);
|
|
2914
|
+
const roles = /* @__PURE__ */ new Set([...fromByRole.keys(), ...toByRole.keys()]);
|
|
2915
|
+
const fadeOut = [];
|
|
2916
|
+
const fadeIn = [];
|
|
2917
|
+
for (const role of roles) {
|
|
2918
|
+
const f = fromByRole.get(role) ?? [];
|
|
2919
|
+
const t = toByRole.get(role) ?? [];
|
|
2920
|
+
const shared = Math.min(f.length, t.length);
|
|
2921
|
+
fadeOut.push(...f.slice(shared));
|
|
2922
|
+
fadeIn.push(...t.slice(shared));
|
|
2923
|
+
}
|
|
2924
|
+
return {
|
|
2925
|
+
fadeOut: fadeOut.filter((x) => !excludeSet.has(x.dbId)),
|
|
2926
|
+
fadeIn: fadeIn.filter((x) => !excludeSet.has(x.dbId))
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
function OrphanRow({
|
|
2930
|
+
track,
|
|
2931
|
+
gesture,
|
|
2932
|
+
selected,
|
|
2933
|
+
disabled,
|
|
2934
|
+
onSelect,
|
|
2935
|
+
testId
|
|
2936
|
+
}) {
|
|
2937
|
+
const primary = track.prompt?.trim() || track.name;
|
|
2938
|
+
const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
|
|
2939
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
2940
|
+
"button",
|
|
2941
|
+
{
|
|
2942
|
+
type: "button",
|
|
2943
|
+
role: "radio",
|
|
2944
|
+
"aria-checked": selected,
|
|
2945
|
+
"data-testid": testId,
|
|
2946
|
+
"data-value": track.dbId,
|
|
2947
|
+
onClick: onSelect,
|
|
2948
|
+
disabled,
|
|
2949
|
+
className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
|
|
2950
|
+
children: [
|
|
2951
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
2952
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
2953
|
+
]
|
|
2954
|
+
}
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
function FadeModal({
|
|
2958
|
+
host,
|
|
2959
|
+
open,
|
|
2960
|
+
fromSceneId,
|
|
2961
|
+
toSceneId,
|
|
2962
|
+
fromSceneName,
|
|
2963
|
+
toSceneName,
|
|
2964
|
+
excludeSourceDbIds,
|
|
2965
|
+
onClose,
|
|
2966
|
+
onCreate,
|
|
2967
|
+
testIdPrefix = "fade-modal"
|
|
2968
|
+
}) {
|
|
2969
|
+
const [load, setLoad] = (0, import_react12.useState)({ status: "loading" });
|
|
2970
|
+
const [selectedDbId, setSelectedDbId] = (0, import_react12.useState)("");
|
|
2971
|
+
const [isCreating, setIsCreating] = (0, import_react12.useState)(false);
|
|
2972
|
+
const [error, setError] = (0, import_react12.useState)(null);
|
|
2973
|
+
const [fromName, setFromName] = (0, import_react12.useState)(null);
|
|
2974
|
+
const [toName, setToName] = (0, import_react12.useState)(null);
|
|
2975
|
+
const cancelRef = (0, import_react12.useRef)(null);
|
|
2976
|
+
const refresh = (0, import_react12.useCallback)(async () => {
|
|
2977
|
+
if (!host.listSceneFamilyTracks) {
|
|
2978
|
+
setLoad({ status: "error", message: "This host does not support fades." });
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
setLoad({ status: "loading" });
|
|
2982
|
+
try {
|
|
2983
|
+
const [from, to, fName, tName] = await Promise.all([
|
|
2984
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
2985
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
2986
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
2987
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null)
|
|
2988
|
+
]);
|
|
2989
|
+
setFromName(fName);
|
|
2990
|
+
setToName(tName);
|
|
2991
|
+
setLoad({ status: "ready", from, to });
|
|
2992
|
+
} catch (err) {
|
|
2993
|
+
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2994
|
+
}
|
|
2995
|
+
}, [host, fromSceneId, toSceneId]);
|
|
2996
|
+
(0, import_react12.useEffect)(() => {
|
|
2997
|
+
if (open) {
|
|
2998
|
+
setError(null);
|
|
2999
|
+
setIsCreating(false);
|
|
3000
|
+
setSelectedDbId("");
|
|
3001
|
+
void refresh();
|
|
3002
|
+
}
|
|
3003
|
+
}, [open, refresh]);
|
|
3004
|
+
const excludeSet = (0, import_react12.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3005
|
+
const { fadeOut, fadeIn } = (0, import_react12.useMemo)(
|
|
3006
|
+
() => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
|
|
3007
|
+
[load, excludeSet]
|
|
3008
|
+
);
|
|
3009
|
+
const allOrphans = (0, import_react12.useMemo)(
|
|
3010
|
+
() => [
|
|
3011
|
+
...fadeOut.map((t) => ({ track: t, direction: "out" })),
|
|
3012
|
+
...fadeIn.map((t) => ({ track: t, direction: "in" }))
|
|
3013
|
+
],
|
|
3014
|
+
[fadeOut, fadeIn]
|
|
3015
|
+
);
|
|
3016
|
+
(0, import_react12.useEffect)(() => {
|
|
3017
|
+
if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
|
|
3018
|
+
setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
|
|
3019
|
+
}
|
|
3020
|
+
}, [allOrphans, selectedDbId]);
|
|
3021
|
+
const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
|
|
3022
|
+
const canCreate = !isCreating && !!selected;
|
|
3023
|
+
const handleClose = (0, import_react12.useCallback)(() => {
|
|
3024
|
+
if (!isCreating) onClose();
|
|
3025
|
+
}, [isCreating, onClose]);
|
|
3026
|
+
const handleCreate = (0, import_react12.useCallback)(async () => {
|
|
3027
|
+
if (!selected) return;
|
|
3028
|
+
setIsCreating(true);
|
|
3029
|
+
setError(null);
|
|
3030
|
+
try {
|
|
3031
|
+
await onCreate(
|
|
3032
|
+
{ dbId: selected.track.dbId, name: selected.track.name, role: selected.track.role },
|
|
3033
|
+
selected.direction,
|
|
3034
|
+
defaultFadeGesture(selected.track.role)
|
|
3035
|
+
);
|
|
3036
|
+
onClose();
|
|
3037
|
+
} catch (err) {
|
|
3038
|
+
setError(err instanceof Error ? err.message : "Failed to create fade.");
|
|
3039
|
+
setIsCreating(false);
|
|
3040
|
+
}
|
|
3041
|
+
}, [selected, onCreate, onClose]);
|
|
3042
|
+
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
3043
|
+
const toLabel = toName ?? toSceneName ?? null;
|
|
3044
|
+
if (!open) return null;
|
|
3045
|
+
const renderSection = (heading, list, section) => {
|
|
3046
|
+
if (list.length === 0) return null;
|
|
3047
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "block", children: [
|
|
3048
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
|
|
3049
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3050
|
+
"div",
|
|
3051
|
+
{
|
|
3052
|
+
role: "radiogroup",
|
|
3053
|
+
"aria-label": heading,
|
|
3054
|
+
"data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
|
|
3055
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3056
|
+
children: list.map((t) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3057
|
+
OrphanRow,
|
|
3058
|
+
{
|
|
3059
|
+
track: t,
|
|
3060
|
+
gesture: defaultFadeGesture(t.role),
|
|
3061
|
+
selected: t.dbId === selectedDbId,
|
|
3062
|
+
disabled: isCreating,
|
|
3063
|
+
onSelect: () => setSelectedDbId(t.dbId),
|
|
3064
|
+
testId: `${testIdPrefix}-option-${t.dbId}`
|
|
3065
|
+
},
|
|
3066
|
+
t.dbId
|
|
3067
|
+
))
|
|
3068
|
+
}
|
|
3069
|
+
)
|
|
3070
|
+
] });
|
|
3071
|
+
};
|
|
3072
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
3073
|
+
"div",
|
|
3074
|
+
{
|
|
3075
|
+
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
3076
|
+
onClick: (e) => e.stopPropagation(),
|
|
3077
|
+
"data-testid": `${testIdPrefix}-box`,
|
|
3078
|
+
children: [
|
|
3079
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
|
|
3080
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
3081
|
+
"Tracks with no counterpart between",
|
|
3082
|
+
" ",
|
|
3083
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
3084
|
+
" and",
|
|
3085
|
+
" ",
|
|
3086
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
3087
|
+
" can gracefully fade out (leaving) or fade in (entering) across this transition."
|
|
3088
|
+
] }),
|
|
3089
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3090
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3091
|
+
load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
3092
|
+
renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
|
|
3093
|
+
renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
|
|
3094
|
+
] })),
|
|
3095
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3096
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3097
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3098
|
+
"button",
|
|
3099
|
+
{
|
|
3100
|
+
ref: cancelRef,
|
|
3101
|
+
"data-testid": `${testIdPrefix}-cancel`,
|
|
3102
|
+
onClick: onClose,
|
|
3103
|
+
disabled: isCreating,
|
|
3104
|
+
className: "px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50",
|
|
3105
|
+
children: "Cancel"
|
|
3106
|
+
}
|
|
3107
|
+
),
|
|
3108
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3109
|
+
"button",
|
|
3110
|
+
{
|
|
3111
|
+
"data-testid": `${testIdPrefix}-confirm`,
|
|
3112
|
+
onClick: handleCreate,
|
|
3113
|
+
disabled: !canCreate,
|
|
3114
|
+
className: `px-3 py-1 text-xs rounded-sm border transition-colors ${canCreate ? "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"}`,
|
|
3115
|
+
children: isCreating ? "Generating fade\u2026" : "Create fade"
|
|
3116
|
+
}
|
|
3117
|
+
)
|
|
3118
|
+
] })
|
|
3119
|
+
]
|
|
3120
|
+
}
|
|
3121
|
+
) });
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/components/ImportTrackModal.tsx
|
|
3125
|
+
var import_react13 = require("react");
|
|
3126
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2681
3127
|
function ImportTrackModal({
|
|
2682
3128
|
host,
|
|
2683
3129
|
open,
|
|
@@ -2689,10 +3135,10 @@ function ImportTrackModal({
|
|
|
2689
3135
|
onPick,
|
|
2690
3136
|
onPortTrack
|
|
2691
3137
|
}) {
|
|
2692
|
-
const [load, setLoad] = (0,
|
|
2693
|
-
const [selectedSceneId, setSelectedSceneId] = (0,
|
|
2694
|
-
const [importingTrackId, setImportingTrackId] = (0,
|
|
2695
|
-
const refresh = (0,
|
|
3138
|
+
const [load, setLoad] = (0, import_react13.useState)({ status: "loading" });
|
|
3139
|
+
const [selectedSceneId, setSelectedSceneId] = (0, import_react13.useState)(null);
|
|
3140
|
+
const [importingTrackId, setImportingTrackId] = (0, import_react13.useState)(null);
|
|
3141
|
+
const refresh = (0, import_react13.useCallback)(async () => {
|
|
2696
3142
|
if (!host.listImportableTracks) {
|
|
2697
3143
|
setLoad({ status: "error", message: "This host does not support importing tracks." });
|
|
2698
3144
|
return;
|
|
@@ -2708,14 +3154,14 @@ function ImportTrackModal({
|
|
|
2708
3154
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
|
|
2709
3155
|
}
|
|
2710
3156
|
}, [host, mode, onPortTrack]);
|
|
2711
|
-
(0,
|
|
3157
|
+
(0, import_react13.useEffect)(() => {
|
|
2712
3158
|
if (open) {
|
|
2713
3159
|
setSelectedSceneId(null);
|
|
2714
3160
|
setImportingTrackId(null);
|
|
2715
3161
|
void refresh();
|
|
2716
3162
|
}
|
|
2717
3163
|
}, [open, refresh]);
|
|
2718
|
-
const handleImport = (0,
|
|
3164
|
+
const handleImport = (0, import_react13.useCallback)(
|
|
2719
3165
|
async (track, sourceSceneId, sceneName, isSameScene) => {
|
|
2720
3166
|
if (isSameScene && onPortTrack) {
|
|
2721
3167
|
if (!track.importable) return;
|
|
@@ -2756,16 +3202,16 @@ function ImportTrackModal({
|
|
|
2756
3202
|
if (!open) return null;
|
|
2757
3203
|
const scenes = load.status === "ready" ? load.scenes : [];
|
|
2758
3204
|
const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
|
|
2759
|
-
return /* @__PURE__ */ (0,
|
|
3205
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2760
3206
|
"div",
|
|
2761
3207
|
{
|
|
2762
3208
|
className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
2763
3209
|
onClick: (e) => e.stopPropagation(),
|
|
2764
3210
|
"data-testid": `${testIdPrefix}-modal`,
|
|
2765
3211
|
children: [
|
|
2766
|
-
/* @__PURE__ */ (0,
|
|
2767
|
-
/* @__PURE__ */ (0,
|
|
2768
|
-
selectedScene && /* @__PURE__ */ (0,
|
|
3212
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
|
|
3213
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
3214
|
+
selectedScene && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2769
3215
|
"button",
|
|
2770
3216
|
{
|
|
2771
3217
|
className: "text-sas-muted hover:text-sas-accent text-xs",
|
|
@@ -2774,9 +3220,9 @@ function ImportTrackModal({
|
|
|
2774
3220
|
children: "\u2190"
|
|
2775
3221
|
}
|
|
2776
3222
|
),
|
|
2777
|
-
/* @__PURE__ */ (0,
|
|
3223
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
|
|
2778
3224
|
] }),
|
|
2779
|
-
/* @__PURE__ */ (0,
|
|
3225
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2780
3226
|
"button",
|
|
2781
3227
|
{
|
|
2782
3228
|
className: "text-sas-muted hover:text-sas-accent text-sm",
|
|
@@ -2786,30 +3232,30 @@ function ImportTrackModal({
|
|
|
2786
3232
|
}
|
|
2787
3233
|
)
|
|
2788
3234
|
] }),
|
|
2789
|
-
/* @__PURE__ */ (0,
|
|
2790
|
-
load.status === "loading" && /* @__PURE__ */ (0,
|
|
2791
|
-
load.status === "error" && /* @__PURE__ */ (0,
|
|
2792
|
-
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0,
|
|
2793
|
-
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0,
|
|
3235
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
|
|
3236
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
|
|
3237
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
3238
|
+
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
|
|
3239
|
+
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2794
3240
|
"button",
|
|
2795
3241
|
{
|
|
2796
3242
|
className: "w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
|
|
2797
3243
|
onClick: () => setSelectedSceneId(scene.sceneId),
|
|
2798
3244
|
"data-testid": `${testIdPrefix}-scene`,
|
|
2799
3245
|
children: [
|
|
2800
|
-
/* @__PURE__ */ (0,
|
|
2801
|
-
/* @__PURE__ */ (0,
|
|
3246
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "truncate", children: scene.sceneName }),
|
|
3247
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-sas-muted", children: [
|
|
2802
3248
|
scene.tracks.length,
|
|
2803
3249
|
" \u2192"
|
|
2804
3250
|
] })
|
|
2805
3251
|
]
|
|
2806
3252
|
}
|
|
2807
3253
|
) }, scene.sceneId)) }),
|
|
2808
|
-
selectedScene && /* @__PURE__ */ (0,
|
|
3254
|
+
selectedScene && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
|
|
2809
3255
|
const busy = importingTrackId === track.trackId;
|
|
2810
3256
|
const gated = mode === "track" && !track.importable;
|
|
2811
3257
|
const disabled = gated || busy;
|
|
2812
|
-
return /* @__PURE__ */ (0,
|
|
3258
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2813
3259
|
"button",
|
|
2814
3260
|
{
|
|
2815
3261
|
className: `w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${disabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent"}`,
|
|
@@ -2819,14 +3265,14 @@ function ImportTrackModal({
|
|
|
2819
3265
|
"data-testid": `${testIdPrefix}-track`,
|
|
2820
3266
|
"data-importable": mode === "sound" || track.importable ? "true" : "false",
|
|
2821
3267
|
children: [
|
|
2822
|
-
/* @__PURE__ */ (0,
|
|
3268
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "truncate", children: [
|
|
2823
3269
|
track.name,
|
|
2824
|
-
track.role ? /* @__PURE__ */ (0,
|
|
3270
|
+
track.role ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-sas-muted", children: [
|
|
2825
3271
|
" \xB7 ",
|
|
2826
3272
|
track.role
|
|
2827
3273
|
] }) : null
|
|
2828
3274
|
] }),
|
|
2829
|
-
busy ? /* @__PURE__ */ (0,
|
|
3275
|
+
busy ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
|
|
2830
3276
|
]
|
|
2831
3277
|
}
|
|
2832
3278
|
) }, track.dbId);
|
|
@@ -2838,8 +3284,38 @@ function ImportTrackModal({
|
|
|
2838
3284
|
}
|
|
2839
3285
|
|
|
2840
3286
|
// src/components/CrossfadeModal.tsx
|
|
2841
|
-
var
|
|
2842
|
-
var
|
|
3287
|
+
var import_react14 = require("react");
|
|
3288
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
3289
|
+
function shortId2(dbId) {
|
|
3290
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3291
|
+
}
|
|
3292
|
+
function CandidateRow({
|
|
3293
|
+
track,
|
|
3294
|
+
selected,
|
|
3295
|
+
disabled,
|
|
3296
|
+
onSelect,
|
|
3297
|
+
testId
|
|
3298
|
+
}) {
|
|
3299
|
+
const primary = track.prompt?.trim() || track.name;
|
|
3300
|
+
const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
|
|
3301
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
3302
|
+
"button",
|
|
3303
|
+
{
|
|
3304
|
+
type: "button",
|
|
3305
|
+
role: "radio",
|
|
3306
|
+
"aria-checked": selected,
|
|
3307
|
+
"data-testid": testId,
|
|
3308
|
+
"data-value": track.dbId,
|
|
3309
|
+
onClick: onSelect,
|
|
3310
|
+
disabled,
|
|
3311
|
+
className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
|
|
3312
|
+
children: [
|
|
3313
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
3314
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
3315
|
+
]
|
|
3316
|
+
}
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
2843
3319
|
function CrossfadeModal({
|
|
2844
3320
|
host,
|
|
2845
3321
|
open,
|
|
@@ -2852,15 +3328,15 @@ function CrossfadeModal({
|
|
|
2852
3328
|
onCreate,
|
|
2853
3329
|
testIdPrefix = "crossfade-modal"
|
|
2854
3330
|
}) {
|
|
2855
|
-
const [load, setLoad] = (0,
|
|
2856
|
-
const [originDbId, setOriginDbId] = (0,
|
|
2857
|
-
const [targetDbId, setTargetDbId] = (0,
|
|
2858
|
-
const [isCreating, setIsCreating] = (0,
|
|
2859
|
-
const [error, setError] = (0,
|
|
2860
|
-
const [fromName, setFromName] = (0,
|
|
2861
|
-
const [toName, setToName] = (0,
|
|
2862
|
-
const cancelRef = (0,
|
|
2863
|
-
const refresh = (0,
|
|
3331
|
+
const [load, setLoad] = (0, import_react14.useState)({ status: "loading" });
|
|
3332
|
+
const [originDbId, setOriginDbId] = (0, import_react14.useState)("");
|
|
3333
|
+
const [targetDbId, setTargetDbId] = (0, import_react14.useState)("");
|
|
3334
|
+
const [isCreating, setIsCreating] = (0, import_react14.useState)(false);
|
|
3335
|
+
const [error, setError] = (0, import_react14.useState)(null);
|
|
3336
|
+
const [fromName, setFromName] = (0, import_react14.useState)(null);
|
|
3337
|
+
const [toName, setToName] = (0, import_react14.useState)(null);
|
|
3338
|
+
const cancelRef = (0, import_react14.useRef)(null);
|
|
3339
|
+
const refresh = (0, import_react14.useCallback)(async () => {
|
|
2864
3340
|
if (!host.listSceneFamilyTracks) {
|
|
2865
3341
|
setLoad({ status: "error", message: "This host does not support crossfade tracks." });
|
|
2866
3342
|
return;
|
|
@@ -2880,7 +3356,7 @@ function CrossfadeModal({
|
|
|
2880
3356
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2881
3357
|
}
|
|
2882
3358
|
}, [host, fromSceneId, toSceneId]);
|
|
2883
|
-
(0,
|
|
3359
|
+
(0, import_react14.useEffect)(() => {
|
|
2884
3360
|
if (open) {
|
|
2885
3361
|
setError(null);
|
|
2886
3362
|
setIsCreating(false);
|
|
@@ -2889,21 +3365,21 @@ function CrossfadeModal({
|
|
|
2889
3365
|
void refresh();
|
|
2890
3366
|
}
|
|
2891
3367
|
}, [open, refresh]);
|
|
2892
|
-
const excludeSet = (0,
|
|
2893
|
-
const originCandidates = (0,
|
|
3368
|
+
const excludeSet = (0, import_react14.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3369
|
+
const originCandidates = (0, import_react14.useMemo)(
|
|
2894
3370
|
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
2895
3371
|
[load, excludeSet]
|
|
2896
3372
|
);
|
|
2897
|
-
const targetCandidates = (0,
|
|
3373
|
+
const targetCandidates = (0, import_react14.useMemo)(
|
|
2898
3374
|
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
2899
3375
|
[load, excludeSet]
|
|
2900
3376
|
);
|
|
2901
|
-
(0,
|
|
3377
|
+
(0, import_react14.useEffect)(() => {
|
|
2902
3378
|
if (!originCandidates.some((t) => t.dbId === originDbId)) {
|
|
2903
3379
|
setOriginDbId(originCandidates[0]?.dbId ?? "");
|
|
2904
3380
|
}
|
|
2905
3381
|
}, [originCandidates, originDbId]);
|
|
2906
|
-
(0,
|
|
3382
|
+
(0, import_react14.useEffect)(() => {
|
|
2907
3383
|
if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
|
|
2908
3384
|
setTargetDbId(targetCandidates[0]?.dbId ?? "");
|
|
2909
3385
|
}
|
|
@@ -2911,10 +3387,10 @@ function CrossfadeModal({
|
|
|
2911
3387
|
const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
|
|
2912
3388
|
const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
|
|
2913
3389
|
const canCreate = !isCreating && !!originTrack && !!targetTrack;
|
|
2914
|
-
const handleClose = (0,
|
|
3390
|
+
const handleClose = (0, import_react14.useCallback)(() => {
|
|
2915
3391
|
if (!isCreating) onClose();
|
|
2916
3392
|
}, [isCreating, onClose]);
|
|
2917
|
-
const handleCreate = (0,
|
|
3393
|
+
const handleCreate = (0, import_react14.useCallback)(async () => {
|
|
2918
3394
|
if (!originTrack || !targetTrack) return;
|
|
2919
3395
|
setIsCreating(true);
|
|
2920
3396
|
setError(null);
|
|
@@ -2932,26 +3408,26 @@ function CrossfadeModal({
|
|
|
2932
3408
|
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
2933
3409
|
const toLabel = toName ?? toSceneName ?? null;
|
|
2934
3410
|
if (!open) return null;
|
|
2935
|
-
return /* @__PURE__ */ (0,
|
|
3411
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
2936
3412
|
"div",
|
|
2937
3413
|
{
|
|
2938
3414
|
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2939
3415
|
onClick: (e) => e.stopPropagation(),
|
|
2940
3416
|
"data-testid": `${testIdPrefix}-box`,
|
|
2941
3417
|
children: [
|
|
2942
|
-
/* @__PURE__ */ (0,
|
|
2943
|
-
/* @__PURE__ */ (0,
|
|
3418
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
|
|
3419
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2944
3420
|
"Bridge a track from",
|
|
2945
3421
|
" ",
|
|
2946
|
-
/* @__PURE__ */ (0,
|
|
3422
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
2947
3423
|
" into one from",
|
|
2948
3424
|
" ",
|
|
2949
|
-
/* @__PURE__ */ (0,
|
|
3425
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
2950
3426
|
". Both layers share one generated part; each keeps its own preset."
|
|
2951
3427
|
] }),
|
|
2952
|
-
load.status === "loading" && /* @__PURE__ */ (0,
|
|
2953
|
-
load.status === "error" && /* @__PURE__ */ (0,
|
|
2954
|
-
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0,
|
|
3428
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3429
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3430
|
+
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
2955
3431
|
"div",
|
|
2956
3432
|
{
|
|
2957
3433
|
className: "text-xs text-sas-muted py-4 text-center",
|
|
@@ -2962,55 +3438,67 @@ function CrossfadeModal({
|
|
|
2962
3438
|
". Add one (or free one from another crossfade) first."
|
|
2963
3439
|
]
|
|
2964
3440
|
}
|
|
2965
|
-
) : /* @__PURE__ */ (0,
|
|
2966
|
-
/* @__PURE__ */ (0,
|
|
2967
|
-
/* @__PURE__ */ (0,
|
|
3441
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
|
|
3442
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "block", children: [
|
|
3443
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2968
3444
|
"Origin ",
|
|
2969
3445
|
fromLabel ? `(${fromLabel})` : "(top)"
|
|
2970
3446
|
] }),
|
|
2971
|
-
/* @__PURE__ */ (0,
|
|
2972
|
-
"
|
|
3447
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3448
|
+
"div",
|
|
2973
3449
|
{
|
|
2974
|
-
"
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3450
|
+
role: "radiogroup",
|
|
3451
|
+
"aria-label": "Origin track",
|
|
3452
|
+
"data-testid": `${testIdPrefix}-origin-list`,
|
|
3453
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3454
|
+
children: originCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3455
|
+
CandidateRow,
|
|
3456
|
+
{
|
|
3457
|
+
track: t,
|
|
3458
|
+
selected: t.dbId === originDbId,
|
|
3459
|
+
disabled: isCreating,
|
|
3460
|
+
onSelect: () => setOriginDbId(t.dbId),
|
|
3461
|
+
testId: `${testIdPrefix}-origin-option-${t.dbId}`
|
|
3462
|
+
},
|
|
3463
|
+
t.dbId
|
|
3464
|
+
))
|
|
2983
3465
|
}
|
|
2984
3466
|
)
|
|
2985
3467
|
] }),
|
|
2986
|
-
/* @__PURE__ */ (0,
|
|
2987
|
-
/* @__PURE__ */ (0,
|
|
3468
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "block", children: [
|
|
3469
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2988
3470
|
"Target ",
|
|
2989
3471
|
toLabel ? `(${toLabel})` : "(bottom)"
|
|
2990
3472
|
] }),
|
|
2991
|
-
targetCandidates.length === 0 ? /* @__PURE__ */ (0,
|
|
3473
|
+
targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
|
|
2992
3474
|
"No available tracks in ",
|
|
2993
3475
|
toLabel ?? "the target scene",
|
|
2994
3476
|
" to crossfade into."
|
|
2995
|
-
] }) : /* @__PURE__ */ (0,
|
|
2996
|
-
"
|
|
3477
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3478
|
+
"div",
|
|
2997
3479
|
{
|
|
2998
|
-
"
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3480
|
+
role: "radiogroup",
|
|
3481
|
+
"aria-label": "Target track",
|
|
3482
|
+
"data-testid": `${testIdPrefix}-target-list`,
|
|
3483
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3484
|
+
children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3485
|
+
CandidateRow,
|
|
3486
|
+
{
|
|
3487
|
+
track: t,
|
|
3488
|
+
selected: t.dbId === targetDbId,
|
|
3489
|
+
disabled: isCreating,
|
|
3490
|
+
onSelect: () => setTargetDbId(t.dbId),
|
|
3491
|
+
testId: `${testIdPrefix}-target-option-${t.dbId}`
|
|
3492
|
+
},
|
|
3493
|
+
t.dbId
|
|
3494
|
+
))
|
|
3007
3495
|
}
|
|
3008
3496
|
)
|
|
3009
3497
|
] })
|
|
3010
3498
|
] })),
|
|
3011
|
-
error && /* @__PURE__ */ (0,
|
|
3012
|
-
/* @__PURE__ */ (0,
|
|
3013
|
-
/* @__PURE__ */ (0,
|
|
3499
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3500
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3501
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3014
3502
|
"button",
|
|
3015
3503
|
{
|
|
3016
3504
|
ref: cancelRef,
|
|
@@ -3021,7 +3509,7 @@ function CrossfadeModal({
|
|
|
3021
3509
|
children: "Cancel"
|
|
3022
3510
|
}
|
|
3023
3511
|
),
|
|
3024
|
-
/* @__PURE__ */ (0,
|
|
3512
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3025
3513
|
"button",
|
|
3026
3514
|
{
|
|
3027
3515
|
"data-testid": `${testIdPrefix}-confirm`,
|
|
@@ -3038,8 +3526,8 @@ function CrossfadeModal({
|
|
|
3038
3526
|
}
|
|
3039
3527
|
|
|
3040
3528
|
// src/components/DownloadPackButton.tsx
|
|
3041
|
-
var
|
|
3042
|
-
var
|
|
3529
|
+
var import_react15 = require("react");
|
|
3530
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3043
3531
|
function formatSize(bytes) {
|
|
3044
3532
|
if (!bytes || bytes <= 0) return "";
|
|
3045
3533
|
const gb = bytes / 1024 ** 3;
|
|
@@ -3055,10 +3543,10 @@ var DownloadPackButton = ({
|
|
|
3055
3543
|
variant = "compact",
|
|
3056
3544
|
onDownloadComplete
|
|
3057
3545
|
}) => {
|
|
3058
|
-
const [status, setStatus] = (0,
|
|
3059
|
-
const [progress, setProgress] = (0,
|
|
3060
|
-
const [errorMessage, setErrorMessage] = (0,
|
|
3061
|
-
(0,
|
|
3546
|
+
const [status, setStatus] = (0, import_react15.useState)("idle");
|
|
3547
|
+
const [progress, setProgress] = (0, import_react15.useState)(0);
|
|
3548
|
+
const [errorMessage, setErrorMessage] = (0, import_react15.useState)(null);
|
|
3549
|
+
(0, import_react15.useEffect)(() => {
|
|
3062
3550
|
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
3063
3551
|
setStatus(p.status);
|
|
3064
3552
|
setProgress(p.progress);
|
|
@@ -3073,7 +3561,7 @@ var DownloadPackButton = ({
|
|
|
3073
3561
|
});
|
|
3074
3562
|
return unsub;
|
|
3075
3563
|
}, [host, packId, onDownloadComplete]);
|
|
3076
|
-
const handleClick = (0,
|
|
3564
|
+
const handleClick = (0, import_react15.useCallback)(async () => {
|
|
3077
3565
|
if (status !== "idle" && status !== "error") return;
|
|
3078
3566
|
try {
|
|
3079
3567
|
setStatus("downloading");
|
|
@@ -3127,8 +3615,8 @@ var DownloadPackButton = ({
|
|
|
3127
3615
|
} else {
|
|
3128
3616
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3129
3617
|
}
|
|
3130
|
-
return /* @__PURE__ */ (0,
|
|
3131
|
-
/* @__PURE__ */ (0,
|
|
3618
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
|
|
3619
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3132
3620
|
"button",
|
|
3133
3621
|
{
|
|
3134
3622
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -3139,12 +3627,12 @@ var DownloadPackButton = ({
|
|
|
3139
3627
|
children: buttonLabel
|
|
3140
3628
|
}
|
|
3141
3629
|
),
|
|
3142
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0,
|
|
3630
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3143
3631
|
] });
|
|
3144
3632
|
};
|
|
3145
3633
|
|
|
3146
3634
|
// src/components/SamplePackCTACard.tsx
|
|
3147
|
-
var
|
|
3635
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3148
3636
|
var SamplePackCTACard = ({
|
|
3149
3637
|
host,
|
|
3150
3638
|
pack,
|
|
@@ -3152,7 +3640,7 @@ var SamplePackCTACard = ({
|
|
|
3152
3640
|
onDownloadComplete
|
|
3153
3641
|
}) => {
|
|
3154
3642
|
if (status === "checking") {
|
|
3155
|
-
return /* @__PURE__ */ (0,
|
|
3643
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3156
3644
|
"div",
|
|
3157
3645
|
{
|
|
3158
3646
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -3163,16 +3651,16 @@ var SamplePackCTACard = ({
|
|
|
3163
3651
|
}
|
|
3164
3652
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
3165
3653
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
3166
|
-
return /* @__PURE__ */ (0,
|
|
3654
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3167
3655
|
"div",
|
|
3168
3656
|
{
|
|
3169
3657
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3170
3658
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3171
3659
|
children: [
|
|
3172
|
-
/* @__PURE__ */ (0,
|
|
3173
|
-
/* @__PURE__ */ (0,
|
|
3174
|
-
/* @__PURE__ */ (0,
|
|
3175
|
-
/* @__PURE__ */ (0,
|
|
3660
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
3661
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
3662
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
3663
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3176
3664
|
DownloadPackButton,
|
|
3177
3665
|
{
|
|
3178
3666
|
host,
|
|
@@ -3189,7 +3677,7 @@ var SamplePackCTACard = ({
|
|
|
3189
3677
|
};
|
|
3190
3678
|
|
|
3191
3679
|
// src/components/WaveformView.tsx
|
|
3192
|
-
var
|
|
3680
|
+
var import_react16 = require("react");
|
|
3193
3681
|
|
|
3194
3682
|
// src/components/waveform.ts
|
|
3195
3683
|
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
@@ -3252,7 +3740,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
3252
3740
|
}
|
|
3253
3741
|
|
|
3254
3742
|
// src/components/WaveformView.tsx
|
|
3255
|
-
var
|
|
3743
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3256
3744
|
var WaveformView = ({
|
|
3257
3745
|
host,
|
|
3258
3746
|
filePath,
|
|
@@ -3261,9 +3749,9 @@ var WaveformView = ({
|
|
|
3261
3749
|
fillStyle,
|
|
3262
3750
|
targetSamples
|
|
3263
3751
|
}) => {
|
|
3264
|
-
const canvasRef = (0,
|
|
3265
|
-
const [peaks, setPeaks] = (0,
|
|
3266
|
-
(0,
|
|
3752
|
+
const canvasRef = (0, import_react16.useRef)(null);
|
|
3753
|
+
const [peaks, setPeaks] = (0, import_react16.useState)(null);
|
|
3754
|
+
(0, import_react16.useEffect)(() => {
|
|
3267
3755
|
let cancelled = false;
|
|
3268
3756
|
let audioContext = null;
|
|
3269
3757
|
(async () => {
|
|
@@ -3289,7 +3777,7 @@ var WaveformView = ({
|
|
|
3289
3777
|
cancelled = true;
|
|
3290
3778
|
};
|
|
3291
3779
|
}, [host, filePath, bins, targetSamples]);
|
|
3292
|
-
(0,
|
|
3780
|
+
(0, import_react16.useEffect)(() => {
|
|
3293
3781
|
if (!peaks) return;
|
|
3294
3782
|
const canvas = canvasRef.current;
|
|
3295
3783
|
if (!canvas) return;
|
|
@@ -3300,7 +3788,7 @@ var WaveformView = ({
|
|
|
3300
3788
|
observer.observe(canvas);
|
|
3301
3789
|
return () => observer.disconnect();
|
|
3302
3790
|
}, [peaks, fillStyle]);
|
|
3303
|
-
return /* @__PURE__ */ (0,
|
|
3791
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3304
3792
|
"canvas",
|
|
3305
3793
|
{
|
|
3306
3794
|
ref: canvasRef,
|
|
@@ -3311,8 +3799,8 @@ var WaveformView = ({
|
|
|
3311
3799
|
};
|
|
3312
3800
|
|
|
3313
3801
|
// src/components/ScrollingWaveform.tsx
|
|
3314
|
-
var
|
|
3315
|
-
var
|
|
3802
|
+
var import_react17 = require("react");
|
|
3803
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3316
3804
|
var ScrollingWaveform = ({
|
|
3317
3805
|
getPeakDb,
|
|
3318
3806
|
active,
|
|
@@ -3320,11 +3808,11 @@ var ScrollingWaveform = ({
|
|
|
3320
3808
|
className,
|
|
3321
3809
|
fillStyle
|
|
3322
3810
|
}) => {
|
|
3323
|
-
const canvasRef = (0,
|
|
3324
|
-
const ringRef = (0,
|
|
3325
|
-
const writeIdxRef = (0,
|
|
3326
|
-
const rafRef = (0,
|
|
3327
|
-
(0,
|
|
3811
|
+
const canvasRef = (0, import_react17.useRef)(null);
|
|
3812
|
+
const ringRef = (0, import_react17.useRef)(new Float32Array(columns));
|
|
3813
|
+
const writeIdxRef = (0, import_react17.useRef)(0);
|
|
3814
|
+
const rafRef = (0, import_react17.useRef)(null);
|
|
3815
|
+
(0, import_react17.useEffect)(() => {
|
|
3328
3816
|
if (ringRef.current.length !== columns) {
|
|
3329
3817
|
const next = new Float32Array(columns);
|
|
3330
3818
|
const prev = ringRef.current;
|
|
@@ -3336,7 +3824,7 @@ var ScrollingWaveform = ({
|
|
|
3336
3824
|
writeIdxRef.current = writeIdxRef.current % columns;
|
|
3337
3825
|
}
|
|
3338
3826
|
}, [columns]);
|
|
3339
|
-
(0,
|
|
3827
|
+
(0, import_react17.useEffect)(() => {
|
|
3340
3828
|
if (!active) {
|
|
3341
3829
|
if (rafRef.current !== null) {
|
|
3342
3830
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -3388,7 +3876,7 @@ var ScrollingWaveform = ({
|
|
|
3388
3876
|
}
|
|
3389
3877
|
};
|
|
3390
3878
|
}, [active, getPeakDb, fillStyle]);
|
|
3391
|
-
return /* @__PURE__ */ (0,
|
|
3879
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
3392
3880
|
"canvas",
|
|
3393
3881
|
{
|
|
3394
3882
|
ref: canvasRef,
|
|
@@ -3399,8 +3887,8 @@ var ScrollingWaveform = ({
|
|
|
3399
3887
|
};
|
|
3400
3888
|
|
|
3401
3889
|
// src/components/OffsetScrubber.tsx
|
|
3402
|
-
var
|
|
3403
|
-
var
|
|
3890
|
+
var import_react18 = require("react");
|
|
3891
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3404
3892
|
var SLIDER_HEIGHT_PX = 28;
|
|
3405
3893
|
var TICK_HEIGHT_PX = 14;
|
|
3406
3894
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -3413,40 +3901,40 @@ function OffsetScrubber({
|
|
|
3413
3901
|
onChange,
|
|
3414
3902
|
disabled = false
|
|
3415
3903
|
}) {
|
|
3416
|
-
const trackRef = (0,
|
|
3417
|
-
const [draftOffset, setDraftOffset] = (0,
|
|
3418
|
-
const [isDragging, setIsDragging] = (0,
|
|
3419
|
-
(0,
|
|
3904
|
+
const trackRef = (0, import_react18.useRef)(null);
|
|
3905
|
+
const [draftOffset, setDraftOffset] = (0, import_react18.useState)(offsetSamples);
|
|
3906
|
+
const [isDragging, setIsDragging] = (0, import_react18.useState)(false);
|
|
3907
|
+
(0, import_react18.useEffect)(() => {
|
|
3420
3908
|
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3421
3909
|
}, [offsetSamples, isDragging]);
|
|
3422
3910
|
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3423
3911
|
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3424
|
-
const beatsForRange = (0,
|
|
3912
|
+
const beatsForRange = (0, import_react18.useMemo)(() => {
|
|
3425
3913
|
return Math.round(60 / projectBpm * sampleRate);
|
|
3426
3914
|
}, [projectBpm, sampleRate]);
|
|
3427
3915
|
const rangeSamples = beatsForRange * meter;
|
|
3428
|
-
const sampleToFraction = (0,
|
|
3916
|
+
const sampleToFraction = (0, import_react18.useCallback)(
|
|
3429
3917
|
(sample) => {
|
|
3430
3918
|
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3431
3919
|
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
3432
3920
|
},
|
|
3433
3921
|
[rangeSamples]
|
|
3434
3922
|
);
|
|
3435
|
-
const fractionToSample = (0,
|
|
3923
|
+
const fractionToSample = (0, import_react18.useCallback)(
|
|
3436
3924
|
(fraction) => {
|
|
3437
3925
|
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3438
3926
|
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
3439
3927
|
},
|
|
3440
3928
|
[rangeSamples]
|
|
3441
3929
|
);
|
|
3442
|
-
const snapTargets = (0,
|
|
3930
|
+
const snapTargets = (0, import_react18.useMemo)(() => {
|
|
3443
3931
|
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
3444
3932
|
const downbeat = cuePoints.beats[0];
|
|
3445
3933
|
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3446
3934
|
const negatives = positives.slice(1).map((p) => -p);
|
|
3447
3935
|
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3448
3936
|
}, [cuePoints]);
|
|
3449
|
-
const snapToBeat = (0,
|
|
3937
|
+
const snapToBeat = (0, import_react18.useCallback)(
|
|
3450
3938
|
(sample) => {
|
|
3451
3939
|
if (snapTargets.length === 0) return sample;
|
|
3452
3940
|
let best = snapTargets[0];
|
|
@@ -3462,7 +3950,7 @@ function OffsetScrubber({
|
|
|
3462
3950
|
},
|
|
3463
3951
|
[snapTargets]
|
|
3464
3952
|
);
|
|
3465
|
-
const handlePointerDown = (0,
|
|
3953
|
+
const handlePointerDown = (0, import_react18.useCallback)(
|
|
3466
3954
|
(e) => {
|
|
3467
3955
|
if (disabled || !cuePoints) return;
|
|
3468
3956
|
e.preventDefault();
|
|
@@ -3496,7 +3984,7 @@ function OffsetScrubber({
|
|
|
3496
3984
|
},
|
|
3497
3985
|
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
3498
3986
|
);
|
|
3499
|
-
const handleResetToZero = (0,
|
|
3987
|
+
const handleResetToZero = (0, import_react18.useCallback)(() => {
|
|
3500
3988
|
if (disabled) return;
|
|
3501
3989
|
setDraftOffset(0);
|
|
3502
3990
|
onChange(0);
|
|
@@ -3504,7 +3992,7 @@ function OffsetScrubber({
|
|
|
3504
3992
|
const thumbFraction = sampleToFraction(draftOffset);
|
|
3505
3993
|
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
3506
3994
|
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
3507
|
-
const ticks = (0,
|
|
3995
|
+
const ticks = (0, import_react18.useMemo)(() => {
|
|
3508
3996
|
if (!cuePoints) return [];
|
|
3509
3997
|
const downbeat = cuePoints.beats[0] ?? 0;
|
|
3510
3998
|
return cuePoints.beats.map((b, i) => {
|
|
@@ -3515,9 +4003,9 @@ function OffsetScrubber({
|
|
|
3515
4003
|
});
|
|
3516
4004
|
}, [cuePoints, sampleToFraction]);
|
|
3517
4005
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
3518
|
-
return /* @__PURE__ */ (0,
|
|
3519
|
-
/* @__PURE__ */ (0,
|
|
3520
|
-
/* @__PURE__ */ (0,
|
|
4006
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
4007
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
4008
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
|
|
3521
4009
|
"div",
|
|
3522
4010
|
{
|
|
3523
4011
|
ref: trackRef,
|
|
@@ -3533,7 +4021,7 @@ function OffsetScrubber({
|
|
|
3533
4021
|
"aria-valuenow": draftOffset,
|
|
3534
4022
|
"aria-disabled": isDisabled,
|
|
3535
4023
|
children: [
|
|
3536
|
-
/* @__PURE__ */ (0,
|
|
4024
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3537
4025
|
"div",
|
|
3538
4026
|
{
|
|
3539
4027
|
"aria-hidden": "true",
|
|
@@ -3541,7 +4029,7 @@ function OffsetScrubber({
|
|
|
3541
4029
|
style: { left: "50%" }
|
|
3542
4030
|
}
|
|
3543
4031
|
),
|
|
3544
|
-
ticks.map((t) => /* @__PURE__ */ (0,
|
|
4032
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3545
4033
|
"div",
|
|
3546
4034
|
{
|
|
3547
4035
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -3556,7 +4044,7 @@ function OffsetScrubber({
|
|
|
3556
4044
|
},
|
|
3557
4045
|
t.i
|
|
3558
4046
|
)),
|
|
3559
|
-
/* @__PURE__ */ (0,
|
|
4047
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3560
4048
|
"div",
|
|
3561
4049
|
{
|
|
3562
4050
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -3573,7 +4061,7 @@ function OffsetScrubber({
|
|
|
3573
4061
|
]
|
|
3574
4062
|
}
|
|
3575
4063
|
),
|
|
3576
|
-
/* @__PURE__ */ (0,
|
|
4064
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3577
4065
|
"span",
|
|
3578
4066
|
{
|
|
3579
4067
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -3581,7 +4069,7 @@ function OffsetScrubber({
|
|
|
3581
4069
|
children: formatOffset(draftOffset, sampleRate)
|
|
3582
4070
|
}
|
|
3583
4071
|
),
|
|
3584
|
-
/* @__PURE__ */ (0,
|
|
4072
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3585
4073
|
"button",
|
|
3586
4074
|
{
|
|
3587
4075
|
type: "button",
|
|
@@ -3593,7 +4081,7 @@ function OffsetScrubber({
|
|
|
3593
4081
|
children: "\u2316"
|
|
3594
4082
|
}
|
|
3595
4083
|
),
|
|
3596
|
-
bpmMismatch && /* @__PURE__ */ (0,
|
|
4084
|
+
bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3597
4085
|
"span",
|
|
3598
4086
|
{
|
|
3599
4087
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -3665,13 +4153,13 @@ function synthesizeCuePoints({
|
|
|
3665
4153
|
}
|
|
3666
4154
|
|
|
3667
4155
|
// src/hooks/useSceneState.ts
|
|
3668
|
-
var
|
|
4156
|
+
var import_react19 = require("react");
|
|
3669
4157
|
function useSceneState(activeSceneId, initialValue) {
|
|
3670
|
-
const [stateMap, setStateMap] = (0,
|
|
3671
|
-
const activeSceneIdRef = (0,
|
|
4158
|
+
const [stateMap, setStateMap] = (0, import_react19.useState)(() => /* @__PURE__ */ new Map());
|
|
4159
|
+
const activeSceneIdRef = (0, import_react19.useRef)(activeSceneId);
|
|
3672
4160
|
activeSceneIdRef.current = activeSceneId;
|
|
3673
4161
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
3674
|
-
const setForCurrentScene = (0,
|
|
4162
|
+
const setForCurrentScene = (0, import_react19.useCallback)((value) => {
|
|
3675
4163
|
const sid = activeSceneIdRef.current;
|
|
3676
4164
|
if (sid === null) return;
|
|
3677
4165
|
setStateMap((prev) => {
|
|
@@ -3682,7 +4170,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3682
4170
|
return newMap;
|
|
3683
4171
|
});
|
|
3684
4172
|
}, [initialValue]);
|
|
3685
|
-
const setForScene = (0,
|
|
4173
|
+
const setForScene = (0, import_react19.useCallback)((sceneId, value) => {
|
|
3686
4174
|
setStateMap((prev) => {
|
|
3687
4175
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
3688
4176
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -3695,10 +4183,10 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3695
4183
|
}
|
|
3696
4184
|
|
|
3697
4185
|
// src/hooks/useAnySolo.ts
|
|
3698
|
-
var
|
|
4186
|
+
var import_react20 = require("react");
|
|
3699
4187
|
function useAnySolo(host) {
|
|
3700
|
-
const [anySolo, setAnySolo] = (0,
|
|
3701
|
-
(0,
|
|
4188
|
+
const [anySolo, setAnySolo] = (0, import_react20.useState)(false);
|
|
4189
|
+
(0, import_react20.useEffect)(() => {
|
|
3702
4190
|
let active = true;
|
|
3703
4191
|
const refresh = () => {
|
|
3704
4192
|
host.isAnySoloActive().then((v) => {
|
|
@@ -3717,7 +4205,7 @@ function useAnySolo(host) {
|
|
|
3717
4205
|
}
|
|
3718
4206
|
|
|
3719
4207
|
// src/hooks/useSoundHistory.ts
|
|
3720
|
-
var
|
|
4208
|
+
var import_react21 = require("react");
|
|
3721
4209
|
var EMPTY = { entries: [], cursor: -1 };
|
|
3722
4210
|
function sameDescriptor(a, b) {
|
|
3723
4211
|
if (a === b) return true;
|
|
@@ -3729,14 +4217,14 @@ function sameDescriptor(a, b) {
|
|
|
3729
4217
|
}
|
|
3730
4218
|
function useSoundHistory(applySound, opts = {}) {
|
|
3731
4219
|
const max = Math.max(2, opts.max ?? 24);
|
|
3732
|
-
const applyRef = (0,
|
|
4220
|
+
const applyRef = (0, import_react21.useRef)(applySound);
|
|
3733
4221
|
applyRef.current = applySound;
|
|
3734
|
-
const onChangeRef = (0,
|
|
4222
|
+
const onChangeRef = (0, import_react21.useRef)(opts.onChange);
|
|
3735
4223
|
onChangeRef.current = opts.onChange;
|
|
3736
|
-
const dataRef = (0,
|
|
3737
|
-
const [, setVersion] = (0,
|
|
3738
|
-
const bump = (0,
|
|
3739
|
-
const commit = (0,
|
|
4224
|
+
const dataRef = (0, import_react21.useRef)({});
|
|
4225
|
+
const [, setVersion] = (0, import_react21.useState)(0);
|
|
4226
|
+
const bump = (0, import_react21.useCallback)(() => setVersion((v) => v + 1), []);
|
|
4227
|
+
const commit = (0, import_react21.useCallback)(
|
|
3740
4228
|
(trackId, next, notify) => {
|
|
3741
4229
|
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
3742
4230
|
bump();
|
|
@@ -3744,7 +4232,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3744
4232
|
},
|
|
3745
4233
|
[bump]
|
|
3746
4234
|
);
|
|
3747
|
-
const record = (0,
|
|
4235
|
+
const record = (0, import_react21.useCallback)(
|
|
3748
4236
|
(trackId, descriptor, label) => {
|
|
3749
4237
|
const h = dataRef.current[trackId];
|
|
3750
4238
|
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
@@ -3759,7 +4247,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3759
4247
|
},
|
|
3760
4248
|
[max, commit]
|
|
3761
4249
|
);
|
|
3762
|
-
const restoreTo = (0,
|
|
4250
|
+
const restoreTo = (0, import_react21.useCallback)(
|
|
3763
4251
|
async (trackId, index) => {
|
|
3764
4252
|
const h = dataRef.current[trackId];
|
|
3765
4253
|
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
@@ -3769,7 +4257,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3769
4257
|
},
|
|
3770
4258
|
[commit]
|
|
3771
4259
|
);
|
|
3772
|
-
const undo = (0,
|
|
4260
|
+
const undo = (0, import_react21.useCallback)(
|
|
3773
4261
|
(trackId) => {
|
|
3774
4262
|
const h = dataRef.current[trackId];
|
|
3775
4263
|
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
@@ -3777,7 +4265,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3777
4265
|
},
|
|
3778
4266
|
[restoreTo]
|
|
3779
4267
|
);
|
|
3780
|
-
const toggleFavorite = (0,
|
|
4268
|
+
const toggleFavorite = (0, import_react21.useCallback)(
|
|
3781
4269
|
(trackId, index) => {
|
|
3782
4270
|
const h = dataRef.current[trackId];
|
|
3783
4271
|
if (!h || index < 0 || index >= h.entries.length) return;
|
|
@@ -3786,7 +4274,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3786
4274
|
},
|
|
3787
4275
|
[commit]
|
|
3788
4276
|
);
|
|
3789
|
-
const restore = (0,
|
|
4277
|
+
const restore = (0, import_react21.useCallback)(
|
|
3790
4278
|
(trackId, state) => {
|
|
3791
4279
|
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
3792
4280
|
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
@@ -3795,15 +4283,15 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3795
4283
|
},
|
|
3796
4284
|
[commit]
|
|
3797
4285
|
);
|
|
3798
|
-
const list = (0,
|
|
4286
|
+
const list = (0, import_react21.useCallback)(
|
|
3799
4287
|
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
3800
4288
|
[]
|
|
3801
4289
|
);
|
|
3802
|
-
const canUndo = (0,
|
|
4290
|
+
const canUndo = (0, import_react21.useCallback)((trackId) => {
|
|
3803
4291
|
const h = dataRef.current[trackId];
|
|
3804
4292
|
return !!h && h.cursor > 0;
|
|
3805
4293
|
}, []);
|
|
3806
|
-
const clear = (0,
|
|
4294
|
+
const clear = (0, import_react21.useCallback)(
|
|
3807
4295
|
(trackId) => {
|
|
3808
4296
|
if (dataRef.current[trackId]) {
|
|
3809
4297
|
const next = { ...dataRef.current };
|
|
@@ -3815,18 +4303,18 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3815
4303
|
},
|
|
3816
4304
|
[bump]
|
|
3817
4305
|
);
|
|
3818
|
-
const reset = (0,
|
|
4306
|
+
const reset = (0, import_react21.useCallback)(() => {
|
|
3819
4307
|
dataRef.current = {};
|
|
3820
4308
|
bump();
|
|
3821
4309
|
}, [bump]);
|
|
3822
|
-
return (0,
|
|
4310
|
+
return (0, import_react21.useMemo)(
|
|
3823
4311
|
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
3824
4312
|
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
3825
4313
|
);
|
|
3826
4314
|
}
|
|
3827
4315
|
|
|
3828
4316
|
// src/hooks/useTrackReorder.ts
|
|
3829
|
-
var
|
|
4317
|
+
var import_react22 = require("react");
|
|
3830
4318
|
function moveItem(arr, from, to) {
|
|
3831
4319
|
const next = arr.slice();
|
|
3832
4320
|
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
@@ -3843,12 +4331,12 @@ function useTrackReorder({
|
|
|
3843
4331
|
getId,
|
|
3844
4332
|
onError
|
|
3845
4333
|
}) {
|
|
3846
|
-
const [draggingIndex, setDraggingIndex] = (0,
|
|
3847
|
-
const [dragOverIndex, setDragOverIndex] = (0,
|
|
3848
|
-
const fromRef = (0,
|
|
3849
|
-
const itemsRef = (0,
|
|
4334
|
+
const [draggingIndex, setDraggingIndex] = (0, import_react22.useState)(null);
|
|
4335
|
+
const [dragOverIndex, setDragOverIndex] = (0, import_react22.useState)(null);
|
|
4336
|
+
const fromRef = (0, import_react22.useRef)(null);
|
|
4337
|
+
const itemsRef = (0, import_react22.useRef)(items);
|
|
3850
4338
|
itemsRef.current = items;
|
|
3851
|
-
const dragPropsFor = (0,
|
|
4339
|
+
const dragPropsFor = (0, import_react22.useCallback)(
|
|
3852
4340
|
(index) => ({
|
|
3853
4341
|
handleProps: {
|
|
3854
4342
|
draggable: true,
|
|
@@ -3910,7 +4398,7 @@ function useTrackReorder({
|
|
|
3910
4398
|
}
|
|
3911
4399
|
|
|
3912
4400
|
// src/constants/sdk-version.ts
|
|
3913
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
4401
|
+
var PLUGIN_SDK_VERSION = "2.28.0";
|
|
3914
4402
|
|
|
3915
4403
|
// src/utils/format-concurrent-tracks.ts
|
|
3916
4404
|
function formatConcurrentTracks(ctx) {
|
|
@@ -4071,6 +4559,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4071
4559
|
FX_DISPLAY_LABELS,
|
|
4072
4560
|
FX_ENGINE_PLUGIN_NAMES,
|
|
4073
4561
|
FX_PRESET_CONFIGS,
|
|
4562
|
+
FadeModal,
|
|
4563
|
+
FadeTrackRow,
|
|
4074
4564
|
FxToggleBar,
|
|
4075
4565
|
GUTTER_W,
|
|
4076
4566
|
ImportTrackModal,
|
|
@@ -4089,6 +4579,7 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4089
4579
|
SamplePackCTACard,
|
|
4090
4580
|
ScrollingWaveform,
|
|
4091
4581
|
SorceryProgressBar,
|
|
4582
|
+
TEXTURAL_ROLES,
|
|
4092
4583
|
TrackDrawer,
|
|
4093
4584
|
TrackMeterStrip,
|
|
4094
4585
|
TrackRow,
|
|
@@ -4096,17 +4587,21 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4096
4587
|
WaveformView,
|
|
4097
4588
|
analyzeWavPeak,
|
|
4098
4589
|
asCrossfadeMeta,
|
|
4590
|
+
asFadeMeta,
|
|
4099
4591
|
buildCrossfadeInpaintPrompt,
|
|
4100
4592
|
buildCrossfadeVolumeCurves,
|
|
4593
|
+
buildFadeVolumeCurve,
|
|
4101
4594
|
calculateTimeBasedTarget,
|
|
4102
4595
|
cellToPx,
|
|
4103
4596
|
centerScrollTop,
|
|
4104
4597
|
computePeaks,
|
|
4105
4598
|
dbToSlider,
|
|
4599
|
+
defaultFadeGesture,
|
|
4106
4600
|
drawWaveform,
|
|
4107
4601
|
formatConcurrentTracks,
|
|
4108
4602
|
moveItem,
|
|
4109
4603
|
parseCrossfadePairs,
|
|
4604
|
+
parseFades,
|
|
4110
4605
|
pickTopKWeighted,
|
|
4111
4606
|
pitchToName,
|
|
4112
4607
|
pxToCell,
|