@signalsandsorcery/plugin-sdk 2.35.4 → 2.35.6
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 +539 -6
- package/dist/index.d.ts +539 -6
- package/dist/index.js +1574 -472
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1299 -215
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2848,7 +2848,10 @@ function asFadeMeta(val) {
|
|
|
2848
2848
|
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2849
2849
|
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
2850
2850
|
soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
|
|
2851
|
-
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
|
|
2851
|
+
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5,
|
|
2852
|
+
groupId: typeof m.groupId === "string" && m.groupId ? m.groupId : void 0,
|
|
2853
|
+
memberIndex: typeof m.memberIndex === "number" ? m.memberIndex : void 0,
|
|
2854
|
+
memberLabel: typeof m.memberLabel === "string" && m.memberLabel ? m.memberLabel : void 0
|
|
2852
2855
|
};
|
|
2853
2856
|
}
|
|
2854
2857
|
function parseFades(sceneData) {
|
|
@@ -2862,6 +2865,33 @@ function parseFades(sceneData) {
|
|
|
2862
2865
|
}
|
|
2863
2866
|
return out;
|
|
2864
2867
|
}
|
|
2868
|
+
function splitFadeEntries(entries) {
|
|
2869
|
+
const singles = [];
|
|
2870
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
2871
|
+
for (const entry of entries) {
|
|
2872
|
+
const gid = entry.meta.groupId;
|
|
2873
|
+
if (!gid) {
|
|
2874
|
+
singles.push(entry);
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
const list = byGroup.get(gid) ?? [];
|
|
2878
|
+
list.push(entry);
|
|
2879
|
+
byGroup.set(gid, list);
|
|
2880
|
+
}
|
|
2881
|
+
const groups = [];
|
|
2882
|
+
for (const [groupId, members] of byGroup) {
|
|
2883
|
+
members.sort((a, b) => (a.meta.memberIndex ?? 0) - (b.meta.memberIndex ?? 0));
|
|
2884
|
+
const head = members[0].meta;
|
|
2885
|
+
groups.push({
|
|
2886
|
+
groupId,
|
|
2887
|
+
direction: head.direction,
|
|
2888
|
+
gesture: head.gesture,
|
|
2889
|
+
sliderPos: head.sliderPos,
|
|
2890
|
+
members
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
return { singles, groups };
|
|
2894
|
+
}
|
|
2865
2895
|
function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
|
|
2866
2896
|
const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
|
|
2867
2897
|
if (gesture === "build") {
|
|
@@ -3046,9 +3076,191 @@ function FadeTrackRow({
|
|
|
3046
3076
|
);
|
|
3047
3077
|
}
|
|
3048
3078
|
|
|
3079
|
+
// src/components/GroupFadeTrackRow.tsx
|
|
3080
|
+
import React12 from "react";
|
|
3081
|
+
import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3082
|
+
function MemberCaption({
|
|
3083
|
+
member,
|
|
3084
|
+
direction
|
|
3085
|
+
}) {
|
|
3086
|
+
const tag = direction === "in" ? "Fade in" : "Fade out";
|
|
3087
|
+
return /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
|
|
3088
|
+
member.memberLabel && /* @__PURE__ */ jsx15("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: member.memberLabel }),
|
|
3089
|
+
/* @__PURE__ */ jsx15("span", { className: "text-[11px] text-sas-text truncate", title: member.sourceName ?? member.name, children: member.sourceName ?? member.name }),
|
|
3090
|
+
member.soundLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: member.soundLabel, children: [
|
|
3091
|
+
"\xB7 ",
|
|
3092
|
+
member.soundLabel
|
|
3093
|
+
] }),
|
|
3094
|
+
!member.memberLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: [
|
|
3095
|
+
"\xB7 ",
|
|
3096
|
+
tag
|
|
3097
|
+
] })
|
|
3098
|
+
] });
|
|
3099
|
+
}
|
|
3100
|
+
function GroupFadeTrackRow({
|
|
3101
|
+
groupLabel,
|
|
3102
|
+
direction,
|
|
3103
|
+
gesture,
|
|
3104
|
+
sliderPos = 0.5,
|
|
3105
|
+
members,
|
|
3106
|
+
onMemberMuteToggle,
|
|
3107
|
+
onMemberSoloToggle,
|
|
3108
|
+
onMemberVolumeChange,
|
|
3109
|
+
onMemberPanChange,
|
|
3110
|
+
onMuteAll,
|
|
3111
|
+
onSoloAll,
|
|
3112
|
+
onDelete,
|
|
3113
|
+
onSliderChange,
|
|
3114
|
+
levels,
|
|
3115
|
+
accentColor = "#9333EA"
|
|
3116
|
+
}) {
|
|
3117
|
+
const [confirmDelete, setConfirmDelete] = React12.useState(false);
|
|
3118
|
+
const allMuted = members.length > 0 && members.every((m) => m.runtimeState.muted);
|
|
3119
|
+
const allSolo = members.length > 0 && members.every((m) => m.runtimeState.solo);
|
|
3120
|
+
const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
|
|
3121
|
+
const leftLabel = direction === "in" ? "(silent)" : groupLabel;
|
|
3122
|
+
const rightLabel = direction === "in" ? groupLabel : "(silent)";
|
|
3123
|
+
return /* @__PURE__ */ jsxs11(
|
|
3124
|
+
"div",
|
|
3125
|
+
{
|
|
3126
|
+
"data-testid": "group-fade-track-row",
|
|
3127
|
+
className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
|
|
3128
|
+
style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
|
|
3129
|
+
children: [
|
|
3130
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60 gap-2", children: [
|
|
3131
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 min-w-0", children: [
|
|
3132
|
+
/* @__PURE__ */ jsx15(
|
|
3133
|
+
"span",
|
|
3134
|
+
{
|
|
3135
|
+
"data-testid": "group-fade-direction-badge",
|
|
3136
|
+
className: "text-[10px] font-bold uppercase tracking-wide flex-shrink-0",
|
|
3137
|
+
style: { color: accentColor },
|
|
3138
|
+
children: badge
|
|
3139
|
+
}
|
|
3140
|
+
),
|
|
3141
|
+
/* @__PURE__ */ jsx15(
|
|
3142
|
+
"span",
|
|
3143
|
+
{
|
|
3144
|
+
"data-testid": "group-fade-label",
|
|
3145
|
+
className: "text-[11px] text-sas-text truncate",
|
|
3146
|
+
title: `${groupLabel} \xB7 ${gesture}`,
|
|
3147
|
+
children: groupLabel
|
|
3148
|
+
}
|
|
3149
|
+
)
|
|
3150
|
+
] }),
|
|
3151
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
|
|
3152
|
+
/* @__PURE__ */ jsx15(
|
|
3153
|
+
"button",
|
|
3154
|
+
{
|
|
3155
|
+
"data-testid": "group-fade-mute-button",
|
|
3156
|
+
onClick: onMuteAll,
|
|
3157
|
+
className: `px-1.5 py-0.5 text-[9px] font-bold rounded-sm border transition-colors ${allMuted ? "bg-sas-danger/20 border-sas-danger text-sas-danger" : "border-sas-border text-sas-muted hover:text-sas-text"}`,
|
|
3158
|
+
title: allMuted ? "Unmute all members" : "Mute all members",
|
|
3159
|
+
children: "M"
|
|
3160
|
+
}
|
|
3161
|
+
),
|
|
3162
|
+
/* @__PURE__ */ jsx15(
|
|
3163
|
+
"button",
|
|
3164
|
+
{
|
|
3165
|
+
"data-testid": "group-fade-solo-button",
|
|
3166
|
+
onClick: onSoloAll,
|
|
3167
|
+
className: `px-1.5 py-0.5 text-[9px] font-bold rounded-sm border transition-colors ${allSolo ? "bg-sas-accent/20 border-sas-accent text-sas-accent" : "border-sas-border text-sas-muted hover:text-sas-text"}`,
|
|
3168
|
+
title: allSolo ? "Unsolo all members" : "Solo all members",
|
|
3169
|
+
children: "S"
|
|
3170
|
+
}
|
|
3171
|
+
),
|
|
3172
|
+
/* @__PURE__ */ jsx15(
|
|
3173
|
+
"button",
|
|
3174
|
+
{
|
|
3175
|
+
"data-testid": "group-fade-delete-button",
|
|
3176
|
+
onClick: () => setConfirmDelete(true),
|
|
3177
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
|
|
3178
|
+
title: "Delete group fade",
|
|
3179
|
+
"aria-label": "Delete group fade",
|
|
3180
|
+
children: "x"
|
|
3181
|
+
}
|
|
3182
|
+
)
|
|
3183
|
+
] })
|
|
3184
|
+
] }),
|
|
3185
|
+
members.map((member) => /* @__PURE__ */ jsx15(
|
|
3186
|
+
TrackRow,
|
|
3187
|
+
{
|
|
3188
|
+
track: { id: member.trackId, name: "", role: member.role },
|
|
3189
|
+
runtimeState: member.runtimeState,
|
|
3190
|
+
fxDetailState: EMPTY_FX_DETAIL_STATE,
|
|
3191
|
+
drawerOpen: false,
|
|
3192
|
+
drawerTab: "fx",
|
|
3193
|
+
levels,
|
|
3194
|
+
accentColor,
|
|
3195
|
+
contentSlot: /* @__PURE__ */ jsx15(MemberCaption, { member, direction }),
|
|
3196
|
+
onMuteToggle: () => onMemberMuteToggle(member.trackId),
|
|
3197
|
+
onSoloToggle: () => onMemberSoloToggle(member.trackId),
|
|
3198
|
+
onVolumeChange: (vol) => onMemberVolumeChange(member.trackId, vol),
|
|
3199
|
+
onPanChange: (pan) => onMemberPanChange(member.trackId, pan)
|
|
3200
|
+
},
|
|
3201
|
+
member.trackId
|
|
3202
|
+
)),
|
|
3203
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "group-fade-slider-row", children: [
|
|
3204
|
+
/* @__PURE__ */ jsx15(
|
|
3205
|
+
"span",
|
|
3206
|
+
{
|
|
3207
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
|
|
3208
|
+
title: leftLabel,
|
|
3209
|
+
children: leftLabel
|
|
3210
|
+
}
|
|
3211
|
+
),
|
|
3212
|
+
/* @__PURE__ */ jsx15(
|
|
3213
|
+
"input",
|
|
3214
|
+
{
|
|
3215
|
+
type: "range",
|
|
3216
|
+
"data-testid": "group-fade-slider",
|
|
3217
|
+
min: 0,
|
|
3218
|
+
max: 1,
|
|
3219
|
+
step: 0.01,
|
|
3220
|
+
value: sliderPos,
|
|
3221
|
+
disabled: !onSliderChange,
|
|
3222
|
+
onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
|
|
3223
|
+
style: { accentColor },
|
|
3224
|
+
className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
|
|
3225
|
+
"aria-label": "Group fade position"
|
|
3226
|
+
}
|
|
3227
|
+
),
|
|
3228
|
+
/* @__PURE__ */ jsx15(
|
|
3229
|
+
"span",
|
|
3230
|
+
{
|
|
3231
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
|
|
3232
|
+
title: rightLabel,
|
|
3233
|
+
children: rightLabel
|
|
3234
|
+
}
|
|
3235
|
+
)
|
|
3236
|
+
] }),
|
|
3237
|
+
/* @__PURE__ */ jsx15(
|
|
3238
|
+
ConfirmDialog,
|
|
3239
|
+
{
|
|
3240
|
+
open: confirmDelete,
|
|
3241
|
+
title: "Delete group fade?",
|
|
3242
|
+
message: /* @__PURE__ */ jsxs11(Fragment4, { children: [
|
|
3243
|
+
"All ",
|
|
3244
|
+
members.length,
|
|
3245
|
+
" member tracks of this fade will be permanently removed from this scene. This cannot be undone."
|
|
3246
|
+
] }),
|
|
3247
|
+
confirmLabel: "Delete",
|
|
3248
|
+
onConfirm: () => {
|
|
3249
|
+
setConfirmDelete(false);
|
|
3250
|
+
onDelete();
|
|
3251
|
+
},
|
|
3252
|
+
onCancel: () => setConfirmDelete(false),
|
|
3253
|
+
testIdPrefix: "group-fade-delete-confirm"
|
|
3254
|
+
}
|
|
3255
|
+
)
|
|
3256
|
+
]
|
|
3257
|
+
}
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3049
3261
|
// src/components/FadeModal.tsx
|
|
3050
3262
|
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
|
|
3051
|
-
import { Fragment as
|
|
3263
|
+
import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3052
3264
|
function shortId(dbId) {
|
|
3053
3265
|
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3054
3266
|
}
|
|
@@ -3091,7 +3303,7 @@ function OrphanRow({
|
|
|
3091
3303
|
}) {
|
|
3092
3304
|
const primary = track.prompt?.trim() || track.name;
|
|
3093
3305
|
const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
|
|
3094
|
-
return /* @__PURE__ */
|
|
3306
|
+
return /* @__PURE__ */ jsxs12(
|
|
3095
3307
|
"button",
|
|
3096
3308
|
{
|
|
3097
3309
|
type: "button",
|
|
@@ -3103,8 +3315,8 @@ function OrphanRow({
|
|
|
3103
3315
|
disabled,
|
|
3104
3316
|
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"}`,
|
|
3105
3317
|
children: [
|
|
3106
|
-
/* @__PURE__ */
|
|
3107
|
-
meta && /* @__PURE__ */
|
|
3318
|
+
/* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
3319
|
+
meta && /* @__PURE__ */ jsx16("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
3108
3320
|
]
|
|
3109
3321
|
}
|
|
3110
3322
|
);
|
|
@@ -3199,16 +3411,16 @@ function FadeModal({
|
|
|
3199
3411
|
if (!open) return null;
|
|
3200
3412
|
const renderSection = (heading, list, section) => {
|
|
3201
3413
|
if (list.length === 0) return null;
|
|
3202
|
-
return /* @__PURE__ */
|
|
3203
|
-
/* @__PURE__ */
|
|
3204
|
-
/* @__PURE__ */
|
|
3414
|
+
return /* @__PURE__ */ jsxs12("div", { className: "block", children: [
|
|
3415
|
+
/* @__PURE__ */ jsx16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
|
|
3416
|
+
/* @__PURE__ */ jsx16(
|
|
3205
3417
|
"div",
|
|
3206
3418
|
{
|
|
3207
3419
|
role: "radiogroup",
|
|
3208
3420
|
"aria-label": heading,
|
|
3209
3421
|
"data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
|
|
3210
3422
|
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3211
|
-
children: list.map((t) => /* @__PURE__ */
|
|
3423
|
+
children: list.map((t) => /* @__PURE__ */ jsx16(
|
|
3212
3424
|
OrphanRow,
|
|
3213
3425
|
{
|
|
3214
3426
|
track: t,
|
|
@@ -3224,32 +3436,32 @@ function FadeModal({
|
|
|
3224
3436
|
)
|
|
3225
3437
|
] });
|
|
3226
3438
|
};
|
|
3227
|
-
return /* @__PURE__ */
|
|
3439
|
+
return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
|
|
3228
3440
|
"div",
|
|
3229
3441
|
{
|
|
3230
3442
|
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
3231
3443
|
onClick: (e) => e.stopPropagation(),
|
|
3232
3444
|
"data-testid": `${testIdPrefix}-box`,
|
|
3233
3445
|
children: [
|
|
3234
|
-
/* @__PURE__ */
|
|
3235
|
-
/* @__PURE__ */
|
|
3446
|
+
/* @__PURE__ */ jsx16("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
|
|
3447
|
+
/* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
3236
3448
|
"Tracks with no counterpart between",
|
|
3237
3449
|
" ",
|
|
3238
|
-
/* @__PURE__ */
|
|
3450
|
+
/* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
3239
3451
|
" and",
|
|
3240
3452
|
" ",
|
|
3241
|
-
/* @__PURE__ */
|
|
3453
|
+
/* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
3242
3454
|
" can gracefully fade out (leaving) or fade in (entering) across this transition."
|
|
3243
3455
|
] }),
|
|
3244
|
-
load.status === "loading" && /* @__PURE__ */
|
|
3245
|
-
load.status === "error" && /* @__PURE__ */
|
|
3246
|
-
load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */
|
|
3456
|
+
load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3457
|
+
load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3458
|
+
load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx16("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__ */ jsxs12(Fragment5, { children: [
|
|
3247
3459
|
renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
|
|
3248
3460
|
renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
|
|
3249
3461
|
] })),
|
|
3250
|
-
error && /* @__PURE__ */
|
|
3251
|
-
/* @__PURE__ */
|
|
3252
|
-
/* @__PURE__ */
|
|
3462
|
+
error && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3463
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3464
|
+
/* @__PURE__ */ jsx16(
|
|
3253
3465
|
"button",
|
|
3254
3466
|
{
|
|
3255
3467
|
ref: cancelRef,
|
|
@@ -3260,7 +3472,7 @@ function FadeModal({
|
|
|
3260
3472
|
children: "Cancel"
|
|
3261
3473
|
}
|
|
3262
3474
|
),
|
|
3263
|
-
/* @__PURE__ */
|
|
3475
|
+
/* @__PURE__ */ jsx16(
|
|
3264
3476
|
"button",
|
|
3265
3477
|
{
|
|
3266
3478
|
"data-testid": `${testIdPrefix}-confirm`,
|
|
@@ -3278,7 +3490,7 @@ function FadeModal({
|
|
|
3278
3490
|
|
|
3279
3491
|
// src/components/ImportTrackModal.tsx
|
|
3280
3492
|
import { useCallback as useCallback6, useEffect as useEffect8, useState as useState10 } from "react";
|
|
3281
|
-
import { jsx as
|
|
3493
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3282
3494
|
function ImportTrackModal({
|
|
3283
3495
|
host,
|
|
3284
3496
|
open,
|
|
@@ -3357,16 +3569,16 @@ function ImportTrackModal({
|
|
|
3357
3569
|
if (!open) return null;
|
|
3358
3570
|
const scenes = load.status === "ready" ? load.scenes : [];
|
|
3359
3571
|
const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
|
|
3360
|
-
return /* @__PURE__ */
|
|
3572
|
+
return /* @__PURE__ */ jsx17(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs13(
|
|
3361
3573
|
"div",
|
|
3362
3574
|
{
|
|
3363
3575
|
className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
3364
3576
|
onClick: (e) => e.stopPropagation(),
|
|
3365
3577
|
"data-testid": `${testIdPrefix}-modal`,
|
|
3366
3578
|
children: [
|
|
3367
|
-
/* @__PURE__ */
|
|
3368
|
-
/* @__PURE__ */
|
|
3369
|
-
selectedScene && /* @__PURE__ */
|
|
3579
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
|
|
3580
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
|
|
3581
|
+
selectedScene && /* @__PURE__ */ jsx17(
|
|
3370
3582
|
"button",
|
|
3371
3583
|
{
|
|
3372
3584
|
className: "text-sas-muted hover:text-sas-accent text-xs",
|
|
@@ -3375,9 +3587,9 @@ function ImportTrackModal({
|
|
|
3375
3587
|
children: "\u2190"
|
|
3376
3588
|
}
|
|
3377
3589
|
),
|
|
3378
|
-
/* @__PURE__ */
|
|
3590
|
+
/* @__PURE__ */ jsx17("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
|
|
3379
3591
|
] }),
|
|
3380
|
-
/* @__PURE__ */
|
|
3592
|
+
/* @__PURE__ */ jsx17(
|
|
3381
3593
|
"button",
|
|
3382
3594
|
{
|
|
3383
3595
|
className: "text-sas-muted hover:text-sas-accent text-sm",
|
|
@@ -3387,30 +3599,30 @@ function ImportTrackModal({
|
|
|
3387
3599
|
}
|
|
3388
3600
|
)
|
|
3389
3601
|
] }),
|
|
3390
|
-
/* @__PURE__ */
|
|
3391
|
-
load.status === "loading" && /* @__PURE__ */
|
|
3392
|
-
load.status === "error" && /* @__PURE__ */
|
|
3393
|
-
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */
|
|
3394
|
-
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */
|
|
3602
|
+
/* @__PURE__ */ jsxs13("div", { className: "overflow-y-auto p-2 flex-1", children: [
|
|
3603
|
+
load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
|
|
3604
|
+
load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
3605
|
+
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx17("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." }),
|
|
3606
|
+
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx17("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsxs13(
|
|
3395
3607
|
"button",
|
|
3396
3608
|
{
|
|
3397
3609
|
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",
|
|
3398
3610
|
onClick: () => setSelectedSceneId(scene.sceneId),
|
|
3399
3611
|
"data-testid": `${testIdPrefix}-scene`,
|
|
3400
3612
|
children: [
|
|
3401
|
-
/* @__PURE__ */
|
|
3402
|
-
/* @__PURE__ */
|
|
3613
|
+
/* @__PURE__ */ jsx17("span", { className: "truncate", children: scene.sceneName }),
|
|
3614
|
+
/* @__PURE__ */ jsxs13("span", { className: "text-sas-muted", children: [
|
|
3403
3615
|
scene.tracks.length,
|
|
3404
3616
|
" \u2192"
|
|
3405
3617
|
] })
|
|
3406
3618
|
]
|
|
3407
3619
|
}
|
|
3408
3620
|
) }, scene.sceneId)) }),
|
|
3409
|
-
selectedScene && /* @__PURE__ */
|
|
3621
|
+
selectedScene && /* @__PURE__ */ jsx17("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
|
|
3410
3622
|
const busy = importingTrackId === track.trackId;
|
|
3411
3623
|
const gated = mode === "track" && !track.importable;
|
|
3412
3624
|
const disabled = gated || busy;
|
|
3413
|
-
return /* @__PURE__ */
|
|
3625
|
+
return /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsxs13(
|
|
3414
3626
|
"button",
|
|
3415
3627
|
{
|
|
3416
3628
|
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"}`,
|
|
@@ -3420,14 +3632,14 @@ function ImportTrackModal({
|
|
|
3420
3632
|
"data-testid": `${testIdPrefix}-track`,
|
|
3421
3633
|
"data-importable": mode === "sound" || track.importable ? "true" : "false",
|
|
3422
3634
|
children: [
|
|
3423
|
-
/* @__PURE__ */
|
|
3635
|
+
/* @__PURE__ */ jsxs13("span", { className: "truncate", children: [
|
|
3424
3636
|
track.name,
|
|
3425
|
-
track.role ? /* @__PURE__ */
|
|
3637
|
+
track.role ? /* @__PURE__ */ jsxs13("span", { className: "text-sas-muted", children: [
|
|
3426
3638
|
" \xB7 ",
|
|
3427
3639
|
track.role
|
|
3428
3640
|
] }) : null
|
|
3429
3641
|
] }),
|
|
3430
|
-
busy ? /* @__PURE__ */
|
|
3642
|
+
busy ? /* @__PURE__ */ jsx17("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx17("span", { className: "text-sas-muted", children: "\u2298" }) : null
|
|
3431
3643
|
]
|
|
3432
3644
|
}
|
|
3433
3645
|
) }, track.dbId);
|
|
@@ -3440,7 +3652,7 @@ function ImportTrackModal({
|
|
|
3440
3652
|
|
|
3441
3653
|
// src/components/CrossfadeModal.tsx
|
|
3442
3654
|
import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef9, useState as useState11 } from "react";
|
|
3443
|
-
import { Fragment as
|
|
3655
|
+
import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3444
3656
|
function shortId2(dbId) {
|
|
3445
3657
|
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3446
3658
|
}
|
|
@@ -3453,7 +3665,7 @@ function CandidateRow({
|
|
|
3453
3665
|
}) {
|
|
3454
3666
|
const primary = track.prompt?.trim() || track.name;
|
|
3455
3667
|
const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
|
|
3456
|
-
return /* @__PURE__ */
|
|
3668
|
+
return /* @__PURE__ */ jsxs14(
|
|
3457
3669
|
"button",
|
|
3458
3670
|
{
|
|
3459
3671
|
type: "button",
|
|
@@ -3465,8 +3677,8 @@ function CandidateRow({
|
|
|
3465
3677
|
disabled,
|
|
3466
3678
|
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"}`,
|
|
3467
3679
|
children: [
|
|
3468
|
-
/* @__PURE__ */
|
|
3469
|
-
meta && /* @__PURE__ */
|
|
3680
|
+
/* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
3681
|
+
meta && /* @__PURE__ */ jsx18("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
3470
3682
|
]
|
|
3471
3683
|
}
|
|
3472
3684
|
);
|
|
@@ -3563,26 +3775,26 @@ function CrossfadeModal({
|
|
|
3563
3775
|
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
3564
3776
|
const toLabel = toName ?? toSceneName ?? null;
|
|
3565
3777
|
if (!open) return null;
|
|
3566
|
-
return /* @__PURE__ */
|
|
3778
|
+
return /* @__PURE__ */ jsx18(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs14(
|
|
3567
3779
|
"div",
|
|
3568
3780
|
{
|
|
3569
3781
|
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
3570
3782
|
onClick: (e) => e.stopPropagation(),
|
|
3571
3783
|
"data-testid": `${testIdPrefix}-box`,
|
|
3572
3784
|
children: [
|
|
3573
|
-
/* @__PURE__ */
|
|
3574
|
-
/* @__PURE__ */
|
|
3785
|
+
/* @__PURE__ */ jsx18("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
|
|
3786
|
+
/* @__PURE__ */ jsxs14("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
3575
3787
|
"Bridge a track from",
|
|
3576
3788
|
" ",
|
|
3577
|
-
/* @__PURE__ */
|
|
3789
|
+
/* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
3578
3790
|
" into one from",
|
|
3579
3791
|
" ",
|
|
3580
|
-
/* @__PURE__ */
|
|
3792
|
+
/* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
3581
3793
|
". Both layers share one generated part; each keeps its own preset."
|
|
3582
3794
|
] }),
|
|
3583
|
-
load.status === "loading" && /* @__PURE__ */
|
|
3584
|
-
load.status === "error" && /* @__PURE__ */
|
|
3585
|
-
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */
|
|
3795
|
+
load.status === "loading" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3796
|
+
load.status === "error" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3797
|
+
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs14(
|
|
3586
3798
|
"div",
|
|
3587
3799
|
{
|
|
3588
3800
|
className: "text-xs text-sas-muted py-4 text-center",
|
|
@@ -3593,20 +3805,20 @@ function CrossfadeModal({
|
|
|
3593
3805
|
". Add one (or free one from another crossfade) first."
|
|
3594
3806
|
]
|
|
3595
3807
|
}
|
|
3596
|
-
) : /* @__PURE__ */
|
|
3597
|
-
/* @__PURE__ */
|
|
3598
|
-
/* @__PURE__ */
|
|
3808
|
+
) : /* @__PURE__ */ jsxs14(Fragment6, { children: [
|
|
3809
|
+
/* @__PURE__ */ jsxs14("div", { className: "block", children: [
|
|
3810
|
+
/* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
3599
3811
|
"Origin ",
|
|
3600
3812
|
fromLabel ? `(${fromLabel})` : "(top)"
|
|
3601
3813
|
] }),
|
|
3602
|
-
/* @__PURE__ */
|
|
3814
|
+
/* @__PURE__ */ jsx18(
|
|
3603
3815
|
"div",
|
|
3604
3816
|
{
|
|
3605
3817
|
role: "radiogroup",
|
|
3606
3818
|
"aria-label": "Origin track",
|
|
3607
3819
|
"data-testid": `${testIdPrefix}-origin-list`,
|
|
3608
3820
|
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3609
|
-
children: originCandidates.map((t) => /* @__PURE__ */
|
|
3821
|
+
children: originCandidates.map((t) => /* @__PURE__ */ jsx18(
|
|
3610
3822
|
CandidateRow,
|
|
3611
3823
|
{
|
|
3612
3824
|
track: t,
|
|
@@ -3620,23 +3832,23 @@ function CrossfadeModal({
|
|
|
3620
3832
|
}
|
|
3621
3833
|
)
|
|
3622
3834
|
] }),
|
|
3623
|
-
/* @__PURE__ */
|
|
3624
|
-
/* @__PURE__ */
|
|
3835
|
+
/* @__PURE__ */ jsxs14("div", { className: "block", children: [
|
|
3836
|
+
/* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
3625
3837
|
"Target ",
|
|
3626
3838
|
toLabel ? `(${toLabel})` : "(bottom)"
|
|
3627
3839
|
] }),
|
|
3628
|
-
targetCandidates.length === 0 ? /* @__PURE__ */
|
|
3840
|
+
targetCandidates.length === 0 ? /* @__PURE__ */ jsxs14("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
|
|
3629
3841
|
"No available tracks in ",
|
|
3630
3842
|
toLabel ?? "the target scene",
|
|
3631
3843
|
" to crossfade into."
|
|
3632
|
-
] }) : /* @__PURE__ */
|
|
3844
|
+
] }) : /* @__PURE__ */ jsx18(
|
|
3633
3845
|
"div",
|
|
3634
3846
|
{
|
|
3635
3847
|
role: "radiogroup",
|
|
3636
3848
|
"aria-label": "Target track",
|
|
3637
3849
|
"data-testid": `${testIdPrefix}-target-list`,
|
|
3638
3850
|
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3639
|
-
children: targetCandidates.map((t) => /* @__PURE__ */
|
|
3851
|
+
children: targetCandidates.map((t) => /* @__PURE__ */ jsx18(
|
|
3640
3852
|
CandidateRow,
|
|
3641
3853
|
{
|
|
3642
3854
|
track: t,
|
|
@@ -3651,9 +3863,9 @@ function CrossfadeModal({
|
|
|
3651
3863
|
)
|
|
3652
3864
|
] })
|
|
3653
3865
|
] })),
|
|
3654
|
-
error && /* @__PURE__ */
|
|
3655
|
-
/* @__PURE__ */
|
|
3656
|
-
/* @__PURE__ */
|
|
3866
|
+
error && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3867
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3868
|
+
/* @__PURE__ */ jsx18(
|
|
3657
3869
|
"button",
|
|
3658
3870
|
{
|
|
3659
3871
|
ref: cancelRef,
|
|
@@ -3664,7 +3876,7 @@ function CrossfadeModal({
|
|
|
3664
3876
|
children: "Cancel"
|
|
3665
3877
|
}
|
|
3666
3878
|
),
|
|
3667
|
-
/* @__PURE__ */
|
|
3879
|
+
/* @__PURE__ */ jsx18(
|
|
3668
3880
|
"button",
|
|
3669
3881
|
{
|
|
3670
3882
|
"data-testid": `${testIdPrefix}-confirm`,
|
|
@@ -3888,7 +4100,7 @@ function dbIdsFromKeys(keys) {
|
|
|
3888
4100
|
}
|
|
3889
4101
|
|
|
3890
4102
|
// src/components/TransitionDesigner.tsx
|
|
3891
|
-
import { jsx as
|
|
4103
|
+
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3892
4104
|
var CROSSFADE_ESTIMATE_MS = 15e3;
|
|
3893
4105
|
var FADE_ESTIMATE_MS = 11e3;
|
|
3894
4106
|
var CREATE_ALL_CONCURRENCY = 5;
|
|
@@ -3909,9 +4121,12 @@ function TransitionDesigner({
|
|
|
3909
4121
|
onCreateCrossfade,
|
|
3910
4122
|
onCreateFade,
|
|
3911
4123
|
onCreateAudioTransition,
|
|
4124
|
+
mapColumnSubjects,
|
|
4125
|
+
fadeEstimateMs,
|
|
3912
4126
|
familyLabel,
|
|
3913
4127
|
testIdPrefix = "transition-designer"
|
|
3914
4128
|
}) {
|
|
4129
|
+
const fadeOnly = !onCreateCrossfade;
|
|
3915
4130
|
const [load, setLoad] = useState13({ status: "loading" });
|
|
3916
4131
|
const [fromName, setFromName] = useState13(null);
|
|
3917
4132
|
const [toName, setToName] = useState13(null);
|
|
@@ -3956,9 +4171,17 @@ function TransitionDesigner({
|
|
|
3956
4171
|
}
|
|
3957
4172
|
setLoad({ status: "loading" });
|
|
3958
4173
|
try {
|
|
3959
|
-
|
|
4174
|
+
let [origin, target] = await Promise.all([
|
|
3960
4175
|
host.listSceneFamilyTracks(fromSceneId),
|
|
3961
|
-
host.listSceneFamilyTracks(toSceneId)
|
|
4176
|
+
host.listSceneFamilyTracks(toSceneId)
|
|
4177
|
+
]);
|
|
4178
|
+
if (mapColumnSubjects) {
|
|
4179
|
+
[origin, target] = await Promise.all([
|
|
4180
|
+
mapColumnSubjects(fromSceneId, origin),
|
|
4181
|
+
mapColumnSubjects(toSceneId, target)
|
|
4182
|
+
]);
|
|
4183
|
+
}
|
|
4184
|
+
const [fName, tName, draftRaw] = await Promise.all([
|
|
3962
4185
|
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3963
4186
|
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
|
|
3964
4187
|
host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
|
|
@@ -3983,7 +4206,7 @@ function TransitionDesigner({
|
|
|
3983
4206
|
message: err instanceof Error ? err.message : "Failed to load tracks."
|
|
3984
4207
|
});
|
|
3985
4208
|
}
|
|
3986
|
-
}, [host, fromSceneId, toSceneId, transitionSceneId]);
|
|
4209
|
+
}, [host, fromSceneId, toSceneId, transitionSceneId, mapColumnSubjects]);
|
|
3987
4210
|
useEffect10(() => {
|
|
3988
4211
|
void refresh();
|
|
3989
4212
|
}, [refresh]);
|
|
@@ -4054,13 +4277,31 @@ function TransitionDesigner({
|
|
|
4054
4277
|
[originSlots, targetSlots, mutate]
|
|
4055
4278
|
);
|
|
4056
4279
|
const rows = useMemo6(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
|
|
4280
|
+
const fadeOnlyRows = useMemo6(
|
|
4281
|
+
() => fadeOnly ? [
|
|
4282
|
+
...originPool.map((t) => ({
|
|
4283
|
+
originId: t.dbId,
|
|
4284
|
+
targetId: null,
|
|
4285
|
+
type: "fade-out"
|
|
4286
|
+
})),
|
|
4287
|
+
...targetPool.map((t) => ({
|
|
4288
|
+
originId: null,
|
|
4289
|
+
targetId: t.dbId,
|
|
4290
|
+
type: "fade-in"
|
|
4291
|
+
}))
|
|
4292
|
+
] : [],
|
|
4293
|
+
[fadeOnly, originPool, targetPool]
|
|
4294
|
+
);
|
|
4295
|
+
const fadeOnlyRowsRef = useRef11(fadeOnlyRows);
|
|
4296
|
+
fadeOnlyRowsRef.current = fadeOnlyRows;
|
|
4297
|
+
const effectiveRows = fadeOnly ? fadeOnlyRows : rows;
|
|
4057
4298
|
const creatingDbIds = useMemo6(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
|
|
4058
4299
|
const eligibleCount = useMemo6(
|
|
4059
|
-
() =>
|
|
4300
|
+
() => effectiveRows.filter((r) => {
|
|
4060
4301
|
const k = rowKey(r);
|
|
4061
4302
|
return k !== null && !creatingKeys.has(k);
|
|
4062
4303
|
}).length,
|
|
4063
|
-
[
|
|
4304
|
+
[effectiveRows, creatingKeys]
|
|
4064
4305
|
);
|
|
4065
4306
|
const createRow = useCallback9(
|
|
4066
4307
|
async (row) => {
|
|
@@ -4078,6 +4319,7 @@ function TransitionDesigner({
|
|
|
4078
4319
|
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
4079
4320
|
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
4080
4321
|
if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
4322
|
+
if (!onCreateCrossfade) throw new Error("This panel does not support crossfades.");
|
|
4081
4323
|
await onCreateCrossfade(
|
|
4082
4324
|
{ dbId: o.dbId, name: o.name, role: o.role },
|
|
4083
4325
|
{ dbId: t.dbId, name: t.name, role: t.role }
|
|
@@ -4117,7 +4359,8 @@ function TransitionDesigner({
|
|
|
4117
4359
|
[onCreateCrossfade, onCreateFade, onCreateAudioTransition]
|
|
4118
4360
|
);
|
|
4119
4361
|
const createAll = useCallback9(async () => {
|
|
4120
|
-
const
|
|
4362
|
+
const source = fadeOnly ? fadeOnlyRowsRef.current : buildRowSlots(originSlotsRef.current, targetSlotsRef.current);
|
|
4363
|
+
const eligible = source.filter((r) => {
|
|
4121
4364
|
const k = rowKey(r);
|
|
4122
4365
|
return k !== null && !creatingKeysRef.current.has(k);
|
|
4123
4366
|
});
|
|
@@ -4133,7 +4376,7 @@ function TransitionDesigner({
|
|
|
4133
4376
|
await Promise.all(
|
|
4134
4377
|
Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
|
|
4135
4378
|
);
|
|
4136
|
-
}, [createRow]);
|
|
4379
|
+
}, [createRow, fadeOnly]);
|
|
4137
4380
|
const fromLabel = fromName ?? "origin";
|
|
4138
4381
|
const toLabel = toName ?? "target";
|
|
4139
4382
|
const cellDragProps = (col, index, locked) => ({
|
|
@@ -4184,15 +4427,15 @@ function TransitionDesigner({
|
|
|
4184
4427
|
const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
|
|
4185
4428
|
const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
|
|
4186
4429
|
if (slotId === null) {
|
|
4187
|
-
return /* @__PURE__ */
|
|
4430
|
+
return /* @__PURE__ */ jsxs15(
|
|
4188
4431
|
"div",
|
|
4189
4432
|
{
|
|
4190
4433
|
...cellDragProps(col, index, false),
|
|
4191
4434
|
"data-testid": `${testIdPrefix}-${col}-gap-${index}`,
|
|
4192
4435
|
className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
|
|
4193
4436
|
children: [
|
|
4194
|
-
/* @__PURE__ */
|
|
4195
|
-
/* @__PURE__ */
|
|
4437
|
+
/* @__PURE__ */ jsx19("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
|
|
4438
|
+
/* @__PURE__ */ jsx19(
|
|
4196
4439
|
"button",
|
|
4197
4440
|
{
|
|
4198
4441
|
type: "button",
|
|
@@ -4209,7 +4452,7 @@ function TransitionDesigner({
|
|
|
4209
4452
|
}
|
|
4210
4453
|
const primary = track ? track.prompt?.trim() || track.name : slotId;
|
|
4211
4454
|
const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
|
|
4212
|
-
return /* @__PURE__ */
|
|
4455
|
+
return /* @__PURE__ */ jsx19(
|
|
4213
4456
|
"div",
|
|
4214
4457
|
{
|
|
4215
4458
|
...cellDragProps(col, index, locked),
|
|
@@ -4217,13 +4460,13 @@ function TransitionDesigner({
|
|
|
4217
4460
|
"data-value": slotId,
|
|
4218
4461
|
className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
|
|
4219
4462
|
title: track ? track.dbId : "Track no longer available",
|
|
4220
|
-
children: /* @__PURE__ */
|
|
4221
|
-
/* @__PURE__ */
|
|
4222
|
-
/* @__PURE__ */
|
|
4223
|
-
/* @__PURE__ */
|
|
4224
|
-
meta && /* @__PURE__ */
|
|
4463
|
+
children: /* @__PURE__ */ jsxs15("div", { className: "flex items-start gap-1", children: [
|
|
4464
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
|
|
4465
|
+
/* @__PURE__ */ jsxs15("div", { className: "min-w-0 flex-1", children: [
|
|
4466
|
+
/* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", children: primary }),
|
|
4467
|
+
meta && /* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
|
|
4225
4468
|
] }),
|
|
4226
|
-
/* @__PURE__ */
|
|
4469
|
+
/* @__PURE__ */ jsx19(
|
|
4227
4470
|
"button",
|
|
4228
4471
|
{
|
|
4229
4472
|
type: "button",
|
|
@@ -4239,22 +4482,159 @@ function TransitionDesigner({
|
|
|
4239
4482
|
}
|
|
4240
4483
|
);
|
|
4241
4484
|
};
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4485
|
+
const renderFadeOnlyRow = (row, i) => {
|
|
4486
|
+
const slotId = row.originId ?? row.targetId;
|
|
4487
|
+
const byId = row.type === "fade-out" ? originById : targetById;
|
|
4488
|
+
const track = byId.get(slotId);
|
|
4489
|
+
const key = rowKey(row);
|
|
4490
|
+
const isCreatingThis = key !== null && creatingKeys.has(key);
|
|
4491
|
+
const errMsg = key !== null ? rowErrors[key] : void 0;
|
|
4492
|
+
return /* @__PURE__ */ jsxs15(
|
|
4493
|
+
"div",
|
|
4494
|
+
{
|
|
4495
|
+
"data-testid": `${testIdPrefix}-row-${i}`,
|
|
4496
|
+
className: "grid grid-cols-[1fr_auto] gap-2 items-center",
|
|
4497
|
+
children: [
|
|
4498
|
+
/* @__PURE__ */ jsxs15(
|
|
4499
|
+
"div",
|
|
4500
|
+
{
|
|
4501
|
+
"data-testid": `${testIdPrefix}-subject-${slotId}`,
|
|
4502
|
+
className: `rounded-sm border border-sas-border bg-sas-panel px-2 py-1.5 ${isCreatingThis ? "opacity-60" : ""}`,
|
|
4503
|
+
title: track ? track.dbId : "Track no longer available",
|
|
4504
|
+
children: [
|
|
4505
|
+
/* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", children: track ? track.prompt?.trim() || track.name : slotId }),
|
|
4506
|
+
/* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing" })
|
|
4507
|
+
]
|
|
4508
|
+
}
|
|
4509
|
+
),
|
|
4510
|
+
/* @__PURE__ */ jsxs15("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
|
|
4511
|
+
/* @__PURE__ */ jsx19(
|
|
4512
|
+
"span",
|
|
4513
|
+
{
|
|
4514
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4515
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
|
|
4516
|
+
children: row.type ? TYPE_LABEL[row.type] : "\u2014"
|
|
4517
|
+
}
|
|
4518
|
+
),
|
|
4519
|
+
isCreatingThis ? /* @__PURE__ */ jsx19("div", { className: "w-full", children: /* @__PURE__ */ jsx19(
|
|
4520
|
+
SorceryProgressBar,
|
|
4521
|
+
{
|
|
4522
|
+
isLoading: true,
|
|
4523
|
+
heightClass: "h-5",
|
|
4524
|
+
statusText: "CREATING",
|
|
4525
|
+
estimatedDurationMs: fadeEstimateMs ?? FADE_ESTIMATE_MS
|
|
4526
|
+
}
|
|
4527
|
+
) }) : /* @__PURE__ */ jsx19(
|
|
4528
|
+
"button",
|
|
4529
|
+
{
|
|
4530
|
+
type: "button",
|
|
4531
|
+
"data-testid": `${testIdPrefix}-create-${i}`,
|
|
4532
|
+
onClick: () => createRow(row),
|
|
4533
|
+
className: "w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg",
|
|
4534
|
+
children: "Create"
|
|
4535
|
+
}
|
|
4536
|
+
),
|
|
4537
|
+
errMsg && /* @__PURE__ */ jsx19(
|
|
4538
|
+
"span",
|
|
4539
|
+
{
|
|
4540
|
+
"data-testid": `${testIdPrefix}-row-error-${i}`,
|
|
4541
|
+
className: "text-[10px] text-sas-danger text-center leading-tight",
|
|
4542
|
+
children: errMsg
|
|
4543
|
+
}
|
|
4544
|
+
)
|
|
4545
|
+
] })
|
|
4546
|
+
]
|
|
4547
|
+
},
|
|
4548
|
+
slotId
|
|
4549
|
+
);
|
|
4550
|
+
};
|
|
4551
|
+
if (fadeOnly) {
|
|
4552
|
+
const outRows = fadeOnlyRows.filter((r) => r.type === "fade-out");
|
|
4553
|
+
const inRows = fadeOnlyRows.filter((r) => r.type === "fade-in");
|
|
4554
|
+
return /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
|
|
4555
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
|
|
4556
|
+
/* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
|
|
4557
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel }),
|
|
4558
|
+
" \u2192",
|
|
4559
|
+
" ",
|
|
4560
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel }),
|
|
4561
|
+
familyLabel ? ` \xB7 ${familyLabel}` : "",
|
|
4562
|
+
" \xB7 each entry fades on its own \u2014 origin fades out, target fades in."
|
|
4563
|
+
] }),
|
|
4564
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4565
|
+
creatingKeys.size > 0 && /* @__PURE__ */ jsxs15(
|
|
4566
|
+
"span",
|
|
4567
|
+
{
|
|
4568
|
+
className: "text-[10px] text-sas-accent whitespace-nowrap",
|
|
4569
|
+
"data-testid": `${testIdPrefix}-creating-count`,
|
|
4570
|
+
children: [
|
|
4571
|
+
creatingKeys.size,
|
|
4572
|
+
" creating\u2026"
|
|
4573
|
+
]
|
|
4574
|
+
}
|
|
4575
|
+
),
|
|
4576
|
+
/* @__PURE__ */ jsxs15(
|
|
4577
|
+
"button",
|
|
4578
|
+
{
|
|
4579
|
+
type: "button",
|
|
4580
|
+
"data-testid": `${testIdPrefix}-create-all`,
|
|
4581
|
+
onClick: createAll,
|
|
4582
|
+
disabled: eligibleCount === 0,
|
|
4583
|
+
title: "Create every fade at once (runs several concurrently)",
|
|
4584
|
+
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"}`,
|
|
4585
|
+
children: [
|
|
4586
|
+
"Create all",
|
|
4587
|
+
eligibleCount > 0 ? ` (${eligibleCount})` : ""
|
|
4588
|
+
]
|
|
4589
|
+
}
|
|
4590
|
+
)
|
|
4591
|
+
] })
|
|
4592
|
+
] }),
|
|
4593
|
+
load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
|
|
4594
|
+
load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
4595
|
+
load.status === "ready" && (fadeOnlyRows.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
|
|
4596
|
+
"Nothing to fade in this panel for either scene. Add tracks to ",
|
|
4597
|
+
fromLabel,
|
|
4598
|
+
" or ",
|
|
4599
|
+
toLabel,
|
|
4600
|
+
" ",
|
|
4601
|
+
"first (or free one by deleting an existing fade)."
|
|
4602
|
+
] }) : /* @__PURE__ */ jsxs15("div", { className: "space-y-3", children: [
|
|
4603
|
+
outRows.length > 0 && /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-out-section`, children: [
|
|
4604
|
+
/* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
4605
|
+
"Origin (",
|
|
4606
|
+
fromLabel,
|
|
4607
|
+
") \u2014 fades out"
|
|
4608
|
+
] }),
|
|
4609
|
+
outRows.map((row, i) => renderFadeOnlyRow(row, i))
|
|
4610
|
+
] }),
|
|
4611
|
+
inRows.length > 0 && /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-in-section`, children: [
|
|
4612
|
+
/* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
4613
|
+
"Target (",
|
|
4614
|
+
toLabel,
|
|
4615
|
+
") \u2014 fades in"
|
|
4616
|
+
] }),
|
|
4617
|
+
inRows.map((row, j) => renderFadeOnlyRow(row, outRows.length + j))
|
|
4618
|
+
] })
|
|
4619
|
+
] }))
|
|
4620
|
+
] });
|
|
4621
|
+
}
|
|
4622
|
+
return /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
|
|
4623
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
|
|
4624
|
+
/* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
|
|
4625
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel }),
|
|
4246
4626
|
" \u2192",
|
|
4247
4627
|
" ",
|
|
4248
|
-
/* @__PURE__ */
|
|
4628
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel }),
|
|
4249
4629
|
familyLabel ? ` \xB7 ${familyLabel}` : "",
|
|
4250
4630
|
" \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
|
|
4251
4631
|
] }),
|
|
4252
|
-
/* @__PURE__ */
|
|
4253
|
-
creatingKeys.size > 0 && /* @__PURE__ */
|
|
4632
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4633
|
+
creatingKeys.size > 0 && /* @__PURE__ */ jsxs15("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
|
|
4254
4634
|
creatingKeys.size,
|
|
4255
4635
|
" creating\u2026"
|
|
4256
4636
|
] }),
|
|
4257
|
-
/* @__PURE__ */
|
|
4637
|
+
/* @__PURE__ */ jsxs15(
|
|
4258
4638
|
"button",
|
|
4259
4639
|
{
|
|
4260
4640
|
type: "button",
|
|
@@ -4271,49 +4651,49 @@ function TransitionDesigner({
|
|
|
4271
4651
|
)
|
|
4272
4652
|
] })
|
|
4273
4653
|
] }),
|
|
4274
|
-
/* @__PURE__ */
|
|
4275
|
-
/* @__PURE__ */
|
|
4654
|
+
/* @__PURE__ */ jsxs15("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
|
|
4655
|
+
/* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
|
|
4276
4656
|
"Origin (",
|
|
4277
4657
|
fromLabel,
|
|
4278
4658
|
")"
|
|
4279
4659
|
] }),
|
|
4280
|
-
/* @__PURE__ */
|
|
4281
|
-
/* @__PURE__ */
|
|
4660
|
+
/* @__PURE__ */ jsx19("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
|
|
4661
|
+
/* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
|
|
4282
4662
|
"Target (",
|
|
4283
4663
|
toLabel,
|
|
4284
4664
|
")"
|
|
4285
4665
|
] })
|
|
4286
4666
|
] }),
|
|
4287
|
-
load.status === "loading" && /* @__PURE__ */
|
|
4288
|
-
load.status === "error" && /* @__PURE__ */
|
|
4289
|
-
load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */
|
|
4667
|
+
load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
|
|
4668
|
+
load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
4669
|
+
load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
|
|
4290
4670
|
"No tracks to arrange in this panel for either scene. Add tracks to ",
|
|
4291
4671
|
fromLabel,
|
|
4292
4672
|
" or ",
|
|
4293
4673
|
toLabel,
|
|
4294
4674
|
" ",
|
|
4295
4675
|
"first (or free one by deleting an existing crossfade/fade)."
|
|
4296
|
-
] }) : /* @__PURE__ */
|
|
4676
|
+
] }) : /* @__PURE__ */ jsx19("div", { className: "space-y-2", children: rows.map((row, i) => {
|
|
4297
4677
|
const key = rowKey(row);
|
|
4298
4678
|
const isCreatingThis = key !== null && creatingKeys.has(key);
|
|
4299
4679
|
const errMsg = key !== null ? rowErrors[key] : void 0;
|
|
4300
|
-
return /* @__PURE__ */
|
|
4680
|
+
return /* @__PURE__ */ jsxs15(
|
|
4301
4681
|
"div",
|
|
4302
4682
|
{
|
|
4303
4683
|
"data-testid": `${testIdPrefix}-row-${i}`,
|
|
4304
4684
|
className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
|
|
4305
4685
|
children: [
|
|
4306
4686
|
renderCell("origin", i, row.originId),
|
|
4307
|
-
/* @__PURE__ */
|
|
4308
|
-
!row.type ? /* @__PURE__ */
|
|
4687
|
+
/* @__PURE__ */ jsxs15("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
|
|
4688
|
+
!row.type ? /* @__PURE__ */ jsx19("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx19(
|
|
4309
4689
|
"span",
|
|
4310
4690
|
{
|
|
4311
4691
|
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4312
4692
|
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
|
|
4313
4693
|
children: TYPE_LABEL[row.type]
|
|
4314
4694
|
}
|
|
4315
|
-
) : audioEffectsEnabled ? /* @__PURE__ */
|
|
4316
|
-
/* @__PURE__ */
|
|
4695
|
+
) : audioEffectsEnabled ? /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
|
|
4696
|
+
/* @__PURE__ */ jsx19(
|
|
4317
4697
|
"select",
|
|
4318
4698
|
{
|
|
4319
4699
|
"data-testid": `${testIdPrefix}-effect-${i}`,
|
|
@@ -4323,11 +4703,11 @@ function TransitionDesigner({
|
|
|
4323
4703
|
if (id) setRowEffect(id, e.target.value);
|
|
4324
4704
|
},
|
|
4325
4705
|
className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
|
|
4326
|
-
children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */
|
|
4706
|
+
children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx19("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
|
|
4327
4707
|
}
|
|
4328
4708
|
),
|
|
4329
|
-
/* @__PURE__ */
|
|
4330
|
-
] }) : /* @__PURE__ */
|
|
4709
|
+
/* @__PURE__ */ jsx19("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
|
|
4710
|
+
] }) : /* @__PURE__ */ jsx19(
|
|
4331
4711
|
"span",
|
|
4332
4712
|
{
|
|
4333
4713
|
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
@@ -4335,7 +4715,7 @@ function TransitionDesigner({
|
|
|
4335
4715
|
children: TYPE_LABEL[row.type]
|
|
4336
4716
|
}
|
|
4337
4717
|
),
|
|
4338
|
-
isCreatingThis ? /* @__PURE__ */
|
|
4718
|
+
isCreatingThis ? /* @__PURE__ */ jsx19("div", { className: "w-full", children: /* @__PURE__ */ jsx19(
|
|
4339
4719
|
SorceryProgressBar,
|
|
4340
4720
|
{
|
|
4341
4721
|
isLoading: true,
|
|
@@ -4343,7 +4723,7 @@ function TransitionDesigner({
|
|
|
4343
4723
|
statusText: "CREATING",
|
|
4344
4724
|
estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
|
|
4345
4725
|
}
|
|
4346
|
-
) }) : /* @__PURE__ */
|
|
4726
|
+
) }) : /* @__PURE__ */ jsx19(
|
|
4347
4727
|
"button",
|
|
4348
4728
|
{
|
|
4349
4729
|
type: "button",
|
|
@@ -4354,7 +4734,7 @@ function TransitionDesigner({
|
|
|
4354
4734
|
children: "Create"
|
|
4355
4735
|
}
|
|
4356
4736
|
),
|
|
4357
|
-
errMsg && /* @__PURE__ */
|
|
4737
|
+
errMsg && /* @__PURE__ */ jsx19(
|
|
4358
4738
|
"span",
|
|
4359
4739
|
{
|
|
4360
4740
|
"data-testid": `${testIdPrefix}-row-error-${i}`,
|
|
@@ -4374,7 +4754,7 @@ function TransitionDesigner({
|
|
|
4374
4754
|
|
|
4375
4755
|
// src/components/PanelMasterStrip.tsx
|
|
4376
4756
|
import { useMemo as useMemo7, useState as useState14 } from "react";
|
|
4377
|
-
import { jsx as
|
|
4757
|
+
import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
4378
4758
|
function PanelMasterStrip({
|
|
4379
4759
|
bus,
|
|
4380
4760
|
levels = null,
|
|
@@ -4401,14 +4781,14 @@ function PanelMasterStrip({
|
|
|
4401
4781
|
(fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
|
|
4402
4782
|
);
|
|
4403
4783
|
}, [availableFx, search]);
|
|
4404
|
-
return /* @__PURE__ */
|
|
4784
|
+
return /* @__PURE__ */ jsxs16(
|
|
4405
4785
|
"div",
|
|
4406
4786
|
{
|
|
4407
4787
|
"data-testid": "panel-master-strip",
|
|
4408
4788
|
className: `flex flex-col gap-1 px-2 py-1.5 rounded-sm border border-sas-border border-l-2 border-l-sas-accent/50 bg-sas-accent/5 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
|
|
4409
4789
|
children: [
|
|
4410
|
-
/* @__PURE__ */
|
|
4411
|
-
/* @__PURE__ */
|
|
4790
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
|
|
4791
|
+
/* @__PURE__ */ jsx20(
|
|
4412
4792
|
"span",
|
|
4413
4793
|
{
|
|
4414
4794
|
className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
|
|
@@ -4416,8 +4796,8 @@ function PanelMasterStrip({
|
|
|
4416
4796
|
children: "BUS"
|
|
4417
4797
|
}
|
|
4418
4798
|
),
|
|
4419
|
-
/* @__PURE__ */
|
|
4420
|
-
/* @__PURE__ */
|
|
4799
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
|
|
4800
|
+
/* @__PURE__ */ jsx20(
|
|
4421
4801
|
VolumeSlider,
|
|
4422
4802
|
{
|
|
4423
4803
|
value: dbToSlider(bus.volume),
|
|
@@ -4425,8 +4805,8 @@ function PanelMasterStrip({
|
|
|
4425
4805
|
disabled
|
|
4426
4806
|
}
|
|
4427
4807
|
),
|
|
4428
|
-
/* @__PURE__ */
|
|
4429
|
-
/* @__PURE__ */
|
|
4808
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
|
|
4809
|
+
/* @__PURE__ */ jsx20(
|
|
4430
4810
|
LevelMeter,
|
|
4431
4811
|
{
|
|
4432
4812
|
peakDb: levels?.leftDb ?? -120,
|
|
@@ -4436,7 +4816,7 @@ function PanelMasterStrip({
|
|
|
4436
4816
|
"data-testid": "bus-meter-left"
|
|
4437
4817
|
}
|
|
4438
4818
|
),
|
|
4439
|
-
/* @__PURE__ */
|
|
4819
|
+
/* @__PURE__ */ jsx20(
|
|
4440
4820
|
LevelMeter,
|
|
4441
4821
|
{
|
|
4442
4822
|
peakDb: levels?.rightDb ?? -120,
|
|
@@ -4447,7 +4827,7 @@ function PanelMasterStrip({
|
|
|
4447
4827
|
)
|
|
4448
4828
|
] })
|
|
4449
4829
|
] }),
|
|
4450
|
-
/* @__PURE__ */
|
|
4830
|
+
/* @__PURE__ */ jsx20(
|
|
4451
4831
|
"button",
|
|
4452
4832
|
{
|
|
4453
4833
|
"data-testid": "bus-mute-button",
|
|
@@ -4458,7 +4838,7 @@ function PanelMasterStrip({
|
|
|
4458
4838
|
children: "M"
|
|
4459
4839
|
}
|
|
4460
4840
|
),
|
|
4461
|
-
/* @__PURE__ */
|
|
4841
|
+
/* @__PURE__ */ jsx20(
|
|
4462
4842
|
"button",
|
|
4463
4843
|
{
|
|
4464
4844
|
"data-testid": "bus-solo-button",
|
|
@@ -4469,14 +4849,14 @@ function PanelMasterStrip({
|
|
|
4469
4849
|
children: "S"
|
|
4470
4850
|
}
|
|
4471
4851
|
),
|
|
4472
|
-
/* @__PURE__ */
|
|
4852
|
+
/* @__PURE__ */ jsx20("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs16(
|
|
4473
4853
|
"span",
|
|
4474
4854
|
{
|
|
4475
4855
|
"data-testid": `bus-fx-chip-${fx.index}`,
|
|
4476
4856
|
className: `flex items-center gap-1 px-1.5 py-0.5 rounded-sm border text-[10px] whitespace-nowrap ${fx.enabled ? "border-sas-accent/60 text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted/50 bg-sas-panel"}`,
|
|
4477
4857
|
title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
|
|
4478
4858
|
children: [
|
|
4479
|
-
/* @__PURE__ */
|
|
4859
|
+
/* @__PURE__ */ jsx20(
|
|
4480
4860
|
"button",
|
|
4481
4861
|
{
|
|
4482
4862
|
"data-testid": `bus-fx-toggle-${fx.index}`,
|
|
@@ -4487,7 +4867,7 @@ function PanelMasterStrip({
|
|
|
4487
4867
|
children: fx.enabled ? "\u25CF" : "\u25CB"
|
|
4488
4868
|
}
|
|
4489
4869
|
),
|
|
4490
|
-
onShowFxEditor ? /* @__PURE__ */
|
|
4870
|
+
onShowFxEditor ? /* @__PURE__ */ jsx20(
|
|
4491
4871
|
"button",
|
|
4492
4872
|
{
|
|
4493
4873
|
"data-testid": `bus-fx-edit-${fx.index}`,
|
|
@@ -4497,8 +4877,8 @@ function PanelMasterStrip({
|
|
|
4497
4877
|
title: `Open ${fx.name} editor`,
|
|
4498
4878
|
children: fx.name
|
|
4499
4879
|
}
|
|
4500
|
-
) : /* @__PURE__ */
|
|
4501
|
-
/* @__PURE__ */
|
|
4880
|
+
) : /* @__PURE__ */ jsx20("span", { className: "max-w-[80px] truncate", children: fx.name }),
|
|
4881
|
+
/* @__PURE__ */ jsx20(
|
|
4502
4882
|
"button",
|
|
4503
4883
|
{
|
|
4504
4884
|
"data-testid": `bus-fx-remove-${fx.index}`,
|
|
@@ -4513,7 +4893,7 @@ function PanelMasterStrip({
|
|
|
4513
4893
|
},
|
|
4514
4894
|
`${fx.index}:${fx.pluginId}`
|
|
4515
4895
|
)) }),
|
|
4516
|
-
/* @__PURE__ */
|
|
4896
|
+
/* @__PURE__ */ jsx20(
|
|
4517
4897
|
"button",
|
|
4518
4898
|
{
|
|
4519
4899
|
"data-testid": "bus-fx-add-button",
|
|
@@ -4525,9 +4905,9 @@ function PanelMasterStrip({
|
|
|
4525
4905
|
}
|
|
4526
4906
|
)
|
|
4527
4907
|
] }),
|
|
4528
|
-
fxPickerOpen && /* @__PURE__ */
|
|
4529
|
-
/* @__PURE__ */
|
|
4530
|
-
/* @__PURE__ */
|
|
4908
|
+
fxPickerOpen && /* @__PURE__ */ jsxs16("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
|
|
4909
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
|
|
4910
|
+
/* @__PURE__ */ jsx20(
|
|
4531
4911
|
"input",
|
|
4532
4912
|
{
|
|
4533
4913
|
type: "text",
|
|
@@ -4537,7 +4917,7 @@ function PanelMasterStrip({
|
|
|
4537
4917
|
className: "sas-input flex-1 px-2 py-1 text-xs"
|
|
4538
4918
|
}
|
|
4539
4919
|
),
|
|
4540
|
-
onRefreshFx && /* @__PURE__ */
|
|
4920
|
+
onRefreshFx && /* @__PURE__ */ jsx20(
|
|
4541
4921
|
"button",
|
|
4542
4922
|
{
|
|
4543
4923
|
onClick: () => onRefreshFx(),
|
|
@@ -4548,8 +4928,8 @@ function PanelMasterStrip({
|
|
|
4548
4928
|
}
|
|
4549
4929
|
)
|
|
4550
4930
|
] }),
|
|
4551
|
-
fxLoading && availableFx.length === 0 ? /* @__PURE__ */
|
|
4552
|
-
filtered.map((fx) => /* @__PURE__ */
|
|
4931
|
+
fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs16("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
|
|
4932
|
+
filtered.map((fx) => /* @__PURE__ */ jsxs16(
|
|
4553
4933
|
"button",
|
|
4554
4934
|
{
|
|
4555
4935
|
"data-testid": `bus-fx-pick-${fx.pluginId}`,
|
|
@@ -4557,13 +4937,13 @@ function PanelMasterStrip({
|
|
|
4557
4937
|
className: "flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent",
|
|
4558
4938
|
title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
|
|
4559
4939
|
children: [
|
|
4560
|
-
/* @__PURE__ */
|
|
4561
|
-
/* @__PURE__ */
|
|
4940
|
+
/* @__PURE__ */ jsx20("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
|
|
4941
|
+
/* @__PURE__ */ jsx20("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
|
|
4562
4942
|
]
|
|
4563
4943
|
},
|
|
4564
4944
|
fx.pluginId
|
|
4565
4945
|
)),
|
|
4566
|
-
filtered.length === 0 && /* @__PURE__ */
|
|
4946
|
+
filtered.length === 0 && /* @__PURE__ */ jsx20("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
|
|
4567
4947
|
] })
|
|
4568
4948
|
] })
|
|
4569
4949
|
]
|
|
@@ -4690,7 +5070,7 @@ function usePanelBus(host, activeSceneId) {
|
|
|
4690
5070
|
|
|
4691
5071
|
// src/components/DownloadPackButton.tsx
|
|
4692
5072
|
import { useCallback as useCallback11, useEffect as useEffect12, useState as useState16 } from "react";
|
|
4693
|
-
import { jsx as
|
|
5073
|
+
import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
4694
5074
|
function formatSize(bytes) {
|
|
4695
5075
|
if (!bytes || bytes <= 0) return "";
|
|
4696
5076
|
const gb = bytes / 1024 ** 3;
|
|
@@ -4778,8 +5158,8 @@ var DownloadPackButton = ({
|
|
|
4778
5158
|
} else {
|
|
4779
5159
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
4780
5160
|
}
|
|
4781
|
-
return /* @__PURE__ */
|
|
4782
|
-
/* @__PURE__ */
|
|
5161
|
+
return /* @__PURE__ */ jsxs17("div", { children: [
|
|
5162
|
+
/* @__PURE__ */ jsx21(
|
|
4783
5163
|
"button",
|
|
4784
5164
|
{
|
|
4785
5165
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -4790,12 +5170,12 @@ var DownloadPackButton = ({
|
|
|
4790
5170
|
children: buttonLabel
|
|
4791
5171
|
}
|
|
4792
5172
|
),
|
|
4793
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */
|
|
5173
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx21("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
4794
5174
|
] });
|
|
4795
5175
|
};
|
|
4796
5176
|
|
|
4797
5177
|
// src/components/SamplePackCTACard.tsx
|
|
4798
|
-
import { jsx as
|
|
5178
|
+
import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
4799
5179
|
var SamplePackCTACard = ({
|
|
4800
5180
|
host,
|
|
4801
5181
|
pack,
|
|
@@ -4803,7 +5183,7 @@ var SamplePackCTACard = ({
|
|
|
4803
5183
|
onDownloadComplete
|
|
4804
5184
|
}) => {
|
|
4805
5185
|
if (status === "checking") {
|
|
4806
|
-
return /* @__PURE__ */
|
|
5186
|
+
return /* @__PURE__ */ jsx22(
|
|
4807
5187
|
"div",
|
|
4808
5188
|
{
|
|
4809
5189
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -4814,16 +5194,16 @@ var SamplePackCTACard = ({
|
|
|
4814
5194
|
}
|
|
4815
5195
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
4816
5196
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
4817
|
-
return /* @__PURE__ */
|
|
5197
|
+
return /* @__PURE__ */ jsxs18(
|
|
4818
5198
|
"div",
|
|
4819
5199
|
{
|
|
4820
5200
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
4821
5201
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
4822
5202
|
children: [
|
|
4823
|
-
/* @__PURE__ */
|
|
4824
|
-
/* @__PURE__ */
|
|
4825
|
-
/* @__PURE__ */
|
|
4826
|
-
/* @__PURE__ */
|
|
5203
|
+
/* @__PURE__ */ jsx22("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
5204
|
+
/* @__PURE__ */ jsx22("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
5205
|
+
/* @__PURE__ */ jsx22("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
5206
|
+
/* @__PURE__ */ jsx22(
|
|
4827
5207
|
DownloadPackButton,
|
|
4828
5208
|
{
|
|
4829
5209
|
host,
|
|
@@ -4903,7 +5283,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
4903
5283
|
}
|
|
4904
5284
|
|
|
4905
5285
|
// src/components/WaveformView.tsx
|
|
4906
|
-
import { jsx as
|
|
5286
|
+
import { jsx as jsx23 } from "react/jsx-runtime";
|
|
4907
5287
|
var WaveformView = ({
|
|
4908
5288
|
host,
|
|
4909
5289
|
filePath,
|
|
@@ -4951,7 +5331,7 @@ var WaveformView = ({
|
|
|
4951
5331
|
observer.observe(canvas);
|
|
4952
5332
|
return () => observer.disconnect();
|
|
4953
5333
|
}, [peaks, fillStyle]);
|
|
4954
|
-
return /* @__PURE__ */
|
|
5334
|
+
return /* @__PURE__ */ jsx23(
|
|
4955
5335
|
"canvas",
|
|
4956
5336
|
{
|
|
4957
5337
|
ref: canvasRef,
|
|
@@ -4963,7 +5343,7 @@ var WaveformView = ({
|
|
|
4963
5343
|
|
|
4964
5344
|
// src/components/ScrollingWaveform.tsx
|
|
4965
5345
|
import { useEffect as useEffect14, useRef as useRef14 } from "react";
|
|
4966
|
-
import { jsx as
|
|
5346
|
+
import { jsx as jsx24 } from "react/jsx-runtime";
|
|
4967
5347
|
var ScrollingWaveform = ({
|
|
4968
5348
|
getPeakDb,
|
|
4969
5349
|
active,
|
|
@@ -5039,7 +5419,7 @@ var ScrollingWaveform = ({
|
|
|
5039
5419
|
}
|
|
5040
5420
|
};
|
|
5041
5421
|
}, [active, getPeakDb, fillStyle]);
|
|
5042
|
-
return /* @__PURE__ */
|
|
5422
|
+
return /* @__PURE__ */ jsx24(
|
|
5043
5423
|
"canvas",
|
|
5044
5424
|
{
|
|
5045
5425
|
ref: canvasRef,
|
|
@@ -5051,7 +5431,7 @@ var ScrollingWaveform = ({
|
|
|
5051
5431
|
|
|
5052
5432
|
// src/components/OffsetScrubber.tsx
|
|
5053
5433
|
import { useCallback as useCallback12, useEffect as useEffect15, useMemo as useMemo8, useRef as useRef15, useState as useState18 } from "react";
|
|
5054
|
-
import { jsx as
|
|
5434
|
+
import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
5055
5435
|
var SLIDER_HEIGHT_PX = 28;
|
|
5056
5436
|
var TICK_HEIGHT_PX = 14;
|
|
5057
5437
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -5166,9 +5546,9 @@ function OffsetScrubber({
|
|
|
5166
5546
|
});
|
|
5167
5547
|
}, [cuePoints, sampleToFraction]);
|
|
5168
5548
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
5169
|
-
return /* @__PURE__ */
|
|
5170
|
-
/* @__PURE__ */
|
|
5171
|
-
/* @__PURE__ */
|
|
5549
|
+
return /* @__PURE__ */ jsxs19("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
5550
|
+
/* @__PURE__ */ jsx25("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
5551
|
+
/* @__PURE__ */ jsxs19(
|
|
5172
5552
|
"div",
|
|
5173
5553
|
{
|
|
5174
5554
|
ref: trackRef,
|
|
@@ -5184,7 +5564,7 @@ function OffsetScrubber({
|
|
|
5184
5564
|
"aria-valuenow": draftOffset,
|
|
5185
5565
|
"aria-disabled": isDisabled,
|
|
5186
5566
|
children: [
|
|
5187
|
-
/* @__PURE__ */
|
|
5567
|
+
/* @__PURE__ */ jsx25(
|
|
5188
5568
|
"div",
|
|
5189
5569
|
{
|
|
5190
5570
|
"aria-hidden": "true",
|
|
@@ -5192,7 +5572,7 @@ function OffsetScrubber({
|
|
|
5192
5572
|
style: { left: "50%" }
|
|
5193
5573
|
}
|
|
5194
5574
|
),
|
|
5195
|
-
ticks.map((t) => /* @__PURE__ */
|
|
5575
|
+
ticks.map((t) => /* @__PURE__ */ jsx25(
|
|
5196
5576
|
"div",
|
|
5197
5577
|
{
|
|
5198
5578
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -5207,7 +5587,7 @@ function OffsetScrubber({
|
|
|
5207
5587
|
},
|
|
5208
5588
|
t.i
|
|
5209
5589
|
)),
|
|
5210
|
-
/* @__PURE__ */
|
|
5590
|
+
/* @__PURE__ */ jsx25(
|
|
5211
5591
|
"div",
|
|
5212
5592
|
{
|
|
5213
5593
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -5224,7 +5604,7 @@ function OffsetScrubber({
|
|
|
5224
5604
|
]
|
|
5225
5605
|
}
|
|
5226
5606
|
),
|
|
5227
|
-
/* @__PURE__ */
|
|
5607
|
+
/* @__PURE__ */ jsx25(
|
|
5228
5608
|
"span",
|
|
5229
5609
|
{
|
|
5230
5610
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -5232,7 +5612,7 @@ function OffsetScrubber({
|
|
|
5232
5612
|
children: formatOffset(draftOffset, sampleRate)
|
|
5233
5613
|
}
|
|
5234
5614
|
),
|
|
5235
|
-
/* @__PURE__ */
|
|
5615
|
+
/* @__PURE__ */ jsx25(
|
|
5236
5616
|
"button",
|
|
5237
5617
|
{
|
|
5238
5618
|
type: "button",
|
|
@@ -5244,7 +5624,7 @@ function OffsetScrubber({
|
|
|
5244
5624
|
children: "\u2316"
|
|
5245
5625
|
}
|
|
5246
5626
|
),
|
|
5247
|
-
bpmMismatch && /* @__PURE__ */
|
|
5627
|
+
bpmMismatch && /* @__PURE__ */ jsx25(
|
|
5248
5628
|
"span",
|
|
5249
5629
|
{
|
|
5250
5630
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -5650,6 +6030,23 @@ function useTransitionOps({
|
|
|
5650
6030
|
},
|
|
5651
6031
|
[host]
|
|
5652
6032
|
);
|
|
6033
|
+
const copyTrackFx = useCallback15(
|
|
6034
|
+
async (newTrackId, sourceDbId) => {
|
|
6035
|
+
if (typeof host.copyTrackFxFrom !== "function") return;
|
|
6036
|
+
try {
|
|
6037
|
+
const res = await host.copyTrackFxFrom(newTrackId, sourceDbId);
|
|
6038
|
+
if (res.externalMissing.length > 0) {
|
|
6039
|
+
host.showToast(
|
|
6040
|
+
"warning",
|
|
6041
|
+
"Some FX not copied",
|
|
6042
|
+
`Missing plugin(s): ${res.externalMissing.join(", ")}`
|
|
6043
|
+
);
|
|
6044
|
+
}
|
|
6045
|
+
} catch {
|
|
6046
|
+
}
|
|
6047
|
+
},
|
|
6048
|
+
[host]
|
|
6049
|
+
);
|
|
5653
6050
|
const [isCreatingCrossfade, setIsCreatingCrossfade] = useState22(false);
|
|
5654
6051
|
const handleCreateCrossfade = useCallback15(
|
|
5655
6052
|
async (origin, target) => {
|
|
@@ -5728,6 +6125,8 @@ function useTransitionOps({
|
|
|
5728
6125
|
};
|
|
5729
6126
|
const originLabel = await copySound(top.id, origin.dbId);
|
|
5730
6127
|
const targetLabel = await copySound(bottom.id, target.dbId);
|
|
6128
|
+
await copyTrackFx(top.id, origin.dbId);
|
|
6129
|
+
await copyTrackFx(bottom.id, target.dbId);
|
|
5731
6130
|
await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
|
|
5732
6131
|
const groupId = top.dbId;
|
|
5733
6132
|
const originMeta = {
|
|
@@ -5779,6 +6178,7 @@ function useTransitionOps({
|
|
|
5779
6178
|
loadTracks
|
|
5780
6179
|
]
|
|
5781
6180
|
);
|
|
6181
|
+
const [isCreatingGroupFade, setIsCreatingGroupFade] = useState22(false);
|
|
5782
6182
|
const [isCreatingFade, setIsCreatingFade] = useState22(false);
|
|
5783
6183
|
const handleCreateFade = useCallback15(
|
|
5784
6184
|
async (selection, direction, gesture) => {
|
|
@@ -5847,6 +6247,7 @@ function useTransitionOps({
|
|
|
5847
6247
|
soundLabel = await adapter.sound.copySnapshot(track.id, snap);
|
|
5848
6248
|
}
|
|
5849
6249
|
}
|
|
6250
|
+
await copyTrackFx(track.id, selection.dbId);
|
|
5850
6251
|
await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
|
|
5851
6252
|
appliedFadeAutomationRef.current.add(track.id);
|
|
5852
6253
|
const meta = {
|
|
@@ -6031,6 +6432,188 @@ function useTransitionOps({
|
|
|
6031
6432
|
},
|
|
6032
6433
|
[host, activeSceneId, applyFadeAutomation, setFadesMeta]
|
|
6033
6434
|
);
|
|
6435
|
+
const handleCreateVerbatimGroupFade = useCallback15(
|
|
6436
|
+
async (subject, direction) => {
|
|
6437
|
+
const groupAdapter = adapter.transitionGroup;
|
|
6438
|
+
if (!groupAdapter) throw new Error("This panel does not support group fades.");
|
|
6439
|
+
const scene = activeSceneId;
|
|
6440
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
6441
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
6442
|
+
if (!scene) throw new Error("No active scene.");
|
|
6443
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
6444
|
+
const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
|
|
6445
|
+
const members = await groupAdapter.expandSubject(sourceSceneId, subject.dbId);
|
|
6446
|
+
if (members.length === 0) {
|
|
6447
|
+
throw new Error("Nothing to fade \u2014 the source group has no members.");
|
|
6448
|
+
}
|
|
6449
|
+
if (tracks.length + members.length > identity.maxTracks) {
|
|
6450
|
+
throw new Error(`Not enough track slots for this fade (needs ${members.length}).`);
|
|
6451
|
+
}
|
|
6452
|
+
setIsCreatingGroupFade(true);
|
|
6453
|
+
const created = [];
|
|
6454
|
+
try {
|
|
6455
|
+
const mc = await host.getMusicalContext();
|
|
6456
|
+
const sliderPos = groupAdapter.defaultSliderPos?.(direction) ?? 0.5;
|
|
6457
|
+
const gesture = "volume";
|
|
6458
|
+
const clipEnd = mc.bars * 4 * 60 / mc.bpm;
|
|
6459
|
+
const maxBeats = mc.bars * 4;
|
|
6460
|
+
for (const member of members) {
|
|
6461
|
+
const handle = await host.createTrack({
|
|
6462
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}-v${member.memberIndex}`,
|
|
6463
|
+
...adapter.createTrackOptions()
|
|
6464
|
+
});
|
|
6465
|
+
created.push({ handle, member, soundLabel: "default" });
|
|
6466
|
+
const role = member.role ?? subject.role ?? "";
|
|
6467
|
+
if (role) await host.setTrackRole(handle.id, role).catch(() => {
|
|
6468
|
+
});
|
|
6469
|
+
const srcMidi = host.readImportableTrackMidi ? await host.readImportableTrackMidi(member.dbId) : { clips: [] };
|
|
6470
|
+
const srcNotes = srcMidi.clips[0]?.notes ?? [];
|
|
6471
|
+
const notes = srcNotes.filter((n) => n.startBeat < maxBeats).map((n) => ({
|
|
6472
|
+
...n,
|
|
6473
|
+
durationBeats: Math.min(n.durationBeats, maxBeats - n.startBeat)
|
|
6474
|
+
}));
|
|
6475
|
+
if (notes.length > 0) {
|
|
6476
|
+
const clip = { startTime: 0, endTime: clipEnd, tempo: mc.bpm, notes };
|
|
6477
|
+
await host.writeMidiClip(handle.id, clip);
|
|
6478
|
+
}
|
|
6479
|
+
if (host.getTrackSound) {
|
|
6480
|
+
const snap = await host.getTrackSound(member.dbId);
|
|
6481
|
+
if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
|
|
6482
|
+
created[created.length - 1].soundLabel = await adapter.sound.copySnapshot(
|
|
6483
|
+
handle.id,
|
|
6484
|
+
snap
|
|
6485
|
+
);
|
|
6486
|
+
}
|
|
6487
|
+
}
|
|
6488
|
+
await copyTrackFx(handle.id, member.dbId);
|
|
6489
|
+
await applyFadeAutomation(handle.id, direction, mc.bars, mc.bpm, sliderPos, gesture);
|
|
6490
|
+
appliedFadeAutomationRef.current.add(handle.id);
|
|
6491
|
+
}
|
|
6492
|
+
const groupId = created[0].handle.dbId;
|
|
6493
|
+
for (const { handle, member, soundLabel } of created) {
|
|
6494
|
+
const meta = {
|
|
6495
|
+
direction,
|
|
6496
|
+
gesture,
|
|
6497
|
+
sourceTrackDbId: member.dbId,
|
|
6498
|
+
sourceSceneId,
|
|
6499
|
+
sourceName: member.name,
|
|
6500
|
+
soundLabel,
|
|
6501
|
+
sliderPos,
|
|
6502
|
+
groupId,
|
|
6503
|
+
memberIndex: member.memberIndex,
|
|
6504
|
+
memberLabel: member.memberLabel
|
|
6505
|
+
};
|
|
6506
|
+
await host.setSceneData(scene, `track:${handle.dbId}:fade`, meta);
|
|
6507
|
+
}
|
|
6508
|
+
await groupAdapter.writeGroupMetas(
|
|
6509
|
+
scene,
|
|
6510
|
+
created.map(({ handle, member }) => ({ newDbId: handle.dbId, member })),
|
|
6511
|
+
groupId
|
|
6512
|
+
);
|
|
6513
|
+
await loadTracks(true);
|
|
6514
|
+
host.showToast(
|
|
6515
|
+
"success",
|
|
6516
|
+
direction === "in" ? "Fade in created" : "Fade out created",
|
|
6517
|
+
subject.name
|
|
6518
|
+
);
|
|
6519
|
+
} catch (err) {
|
|
6520
|
+
for (const { handle } of [...created].reverse()) {
|
|
6521
|
+
try {
|
|
6522
|
+
await host.deleteTrack(handle.id);
|
|
6523
|
+
} catch {
|
|
6524
|
+
}
|
|
6525
|
+
await host.deleteSceneData(scene, `track:${handle.dbId}:fade`).catch(() => {
|
|
6526
|
+
});
|
|
6527
|
+
for (const suffix of groupAdapter.cleanupKeySuffixes) {
|
|
6528
|
+
await host.deleteSceneData(scene, `track:${handle.dbId}:${suffix}`).catch(() => {
|
|
6529
|
+
});
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
6533
|
+
} finally {
|
|
6534
|
+
setIsCreatingGroupFade(false);
|
|
6535
|
+
}
|
|
6536
|
+
},
|
|
6537
|
+
[
|
|
6538
|
+
host,
|
|
6539
|
+
adapter,
|
|
6540
|
+
identity,
|
|
6541
|
+
activeSceneId,
|
|
6542
|
+
isConnected,
|
|
6543
|
+
tracks.length,
|
|
6544
|
+
sceneContext,
|
|
6545
|
+
applyFadeAutomation,
|
|
6546
|
+
copyTrackFx,
|
|
6547
|
+
loadTracks
|
|
6548
|
+
]
|
|
6549
|
+
);
|
|
6550
|
+
const groupFadeSliderTimers = useRef18({});
|
|
6551
|
+
const handleGroupFadeSlider = useCallback15(
|
|
6552
|
+
(group, pos) => {
|
|
6553
|
+
setFadesMeta(
|
|
6554
|
+
(prev) => prev.map(
|
|
6555
|
+
(f) => f.meta.groupId === group.groupId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f
|
|
6556
|
+
)
|
|
6557
|
+
);
|
|
6558
|
+
if (groupFadeSliderTimers.current[group.groupId]) {
|
|
6559
|
+
clearTimeout(groupFadeSliderTimers.current[group.groupId]);
|
|
6560
|
+
}
|
|
6561
|
+
groupFadeSliderTimers.current[group.groupId] = setTimeout(() => {
|
|
6562
|
+
void (async () => {
|
|
6563
|
+
const mc = await host.getMusicalContext();
|
|
6564
|
+
const sceneData = activeSceneId ? await host.getAllSceneData(activeSceneId) : null;
|
|
6565
|
+
for (const member of group.members) {
|
|
6566
|
+
await applyFadeAutomation(
|
|
6567
|
+
member.track.handle.id,
|
|
6568
|
+
group.direction,
|
|
6569
|
+
mc.bars,
|
|
6570
|
+
mc.bpm,
|
|
6571
|
+
pos,
|
|
6572
|
+
group.gesture
|
|
6573
|
+
);
|
|
6574
|
+
if (activeSceneId && sceneData) {
|
|
6575
|
+
const meta = asFadeMeta(sceneData[`track:${member.dbId}:fade`]);
|
|
6576
|
+
if (meta) {
|
|
6577
|
+
host.setSceneData(activeSceneId, `track:${member.dbId}:fade`, {
|
|
6578
|
+
...meta,
|
|
6579
|
+
sliderPos: pos
|
|
6580
|
+
}).catch(() => {
|
|
6581
|
+
});
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
})();
|
|
6586
|
+
}, 200);
|
|
6587
|
+
},
|
|
6588
|
+
[host, activeSceneId, applyFadeAutomation, setFadesMeta]
|
|
6589
|
+
);
|
|
6590
|
+
const handleGroupFadeDelete = useCallback15(
|
|
6591
|
+
async (group) => {
|
|
6592
|
+
const suffixes = ["fade", ...adapter.transitionGroup?.cleanupKeySuffixes ?? []];
|
|
6593
|
+
try {
|
|
6594
|
+
for (const member of group.members) {
|
|
6595
|
+
await host.deleteTrack(member.track.handle.id);
|
|
6596
|
+
if (activeSceneId) {
|
|
6597
|
+
for (const suffix of suffixes) {
|
|
6598
|
+
await host.deleteSceneData(activeSceneId, `track:${member.dbId}:${suffix}`).catch(() => {
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
setFadesMeta((prev) => prev.filter((f) => f.meta.groupId !== group.groupId));
|
|
6604
|
+
const gone = new Set(group.members.map((m) => m.track.handle.id));
|
|
6605
|
+
setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
|
|
6606
|
+
host.showToast("success", "Fade removed");
|
|
6607
|
+
} catch (err) {
|
|
6608
|
+
host.showToast(
|
|
6609
|
+
"error",
|
|
6610
|
+
"Failed to delete fade",
|
|
6611
|
+
err instanceof Error ? err.message : String(err)
|
|
6612
|
+
);
|
|
6613
|
+
}
|
|
6614
|
+
},
|
|
6615
|
+
[host, adapter, activeSceneId, setFadesMeta, setTracks]
|
|
6616
|
+
);
|
|
6034
6617
|
const lastResyncKeyRef = useRef18("");
|
|
6035
6618
|
useEffect17(() => {
|
|
6036
6619
|
if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
|
|
@@ -6102,12 +6685,16 @@ function useTransitionOps({
|
|
|
6102
6685
|
handleCrossfadeDelete,
|
|
6103
6686
|
handleCrossfadeSlider,
|
|
6104
6687
|
handleFadeDelete,
|
|
6105
|
-
handleFadeSlider
|
|
6688
|
+
handleFadeSlider,
|
|
6689
|
+
isCreatingGroupFade,
|
|
6690
|
+
handleCreateVerbatimGroupFade,
|
|
6691
|
+
handleGroupFadeSlider,
|
|
6692
|
+
handleGroupFadeDelete
|
|
6106
6693
|
};
|
|
6107
6694
|
}
|
|
6108
6695
|
|
|
6109
6696
|
// src/panel-core/useGeneratorPanelCore.tsx
|
|
6110
|
-
import { jsx as
|
|
6697
|
+
import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
6111
6698
|
var EMPTY_PLACEHOLDERS = [];
|
|
6112
6699
|
function useGeneratorPanelCore({
|
|
6113
6700
|
ui,
|
|
@@ -6590,8 +7177,8 @@ function useGeneratorPanelCore({
|
|
|
6590
7177
|
if (!onHeaderContent) return;
|
|
6591
7178
|
const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
|
|
6592
7179
|
onHeaderContent(
|
|
6593
|
-
/* @__PURE__ */
|
|
6594
|
-
features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */
|
|
7180
|
+
/* @__PURE__ */ jsxs20("div", { className: "flex gap-1 items-center", children: [
|
|
7181
|
+
features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx26(
|
|
6595
7182
|
"button",
|
|
6596
7183
|
{
|
|
6597
7184
|
"data-testid": `import-from-scene-${identity.familyKey}-button`,
|
|
@@ -6605,7 +7192,7 @@ function useGeneratorPanelCore({
|
|
|
6605
7192
|
children: identity.importTrackLabel ?? "Import Track"
|
|
6606
7193
|
}
|
|
6607
7194
|
),
|
|
6608
|
-
(!canCrossfade || !designerView) && /* @__PURE__ */
|
|
7195
|
+
(!canCrossfade || !designerView) && /* @__PURE__ */ jsx26(
|
|
6609
7196
|
"button",
|
|
6610
7197
|
{
|
|
6611
7198
|
"data-testid": `add-${identity.familyKey}-track-button`,
|
|
@@ -6621,7 +7208,7 @@ function useGeneratorPanelCore({
|
|
|
6621
7208
|
children: identity.addTrackLabel ?? "Add Track"
|
|
6622
7209
|
}
|
|
6623
7210
|
),
|
|
6624
|
-
canCrossfade && /* @__PURE__ */
|
|
7211
|
+
canCrossfade && /* @__PURE__ */ jsxs20(
|
|
6625
7212
|
"button",
|
|
6626
7213
|
{
|
|
6627
7214
|
"data-testid": `${identity.familyKey}-view-toggle`,
|
|
@@ -6640,7 +7227,7 @@ function useGeneratorPanelCore({
|
|
|
6640
7227
|
title: designerView ? "Back to the track list" : "Open the transition designer",
|
|
6641
7228
|
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",
|
|
6642
7229
|
children: [
|
|
6643
|
-
transitionSourceTotal > 0 && /* @__PURE__ */
|
|
7230
|
+
transitionSourceTotal > 0 && /* @__PURE__ */ jsx26(
|
|
6644
7231
|
"span",
|
|
6645
7232
|
{
|
|
6646
7233
|
className: "absolute inset-y-0 left-0 bg-sas-accent/25",
|
|
@@ -6648,7 +7235,7 @@ function useGeneratorPanelCore({
|
|
|
6648
7235
|
"aria-hidden": true
|
|
6649
7236
|
}
|
|
6650
7237
|
),
|
|
6651
|
-
/* @__PURE__ */
|
|
7238
|
+
/* @__PURE__ */ jsxs20("span", { className: "relative", children: [
|
|
6652
7239
|
"\u21C4 ",
|
|
6653
7240
|
designerView ? "Transition" : "Tracks",
|
|
6654
7241
|
transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
|
|
@@ -7206,6 +7793,10 @@ function useGeneratorPanelCore({
|
|
|
7206
7793
|
}
|
|
7207
7794
|
return { resolvedFades: list, fadeMemberDbIds: members };
|
|
7208
7795
|
}, [tracks, fadesMeta]);
|
|
7796
|
+
const { singles: resolvedSingleFades, groups: resolvedGroupFades } = useMemo10(
|
|
7797
|
+
() => splitFadeEntries(resolvedFades),
|
|
7798
|
+
[resolvedFades]
|
|
7799
|
+
);
|
|
7209
7800
|
const transition = useTransitionOps({
|
|
7210
7801
|
host,
|
|
7211
7802
|
adapter,
|
|
@@ -7342,6 +7933,8 @@ function useGeneratorPanelCore({
|
|
|
7342
7933
|
crossfadeMemberDbIds,
|
|
7343
7934
|
resolvedFades,
|
|
7344
7935
|
fadeMemberDbIds,
|
|
7936
|
+
resolvedSingleFades,
|
|
7937
|
+
resolvedGroupFades,
|
|
7345
7938
|
resolvedGenericGroups,
|
|
7346
7939
|
genericGroupMemberDbIds,
|
|
7347
7940
|
availableInstruments,
|
|
@@ -7379,8 +7972,8 @@ function useGeneratorPanelCore({
|
|
|
7379
7972
|
}
|
|
7380
7973
|
|
|
7381
7974
|
// src/panel-core/GeneratorPanelShell.tsx
|
|
7382
|
-
import
|
|
7383
|
-
import { Fragment as
|
|
7975
|
+
import React23, { useCallback as useCallback17 } from "react";
|
|
7976
|
+
import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
7384
7977
|
function GeneratorPanelShell({ core, slots }) {
|
|
7385
7978
|
const {
|
|
7386
7979
|
ui,
|
|
@@ -7409,7 +8002,8 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7409
8002
|
fadesMeta,
|
|
7410
8003
|
resolvedCrossfadePairs,
|
|
7411
8004
|
crossfadeMemberDbIds,
|
|
7412
|
-
|
|
8005
|
+
resolvedSingleFades,
|
|
8006
|
+
resolvedGroupFades,
|
|
7413
8007
|
fadeMemberDbIds,
|
|
7414
8008
|
resolvedGenericGroups,
|
|
7415
8009
|
genericGroupMemberDbIds,
|
|
@@ -7535,12 +8129,12 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7535
8129
|
]
|
|
7536
8130
|
);
|
|
7537
8131
|
if (!activeSceneId) {
|
|
7538
|
-
return /* @__PURE__ */
|
|
8132
|
+
return /* @__PURE__ */ jsx27(
|
|
7539
8133
|
"div",
|
|
7540
8134
|
{
|
|
7541
8135
|
"data-testid": `no-scene-placeholder-${identity.familyKey}`,
|
|
7542
8136
|
className: "flex items-center justify-center py-8",
|
|
7543
|
-
children: /* @__PURE__ */
|
|
8137
|
+
children: /* @__PURE__ */ jsx27(
|
|
7544
8138
|
"button",
|
|
7545
8139
|
{
|
|
7546
8140
|
onClick: () => onSelectScene?.(),
|
|
@@ -7552,12 +8146,12 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7552
8146
|
);
|
|
7553
8147
|
}
|
|
7554
8148
|
if (!sceneContext?.hasContract) {
|
|
7555
|
-
return /* @__PURE__ */
|
|
8149
|
+
return /* @__PURE__ */ jsx27(
|
|
7556
8150
|
"div",
|
|
7557
8151
|
{
|
|
7558
8152
|
"data-testid": `no-contract-placeholder-${identity.familyKey}`,
|
|
7559
8153
|
className: "flex items-center justify-center py-8",
|
|
7560
|
-
children: /* @__PURE__ */
|
|
8154
|
+
children: /* @__PURE__ */ jsx27(
|
|
7561
8155
|
"button",
|
|
7562
8156
|
{
|
|
7563
8157
|
onClick: () => onOpenContract?.(),
|
|
@@ -7569,7 +8163,7 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7569
8163
|
);
|
|
7570
8164
|
}
|
|
7571
8165
|
if (features.bulkComposePlaceholders && isComposing) {
|
|
7572
|
-
return /* @__PURE__ */
|
|
8166
|
+
return /* @__PURE__ */ jsx27("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx27(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
|
|
7573
8167
|
}
|
|
7574
8168
|
const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
|
|
7575
8169
|
if (activePlaceholders.length > 0) {
|
|
@@ -7580,18 +8174,18 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7580
8174
|
tracksByDbId.set(t.handle.id, t);
|
|
7581
8175
|
}
|
|
7582
8176
|
}
|
|
7583
|
-
return /* @__PURE__ */
|
|
8177
|
+
return /* @__PURE__ */ jsx27("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
|
|
7584
8178
|
const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
|
|
7585
8179
|
if (loadedTrack) {
|
|
7586
|
-
return /* @__PURE__ */
|
|
8180
|
+
return /* @__PURE__ */ jsx27(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
|
|
7587
8181
|
}
|
|
7588
|
-
return /* @__PURE__ */
|
|
8182
|
+
return /* @__PURE__ */ jsx27(
|
|
7589
8183
|
"div",
|
|
7590
8184
|
{
|
|
7591
8185
|
"data-testid": "bulk-placeholder-track",
|
|
7592
8186
|
className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
|
|
7593
8187
|
style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
|
|
7594
|
-
children: /* @__PURE__ */
|
|
8188
|
+
children: /* @__PURE__ */ jsx27(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
|
|
7595
8189
|
},
|
|
7596
8190
|
ph.id
|
|
7597
8191
|
);
|
|
@@ -7603,13 +8197,13 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7603
8197
|
supportsMeters,
|
|
7604
8198
|
levels: supportsMeters ? trackLevels : void 0,
|
|
7605
8199
|
handlers,
|
|
7606
|
-
renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */
|
|
8200
|
+
renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx27(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
|
|
7607
8201
|
setGroupMute,
|
|
7608
8202
|
setGroupSolo,
|
|
7609
8203
|
deleteGroup
|
|
7610
8204
|
};
|
|
7611
|
-
return /* @__PURE__ */
|
|
7612
|
-
features.importTracks && host.listImportableTracks && /* @__PURE__ */
|
|
8205
|
+
return /* @__PURE__ */ jsxs21("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
|
|
8206
|
+
features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx27(
|
|
7613
8207
|
ImportTrackModal,
|
|
7614
8208
|
{
|
|
7615
8209
|
host,
|
|
@@ -7622,7 +8216,7 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7622
8216
|
testIdPrefix: `${identity.familyKey}-import`
|
|
7623
8217
|
}
|
|
7624
8218
|
),
|
|
7625
|
-
features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */
|
|
8219
|
+
features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx27(
|
|
7626
8220
|
ImportTrackModal,
|
|
7627
8221
|
{
|
|
7628
8222
|
host,
|
|
@@ -7637,7 +8231,7 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7637
8231
|
}
|
|
7638
8232
|
),
|
|
7639
8233
|
slots?.modals,
|
|
7640
|
-
canCrossfade && xfFromId && xfToId && /* @__PURE__ */
|
|
8234
|
+
canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx27("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx27(
|
|
7641
8235
|
TransitionDesigner,
|
|
7642
8236
|
{
|
|
7643
8237
|
host,
|
|
@@ -7648,14 +8242,16 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7648
8242
|
...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
|
|
7649
8243
|
...fadesMeta.map((f) => f.meta.sourceTrackDbId)
|
|
7650
8244
|
],
|
|
7651
|
-
onCreateCrossfade: transition.handleCreateCrossfade,
|
|
7652
|
-
onCreateFade: transition.handleCreateFade,
|
|
8245
|
+
onCreateCrossfade: adapter.transitionGroup?.fadeOnly ? void 0 : transition.handleCreateCrossfade,
|
|
8246
|
+
onCreateFade: adapter.transitionGroup ? (sel, dir) => transition.handleCreateVerbatimGroupFade(sel, dir) : transition.handleCreateFade,
|
|
8247
|
+
mapColumnSubjects: adapter.transitionGroup ? (sceneId, tracks2) => adapter.transitionGroup.mapColumnSubjects(sceneId, tracks2) : void 0,
|
|
8248
|
+
fadeEstimateMs: adapter.transitionGroup?.fadeEstimateMs,
|
|
7653
8249
|
familyLabel: identity.familyLabel,
|
|
7654
8250
|
testIdPrefix: `${identity.familyKey}-transition-designer`
|
|
7655
8251
|
}
|
|
7656
8252
|
) }),
|
|
7657
|
-
!(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */
|
|
7658
|
-
panelBus.supported && panelBus.bus && /* @__PURE__ */
|
|
8253
|
+
!(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx27("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs21(Fragment7, { children: [
|
|
8254
|
+
panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx27(
|
|
7659
8255
|
PanelMasterStrip,
|
|
7660
8256
|
{
|
|
7661
8257
|
bus: panelBus.bus,
|
|
@@ -7676,7 +8272,7 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7676
8272
|
}
|
|
7677
8273
|
),
|
|
7678
8274
|
slots?.beforeRows,
|
|
7679
|
-
resolvedCrossfadePairs.map((pair) => /* @__PURE__ */
|
|
8275
|
+
resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx27(
|
|
7680
8276
|
CrossfadeTrackRow,
|
|
7681
8277
|
{
|
|
7682
8278
|
accentColor: identity.transitionAccentColor,
|
|
@@ -7713,7 +8309,7 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7713
8309
|
},
|
|
7714
8310
|
pair.groupId
|
|
7715
8311
|
)),
|
|
7716
|
-
|
|
8312
|
+
resolvedSingleFades.map((fade) => /* @__PURE__ */ jsx27(
|
|
7717
8313
|
FadeTrackRow,
|
|
7718
8314
|
{
|
|
7719
8315
|
accentColor: identity.transitionAccentColor,
|
|
@@ -7738,21 +8334,56 @@ function GeneratorPanelShell({ core, slots }) {
|
|
|
7738
8334
|
},
|
|
7739
8335
|
fade.dbId
|
|
7740
8336
|
)),
|
|
8337
|
+
resolvedGroupFades.map((group) => /* @__PURE__ */ jsx27(
|
|
8338
|
+
GroupFadeTrackRow,
|
|
8339
|
+
{
|
|
8340
|
+
accentColor: identity.transitionAccentColor,
|
|
8341
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
8342
|
+
direction: group.direction,
|
|
8343
|
+
gesture: group.gesture,
|
|
8344
|
+
sliderPos: group.sliderPos,
|
|
8345
|
+
groupLabel: adapter.transitionGroup?.groupRowLabel?.(group.members.length) ?? `Group (${group.members.length} tracks)`,
|
|
8346
|
+
members: group.members.map((m) => ({
|
|
8347
|
+
trackId: m.track.handle.id,
|
|
8348
|
+
name: m.track.handle.name,
|
|
8349
|
+
role: m.track.role,
|
|
8350
|
+
sourceName: m.meta.sourceName,
|
|
8351
|
+
soundLabel: m.meta.soundLabel,
|
|
8352
|
+
memberLabel: m.meta.memberLabel,
|
|
8353
|
+
runtimeState: m.track.runtimeState
|
|
8354
|
+
})),
|
|
8355
|
+
onMemberMuteToggle: (trackId) => handlers.muteToggle(trackId),
|
|
8356
|
+
onMemberSoloToggle: (trackId) => handlers.soloToggle(trackId),
|
|
8357
|
+
onMemberVolumeChange: (trackId, vol) => handlers.volumeChange(trackId, vol),
|
|
8358
|
+
onMemberPanChange: (trackId, pan) => handlers.panChange(trackId, pan),
|
|
8359
|
+
onMuteAll: () => setGroupMute(
|
|
8360
|
+
group.members.map((m) => m.track.handle.id),
|
|
8361
|
+
!group.members.every((m) => m.track.runtimeState.muted)
|
|
8362
|
+
),
|
|
8363
|
+
onSoloAll: () => setGroupSolo(
|
|
8364
|
+
group.members.map((m) => m.track.handle.id),
|
|
8365
|
+
!group.members.every((m) => m.track.runtimeState.solo)
|
|
8366
|
+
),
|
|
8367
|
+
onSliderChange: (pos) => transition.handleGroupFadeSlider(group, pos),
|
|
8368
|
+
onDelete: () => transition.handleGroupFadeDelete(group)
|
|
8369
|
+
},
|
|
8370
|
+
group.groupId
|
|
8371
|
+
)),
|
|
7741
8372
|
(adapter.groupExtensions ?? []).flatMap(
|
|
7742
|
-
(ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */
|
|
8373
|
+
(ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).filter((group) => !group.members.every((m) => fadeMemberDbIds.has(m.dbId))).map((group) => /* @__PURE__ */ jsx27(React23.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
|
|
7743
8374
|
),
|
|
7744
8375
|
tracks.map((track, index) => {
|
|
7745
8376
|
if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
|
|
7746
8377
|
return null;
|
|
7747
8378
|
}
|
|
7748
|
-
return /* @__PURE__ */
|
|
8379
|
+
return /* @__PURE__ */ jsx27(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
|
|
7749
8380
|
}),
|
|
7750
8381
|
slots?.afterRows
|
|
7751
8382
|
] })),
|
|
7752
8383
|
features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
|
|
7753
8384
|
const hasAnyMidi = tracks.some((t) => t.hasMidi);
|
|
7754
8385
|
const exportDisabled = isExportingMidi || !hasAnyMidi;
|
|
7755
|
-
return /* @__PURE__ */
|
|
8386
|
+
return /* @__PURE__ */ jsx27("div", { className: "pt-2", children: /* @__PURE__ */ jsx27(
|
|
7756
8387
|
"button",
|
|
7757
8388
|
{
|
|
7758
8389
|
"data-testid": "export-midi-tracks-button",
|
|
@@ -7819,31 +8450,447 @@ function createSurgeSoundAdapter(host, overrides = {}) {
|
|
|
7819
8450
|
};
|
|
7820
8451
|
}
|
|
7821
8452
|
|
|
8453
|
+
// src/ensemble-core/voice-spec.ts
|
|
8454
|
+
var TOP = {
|
|
8455
|
+
label: "high florid line",
|
|
8456
|
+
role: "lead",
|
|
8457
|
+
registerLow: 72,
|
|
8458
|
+
registerHigh: 96,
|
|
8459
|
+
maxNotesPerBar: 8,
|
|
8460
|
+
rhythmPalette: "8ths and 16ths; melisma and short runs welcome",
|
|
8461
|
+
harmonicDiscipline: "freest voice \u2014 non-chord tones as passing/neighbor tones on weak beats, resolving by step",
|
|
8462
|
+
monoPreference: "high"
|
|
8463
|
+
};
|
|
8464
|
+
var COUNTER = {
|
|
8465
|
+
label: "countermelody",
|
|
8466
|
+
role: "strings",
|
|
8467
|
+
registerLow: 65,
|
|
8468
|
+
registerHigh: 86,
|
|
8469
|
+
maxNotesPerBar: 6,
|
|
8470
|
+
rhythmPalette: "8ths and quarters; move when the top voice rests",
|
|
8471
|
+
harmonicDiscipline: "mostly chord tones; may imitate the top voice's motifs a bar later",
|
|
8472
|
+
monoPreference: "high"
|
|
8473
|
+
};
|
|
8474
|
+
var INNER = {
|
|
8475
|
+
label: "inner voice",
|
|
8476
|
+
role: "strings",
|
|
8477
|
+
registerLow: 55,
|
|
8478
|
+
registerHigh: 76,
|
|
8479
|
+
maxNotesPerBar: 4,
|
|
8480
|
+
rhythmPalette: "quarters and halves",
|
|
8481
|
+
harmonicDiscipline: "chord tones with smooth stepwise motion between them",
|
|
8482
|
+
monoPreference: "high"
|
|
8483
|
+
};
|
|
8484
|
+
var INNER_2 = {
|
|
8485
|
+
label: "second inner voice",
|
|
8486
|
+
role: "strings",
|
|
8487
|
+
registerLow: 60,
|
|
8488
|
+
registerHigh: 81,
|
|
8489
|
+
maxNotesPerBar: 5,
|
|
8490
|
+
rhythmPalette: "quarters with occasional 8th-note motion",
|
|
8491
|
+
harmonicDiscipline: "chord tones; fill gaps the other inner voice leaves",
|
|
8492
|
+
monoPreference: "high"
|
|
8493
|
+
};
|
|
8494
|
+
var TENOR = {
|
|
8495
|
+
label: "low counterline",
|
|
8496
|
+
role: "strings",
|
|
8497
|
+
registerLow: 43,
|
|
8498
|
+
registerHigh: 64,
|
|
8499
|
+
maxNotesPerBar: 3,
|
|
8500
|
+
rhythmPalette: "quarters and halves; brief walking figures at cadences",
|
|
8501
|
+
harmonicDiscipline: "roots and fifths emphasized; passing tones only between chord tones",
|
|
8502
|
+
monoPreference: "low"
|
|
8503
|
+
};
|
|
8504
|
+
var BASS = {
|
|
8505
|
+
label: "bassline",
|
|
8506
|
+
role: "bass",
|
|
8507
|
+
registerLow: 36,
|
|
8508
|
+
registerHigh: 60,
|
|
8509
|
+
maxNotesPerBar: 3,
|
|
8510
|
+
rhythmPalette: "quarters and halves",
|
|
8511
|
+
harmonicDiscipline: "chord roots and fifths; stepwise approaches into chord changes",
|
|
8512
|
+
monoPreference: "low"
|
|
8513
|
+
};
|
|
8514
|
+
var SUB = {
|
|
8515
|
+
label: "sub anchor",
|
|
8516
|
+
role: "808s",
|
|
8517
|
+
registerLow: 24,
|
|
8518
|
+
registerHigh: 43,
|
|
8519
|
+
maxNotesPerBar: 2,
|
|
8520
|
+
rhythmPalette: "halves and whole notes; sustain into the bar",
|
|
8521
|
+
harmonicDiscipline: "ROOT pitch class only \u2014 the harmonic anchor",
|
|
8522
|
+
rootOnly: true,
|
|
8523
|
+
monoPreference: "low"
|
|
8524
|
+
};
|
|
8525
|
+
var ENSEMBLE_MIN_VOICES = 2;
|
|
8526
|
+
var ENSEMBLE_MAX_VOICES = 6;
|
|
8527
|
+
var SPEC_TABLES = {
|
|
8528
|
+
2: [TOP, BASS],
|
|
8529
|
+
3: [TOP, INNER, BASS],
|
|
8530
|
+
4: [TOP, COUNTER, TENOR, BASS],
|
|
8531
|
+
5: [TOP, COUNTER, INNER, TENOR, SUB],
|
|
8532
|
+
6: [TOP, COUNTER, INNER_2, INNER, TENOR, SUB]
|
|
8533
|
+
};
|
|
8534
|
+
function defaultVoiceSpecs(voiceCount) {
|
|
8535
|
+
const n = Math.max(ENSEMBLE_MIN_VOICES, Math.min(ENSEMBLE_MAX_VOICES, Math.round(voiceCount)));
|
|
8536
|
+
return SPEC_TABLES[n].map((spec, voiceIndex) => ({ ...spec, voiceIndex }));
|
|
8537
|
+
}
|
|
8538
|
+
|
|
8539
|
+
// src/ensemble-core/enforce-voice.ts
|
|
8540
|
+
var MIN_NOTE_DURATION_BEATS = 0.0625;
|
|
8541
|
+
var BEATS_PER_BAR = 4;
|
|
8542
|
+
function foldPitchToRegister(pitch, low, high) {
|
|
8543
|
+
let p = pitch;
|
|
8544
|
+
while (p < low) p += 12;
|
|
8545
|
+
while (p > high) p -= 12;
|
|
8546
|
+
if (p < low) p = low;
|
|
8547
|
+
if (p > high) p = high;
|
|
8548
|
+
return p;
|
|
8549
|
+
}
|
|
8550
|
+
function nearestPitchWithPc(reference, pc, low, high) {
|
|
8551
|
+
let best = null;
|
|
8552
|
+
for (let p = low; p <= high; p++) {
|
|
8553
|
+
if ((p % 12 + 12) % 12 !== pc) continue;
|
|
8554
|
+
if (best === null || Math.abs(p - reference) < Math.abs(best - reference)) best = p;
|
|
8555
|
+
}
|
|
8556
|
+
return best ?? foldPitchToRegister(reference, low, high);
|
|
8557
|
+
}
|
|
8558
|
+
function snapToNearestPc(pitch, pcs) {
|
|
8559
|
+
if (pcs.size === 0) return pitch;
|
|
8560
|
+
for (let d = 0; d <= 6; d++) {
|
|
8561
|
+
if (pcs.has(((pitch - d) % 12 + 12) % 12)) return pitch - d;
|
|
8562
|
+
if (pcs.has(((pitch + d) % 12 + 12) % 12)) return pitch + d;
|
|
8563
|
+
}
|
|
8564
|
+
return pitch;
|
|
8565
|
+
}
|
|
8566
|
+
function enforceVoice(rawNotes, spec, opts) {
|
|
8567
|
+
const repairs = [];
|
|
8568
|
+
const clipEnd = opts.bars * BEATS_PER_BAR;
|
|
8569
|
+
let notes = [];
|
|
8570
|
+
for (const n of rawNotes) {
|
|
8571
|
+
if (!Number.isFinite(n.pitch) || !Number.isFinite(n.startBeat) || !Number.isFinite(n.durationBeats)) continue;
|
|
8572
|
+
if (n.startBeat >= clipEnd || n.startBeat < 0) {
|
|
8573
|
+
repairs.push(`voice ${spec.voiceIndex}: dropped note outside the ${opts.bars}-bar clip (start ${n.startBeat})`);
|
|
8574
|
+
continue;
|
|
8575
|
+
}
|
|
8576
|
+
const durationBeats = Math.max(
|
|
8577
|
+
MIN_NOTE_DURATION_BEATS,
|
|
8578
|
+
Math.min(n.durationBeats, clipEnd - n.startBeat)
|
|
8579
|
+
);
|
|
8580
|
+
notes.push({ ...n, durationBeats });
|
|
8581
|
+
}
|
|
8582
|
+
notes = notes.map((n) => {
|
|
8583
|
+
const folded = foldPitchToRegister(Math.round(n.pitch), spec.registerLow, spec.registerHigh);
|
|
8584
|
+
if (folded !== n.pitch) {
|
|
8585
|
+
repairs.push(`voice ${spec.voiceIndex}: folded pitch ${n.pitch} into register ${spec.registerLow}-${spec.registerHigh} (${folded})`);
|
|
8586
|
+
}
|
|
8587
|
+
return { ...n, pitch: folded };
|
|
8588
|
+
});
|
|
8589
|
+
if (spec.rootOnly && opts.chordRootPcAtBar) {
|
|
8590
|
+
notes = notes.map((n) => {
|
|
8591
|
+
const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
|
|
8592
|
+
const rootPc = opts.chordRootPcAtBar(bar);
|
|
8593
|
+
if (rootPc === null) return n;
|
|
8594
|
+
const pinned = nearestPitchWithPc(n.pitch, rootPc, spec.registerLow, spec.registerHigh);
|
|
8595
|
+
if (pinned !== n.pitch) {
|
|
8596
|
+
repairs.push(`voice ${spec.voiceIndex}: pinned bar ${bar + 1} note to the chord root (${n.pitch} \u2192 ${pinned})`);
|
|
8597
|
+
}
|
|
8598
|
+
return { ...n, pitch: pinned };
|
|
8599
|
+
});
|
|
8600
|
+
}
|
|
8601
|
+
if (opts.scalePcs && opts.scalePcs.size > 0 && !spec.rootOnly) {
|
|
8602
|
+
notes = notes.map((n) => {
|
|
8603
|
+
const pc = (n.pitch % 12 + 12) % 12;
|
|
8604
|
+
if (opts.scalePcs.has(pc)) return n;
|
|
8605
|
+
const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
|
|
8606
|
+
const chordPcs = opts.chordPcsAtBar?.(bar);
|
|
8607
|
+
if (chordPcs?.has(pc)) return n;
|
|
8608
|
+
const snapped = foldPitchToRegister(
|
|
8609
|
+
snapToNearestPc(n.pitch, opts.scalePcs),
|
|
8610
|
+
spec.registerLow,
|
|
8611
|
+
spec.registerHigh
|
|
8612
|
+
);
|
|
8613
|
+
if (snapped !== n.pitch) {
|
|
8614
|
+
repairs.push(`voice ${spec.voiceIndex}: snapped out-of-key pitch ${n.pitch} \u2192 ${snapped}`);
|
|
8615
|
+
}
|
|
8616
|
+
return { ...n, pitch: snapped };
|
|
8617
|
+
});
|
|
8618
|
+
}
|
|
8619
|
+
notes.sort((a, b) => a.startBeat - b.startBeat || (spec.monoPreference === "high" ? b.pitch - a.pitch : a.pitch - b.pitch));
|
|
8620
|
+
const mono = [];
|
|
8621
|
+
for (const n of notes) {
|
|
8622
|
+
const prev = mono[mono.length - 1];
|
|
8623
|
+
if (prev && Math.abs(prev.startBeat - n.startBeat) < 1e-9) {
|
|
8624
|
+
repairs.push(`voice ${spec.voiceIndex}: dropped simultaneous note ${n.pitch} at beat ${n.startBeat} (voice is one line)`);
|
|
8625
|
+
continue;
|
|
8626
|
+
}
|
|
8627
|
+
if (prev && prev.startBeat + prev.durationBeats > n.startBeat) {
|
|
8628
|
+
prev.durationBeats = Math.max(MIN_NOTE_DURATION_BEATS, n.startBeat - prev.startBeat);
|
|
8629
|
+
}
|
|
8630
|
+
mono.push({ ...n });
|
|
8631
|
+
}
|
|
8632
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
8633
|
+
for (const n of mono) {
|
|
8634
|
+
const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
|
|
8635
|
+
const bucket = byBar.get(bar) ?? [];
|
|
8636
|
+
bucket.push(n);
|
|
8637
|
+
byBar.set(bar, bucket);
|
|
8638
|
+
}
|
|
8639
|
+
const kept = new Set(mono);
|
|
8640
|
+
for (const [bar, bucket] of byBar) {
|
|
8641
|
+
if (bucket.length <= spec.maxNotesPerBar) continue;
|
|
8642
|
+
const strength = (n) => {
|
|
8643
|
+
const beatInBar = n.startBeat - bar * BEATS_PER_BAR;
|
|
8644
|
+
const onDownbeat = Math.abs(beatInBar % 1) < 1e-9 ? 1 : 0;
|
|
8645
|
+
return n.durationBeats * 4 + n.velocity / 127 + onDownbeat * 2;
|
|
8646
|
+
};
|
|
8647
|
+
const ranked = [...bucket].sort((a, b) => strength(a) - strength(b));
|
|
8648
|
+
const excess = bucket.length - spec.maxNotesPerBar;
|
|
8649
|
+
for (let i = 0; i < excess; i++) {
|
|
8650
|
+
kept.delete(ranked[i]);
|
|
8651
|
+
}
|
|
8652
|
+
repairs.push(`voice ${spec.voiceIndex}: thinned bar ${bar + 1} from ${bucket.length} to ${spec.maxNotesPerBar} notes (density cap)`);
|
|
8653
|
+
}
|
|
8654
|
+
return { notes: mono.filter((n) => kept.has(n)), repairs };
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
// src/ensemble-core/analyze-ensemble.ts
|
|
8658
|
+
var EPS = 1e-6;
|
|
8659
|
+
function pitchAt(voice, beat) {
|
|
8660
|
+
for (const n of voice) {
|
|
8661
|
+
if (n.startBeat - EPS <= beat && beat < n.startBeat + n.durationBeats - EPS) return n.pitch;
|
|
8662
|
+
}
|
|
8663
|
+
return null;
|
|
8664
|
+
}
|
|
8665
|
+
function onsetSet(voice) {
|
|
8666
|
+
return [...new Set(voice.map((n) => Math.round(n.startBeat * 96) / 96))].sort((a, b) => a - b);
|
|
8667
|
+
}
|
|
8668
|
+
function analyzeEnsemble(voices) {
|
|
8669
|
+
const pairs = [];
|
|
8670
|
+
for (let i = 0; i + 1 < voices.length; i++) {
|
|
8671
|
+
const upper = voices[i];
|
|
8672
|
+
const lower = voices[i + 1];
|
|
8673
|
+
const upperOnsets = onsetSet(upper);
|
|
8674
|
+
const lowerOnsets = new Set(onsetSet(lower));
|
|
8675
|
+
const shared = upperOnsets.filter((b) => lowerOnsets.has(b));
|
|
8676
|
+
const onsetIndependence = upperOnsets.length === 0 ? 1 : 1 - shared.length / upperOnsets.length;
|
|
8677
|
+
const motion = { contrary: 0, oblique: 0, similar: 0, parallel: 0 };
|
|
8678
|
+
const parallelPerfects = [];
|
|
8679
|
+
for (let s = 0; s + 1 < shared.length; s++) {
|
|
8680
|
+
const [b0, b1] = [shared[s], shared[s + 1]];
|
|
8681
|
+
const u0 = pitchAt(upper, b0);
|
|
8682
|
+
const u1 = pitchAt(upper, b1);
|
|
8683
|
+
const l0 = pitchAt(lower, b0);
|
|
8684
|
+
const l1 = pitchAt(lower, b1);
|
|
8685
|
+
if (u0 === null || u1 === null || l0 === null || l1 === null) continue;
|
|
8686
|
+
const du = Math.sign(u1 - u0);
|
|
8687
|
+
const dl = Math.sign(l1 - l0);
|
|
8688
|
+
let kind;
|
|
8689
|
+
if (du === 0 || dl === 0) kind = "oblique";
|
|
8690
|
+
else if (du !== dl) kind = "contrary";
|
|
8691
|
+
else {
|
|
8692
|
+
kind = u1 - l1 === u0 - l0 ? "parallel" : "similar";
|
|
8693
|
+
}
|
|
8694
|
+
motion[kind] += 1;
|
|
8695
|
+
if (kind === "parallel") {
|
|
8696
|
+
const intervalPc = ((u1 - l1) % 12 + 12) % 12;
|
|
8697
|
+
if (intervalPc === 0 || intervalPc === 7) {
|
|
8698
|
+
parallelPerfects.push({ atBeat: b1, intervalPc });
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
}
|
|
8702
|
+
const crossings = [];
|
|
8703
|
+
for (const b of [...upperOnsets, ...onsetSet(lower)]) {
|
|
8704
|
+
const u = pitchAt(upper, b);
|
|
8705
|
+
const l = pitchAt(lower, b);
|
|
8706
|
+
if (u !== null && l !== null && u < l) crossings.push(b);
|
|
8707
|
+
}
|
|
8708
|
+
pairs.push({
|
|
8709
|
+
upperVoice: i,
|
|
8710
|
+
onsetIndependence,
|
|
8711
|
+
motion,
|
|
8712
|
+
parallelPerfects,
|
|
8713
|
+
crossings: [...new Set(crossings)].sort((a, b) => a - b)
|
|
8714
|
+
});
|
|
8715
|
+
}
|
|
8716
|
+
const allOnsets = voices.map(onsetSet);
|
|
8717
|
+
const union = new Set(allOnsets.flat());
|
|
8718
|
+
let sharedByAll = 0;
|
|
8719
|
+
for (const b of union) {
|
|
8720
|
+
if (allOnsets.every((o) => o.length === 0 || o.includes(b))) sharedByAll += 1;
|
|
8721
|
+
}
|
|
8722
|
+
const homorhythmScore = union.size === 0 ? 0 : sharedByAll / union.size;
|
|
8723
|
+
return { pairs, homorhythmScore };
|
|
8724
|
+
}
|
|
8725
|
+
function describeViolations(analysis, rules) {
|
|
8726
|
+
const out = [];
|
|
8727
|
+
for (const pair of analysis.pairs) {
|
|
8728
|
+
const label = `between voice ${pair.upperVoice + 1} and voice ${pair.upperVoice + 2}`;
|
|
8729
|
+
if (rules.forbidParallelPerfects) {
|
|
8730
|
+
for (const p of pair.parallelPerfects) {
|
|
8731
|
+
out.push(`Parallel ${p.intervalPc === 0 ? "octaves" : "fifths"} ${label} at beat ${p.atBeat} \u2014 approach perfect intervals by contrary or oblique motion.`);
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8734
|
+
if (rules.forbidVoiceCrossing && pair.crossings.length > 0) {
|
|
8735
|
+
out.push(`Voice crossing ${label} at beat${pair.crossings.length > 1 ? "s" : ""} ${pair.crossings.join(", ")} \u2014 keep each voice inside its register lane.`);
|
|
8736
|
+
}
|
|
8737
|
+
if (pair.onsetIndependence < rules.minOnsetIndependence) {
|
|
8738
|
+
out.push(`Voices ${pair.upperVoice + 1} and ${pair.upperVoice + 2} attack together too often (independence ${pair.onsetIndependence.toFixed(2)} < ${rules.minOnsetIndependence}) \u2014 stagger entrances and let one voice move while the other holds.`);
|
|
8739
|
+
}
|
|
8740
|
+
}
|
|
8741
|
+
return out;
|
|
8742
|
+
}
|
|
8743
|
+
|
|
8744
|
+
// src/ensemble-core/styles.ts
|
|
8745
|
+
var ENSEMBLE_STYLES = ["counterpoint", "chorale", "interlock"];
|
|
8746
|
+
var STYLE_RULES = {
|
|
8747
|
+
counterpoint: {
|
|
8748
|
+
forbidParallelPerfects: true,
|
|
8749
|
+
forbidVoiceCrossing: true,
|
|
8750
|
+
minOnsetIndependence: 0.35,
|
|
8751
|
+
promptParagraph: "STYLE \u2014 COUNTERPOINT (modern, not strict species): independent singable lines. Favor contrary and oblique motion between neighbors; approach perfect intervals by contrary motion; imitate motifs between voices a bar apart; stagger entrances so voices converse instead of speaking at once."
|
|
8752
|
+
},
|
|
8753
|
+
chorale: {
|
|
8754
|
+
forbidParallelPerfects: true,
|
|
8755
|
+
forbidVoiceCrossing: true,
|
|
8756
|
+
minOnsetIndependence: 0,
|
|
8757
|
+
promptParagraph: "STYLE \u2014 CHORALE: homorhythmic block harmony. Voices move together on the same rhythm with smooth voice-leading \u2014 nearest chord tone, common tones held, no leaps larger than a fifth in inner voices."
|
|
8758
|
+
},
|
|
8759
|
+
interlock: {
|
|
8760
|
+
forbidParallelPerfects: false,
|
|
8761
|
+
forbidVoiceCrossing: false,
|
|
8762
|
+
minOnsetIndependence: 0.6,
|
|
8763
|
+
promptParagraph: "STYLE \u2014 INTERLOCK (minimal / systems music): short repeating cells that mesh like gears. Each voice keeps its own ostinato; onsets rarely coincide with the neighboring voice; parallel motion and doubling are welcome when the composite rhythm stays busy and even."
|
|
8764
|
+
}
|
|
8765
|
+
};
|
|
8766
|
+
|
|
8767
|
+
// src/ensemble-core/ensemble-schema.ts
|
|
8768
|
+
var SUBMIT_ENSEMBLE_TOOL_NAME = "submit_ensemble";
|
|
8769
|
+
function buildSubmitEnsembleParameters(voiceCount) {
|
|
8770
|
+
return {
|
|
8771
|
+
type: "object",
|
|
8772
|
+
properties: {
|
|
8773
|
+
voices: {
|
|
8774
|
+
type: "array",
|
|
8775
|
+
description: `Exactly ${voiceCount} voices, voiceIndex 0 (top) through ${voiceCount - 1} (bottom).`,
|
|
8776
|
+
items: {
|
|
8777
|
+
type: "object",
|
|
8778
|
+
properties: {
|
|
8779
|
+
voiceIndex: { type: "integer", description: "0 = top voice" },
|
|
8780
|
+
notes: {
|
|
8781
|
+
type: "array",
|
|
8782
|
+
items: {
|
|
8783
|
+
type: "object",
|
|
8784
|
+
properties: {
|
|
8785
|
+
pitch: { type: "integer", description: "MIDI note number 0-127" },
|
|
8786
|
+
startBeat: { type: "number", description: "quarter-note beats from clip start" },
|
|
8787
|
+
durationBeats: { type: "number", description: "duration in quarter-note beats" },
|
|
8788
|
+
velocity: { type: "integer", description: "1-127" }
|
|
8789
|
+
},
|
|
8790
|
+
required: ["pitch", "startBeat", "durationBeats", "velocity"]
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8793
|
+
},
|
|
8794
|
+
required: ["voiceIndex", "notes"]
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
},
|
|
8798
|
+
required: ["voices"]
|
|
8799
|
+
};
|
|
8800
|
+
}
|
|
8801
|
+
function parseEnsembleArgs(args, voiceCount) {
|
|
8802
|
+
if (typeof args !== "object" || args === null) return null;
|
|
8803
|
+
const voicesRaw = args.voices;
|
|
8804
|
+
if (!Array.isArray(voicesRaw)) return null;
|
|
8805
|
+
const warnings = [];
|
|
8806
|
+
const voiceNotes = Array.from({ length: voiceCount }, () => []);
|
|
8807
|
+
for (const v of voicesRaw) {
|
|
8808
|
+
if (typeof v !== "object" || v === null) continue;
|
|
8809
|
+
const idxRaw = v.voiceIndex;
|
|
8810
|
+
const idx = typeof idxRaw === "number" ? Math.round(idxRaw) : NaN;
|
|
8811
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= voiceCount) {
|
|
8812
|
+
warnings.push(`ignored voice with out-of-range voiceIndex ${String(idxRaw)}`);
|
|
8813
|
+
continue;
|
|
8814
|
+
}
|
|
8815
|
+
const notesRaw = v.notes;
|
|
8816
|
+
if (!Array.isArray(notesRaw)) continue;
|
|
8817
|
+
const notes = [];
|
|
8818
|
+
let dropped = 0;
|
|
8819
|
+
for (const n of notesRaw) {
|
|
8820
|
+
const note = n;
|
|
8821
|
+
if (typeof note?.pitch === "number" && typeof note?.startBeat === "number" && typeof note?.durationBeats === "number" && typeof note?.velocity === "number" && note.durationBeats > 0 && note.startBeat >= 0) {
|
|
8822
|
+
notes.push({
|
|
8823
|
+
pitch: Math.max(0, Math.min(127, Math.round(note.pitch))),
|
|
8824
|
+
startBeat: note.startBeat,
|
|
8825
|
+
durationBeats: note.durationBeats,
|
|
8826
|
+
velocity: Math.max(1, Math.min(127, Math.round(note.velocity)))
|
|
8827
|
+
});
|
|
8828
|
+
} else {
|
|
8829
|
+
dropped += 1;
|
|
8830
|
+
}
|
|
8831
|
+
}
|
|
8832
|
+
if (dropped > 0) warnings.push(`voice ${idx}: dropped ${dropped} malformed note(s)`);
|
|
8833
|
+
voiceNotes[idx] = notes;
|
|
8834
|
+
}
|
|
8835
|
+
const returned = voicesRaw.length;
|
|
8836
|
+
if (returned !== voiceCount) {
|
|
8837
|
+
warnings.push(`model returned ${returned} voices for a ${voiceCount}-voice ensemble`);
|
|
8838
|
+
}
|
|
8839
|
+
const anyNotes = voiceNotes.some((v) => v.length > 0);
|
|
8840
|
+
return anyNotes ? { voiceNotes, warnings } : null;
|
|
8841
|
+
}
|
|
8842
|
+
|
|
8843
|
+
// src/ensemble-core/ensemble-prompt.ts
|
|
8844
|
+
function voiceContractLine(spec) {
|
|
8845
|
+
const root = spec.rootOnly ? " ROOT PITCH CLASS ONLY (each bar's chord root)." : "";
|
|
8846
|
+
return `- Voice ${spec.voiceIndex + 1} (${spec.label}): MIDI ${spec.registerLow}-${spec.registerHigh}, max ${spec.maxNotesPerBar} notes/bar, rhythm: ${spec.rhythmPalette}. ${spec.harmonicDiscipline}.${root}`;
|
|
8847
|
+
}
|
|
8848
|
+
function buildEnsembleSystemPrompt(specs, style) {
|
|
8849
|
+
const styleParagraph = STYLE_RULES[style].promptParagraph;
|
|
8850
|
+
return `You are an ensemble composer. Compose ${specs.length} voices as ONE piece of music \u2014 not ${specs.length} independent parts.
|
|
8851
|
+
|
|
8852
|
+
Submit your composition by calling the ${SUBMIT_ENSEMBLE_TOOL_NAME} tool with all ${specs.length} voices.
|
|
8853
|
+
|
|
8854
|
+
THE ENSEMBLE RULES (most important):
|
|
8855
|
+
1. The voices are composed TOGETHER. They must relate: imitate and answer each other's motifs, move in contrary or oblique motion against neighbors, and stagger entrances so lines converse.
|
|
8856
|
+
2. Each voice is a SINGLE monophonic line \u2014 within one voice, no two notes overlap in time and no chords. Voices MAY and SHOULD overlap EACH OTHER; that overlap is the music.
|
|
8857
|
+
3. Complexity decreases downward: the top voice is the most active and ornamented, the bottom voice the sparsest anchor. Respect each voice's register window and notes-per-bar cap exactly.
|
|
8858
|
+
4. Follow the chord progression in the musical context exactly. Non-chord tones only as passing or neighbor tones on weak beats, resolving by step.
|
|
8859
|
+
5. Not every voice plays all the time \u2014 rests are structural. Avoid all voices attacking the same beat except at phrase boundaries.
|
|
8860
|
+
6. startBeat/durationBeats are in quarter-note beats from the start of the clip; fill the stated bar length, and make bar 1 land immediately (no long silent intro).
|
|
8861
|
+
|
|
8862
|
+
${styleParagraph}
|
|
8863
|
+
|
|
8864
|
+
PER-VOICE CONTRACTS:
|
|
8865
|
+
${specs.map(voiceContractLine).join("\n")}`;
|
|
8866
|
+
}
|
|
8867
|
+
function buildViolationRetrySuffix(violations) {
|
|
8868
|
+
if (violations.length === 0) return "";
|
|
8869
|
+
return `
|
|
8870
|
+
|
|
8871
|
+
Your previous ensemble had these problems \u2014 fix them while keeping everything that worked:
|
|
8872
|
+
` + violations.map((v) => `- ${v}`).join("\n");
|
|
8873
|
+
}
|
|
8874
|
+
|
|
7822
8875
|
// src/constants/sdk-version.ts
|
|
7823
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
8876
|
+
var PLUGIN_SDK_VERSION = "2.42.0";
|
|
7824
8877
|
|
|
7825
8878
|
// src/utils/format-concurrent-tracks.ts
|
|
7826
8879
|
function formatConcurrentTracks(ctx) {
|
|
7827
8880
|
const tracks = ctx.concurrentTracks;
|
|
7828
8881
|
if (!tracks || tracks.length === 0) return "";
|
|
7829
|
-
const
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
if (track.truncated && typeof track.originalNoteCount === "number") {
|
|
7842
|
-
const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
|
|
7843
|
-
if (dropped > 0) {
|
|
7844
|
-
lines.push(` \u2026 (${dropped} more notes truncated)`);
|
|
7845
|
-
}
|
|
7846
|
-
}
|
|
8882
|
+
const pinned = tracks.filter((t) => t.pinned);
|
|
8883
|
+
const ambient = tracks.filter((t) => !t.pinned);
|
|
8884
|
+
const lines = [];
|
|
8885
|
+
if (pinned.length > 0) {
|
|
8886
|
+
lines.push(
|
|
8887
|
+
"REFERENCE TRACKS (write in counterpoint against these \u2014 interlock with their rhythm, avoid doubling their onsets, favor contrary or oblique motion):"
|
|
8888
|
+
);
|
|
8889
|
+
for (const track of pinned) pushTrackLines(lines, track);
|
|
8890
|
+
}
|
|
8891
|
+
if (ambient.length > 0) {
|
|
8892
|
+
lines.push(`Concurrent tracks in scene (already generated):`);
|
|
8893
|
+
for (const track of ambient) pushTrackLines(lines, track);
|
|
7847
8894
|
}
|
|
7848
8895
|
if (ctx.truncatedTrackCount && ctx.truncatedTrackCount > 0) {
|
|
7849
8896
|
lines.push(
|
|
@@ -7852,6 +8899,25 @@ function formatConcurrentTracks(ctx) {
|
|
|
7852
8899
|
}
|
|
7853
8900
|
return lines.join("\n");
|
|
7854
8901
|
}
|
|
8902
|
+
function pushTrackLines(lines, track) {
|
|
8903
|
+
const nameStr = track.name ? ` name="${escapeQuotes(track.name)}"` : "";
|
|
8904
|
+
const promptStr = track.prompt ? ` prompt="${escapeQuotes(track.prompt)}"` : "";
|
|
8905
|
+
lines.push(` - role=${track.role ?? "unknown"}${nameStr}${promptStr}`);
|
|
8906
|
+
if (track.notesByChord.length === 0) {
|
|
8907
|
+
lines.push(` (no notes)`);
|
|
8908
|
+
} else {
|
|
8909
|
+
for (const segment of track.notesByChord) {
|
|
8910
|
+
if (segment.notes.length === 0) continue;
|
|
8911
|
+
lines.push(` ${formatChordSegment(segment)}`);
|
|
8912
|
+
}
|
|
8913
|
+
}
|
|
8914
|
+
if (track.truncated && typeof track.originalNoteCount === "number") {
|
|
8915
|
+
const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
|
|
8916
|
+
if (dropped > 0) {
|
|
8917
|
+
lines.push(` \u2026 (${dropped} more notes truncated)`);
|
|
8918
|
+
}
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
7855
8921
|
function formatChordSegment(segment) {
|
|
7856
8922
|
const [start, end] = segment.chordRangeQn;
|
|
7857
8923
|
const notesJson = JSON.stringify(segment.notes.map(compactNote2));
|
|
@@ -7976,6 +9042,9 @@ export {
|
|
|
7976
9042
|
DownloadPackButton,
|
|
7977
9043
|
EMPTY_FX_DETAIL_STATE,
|
|
7978
9044
|
EMPTY_FX_STATE,
|
|
9045
|
+
ENSEMBLE_MAX_VOICES,
|
|
9046
|
+
ENSEMBLE_MIN_VOICES,
|
|
9047
|
+
ENSEMBLE_STYLES,
|
|
7979
9048
|
EQUAL_POWER_GAIN,
|
|
7980
9049
|
FX_CATEGORIES,
|
|
7981
9050
|
FX_CHAIN_ORDER,
|
|
@@ -7987,9 +9056,11 @@ export {
|
|
|
7987
9056
|
FxToggleBar,
|
|
7988
9057
|
GUTTER_W,
|
|
7989
9058
|
GeneratorPanelShell,
|
|
9059
|
+
GroupFadeTrackRow,
|
|
7990
9060
|
ImportTrackModal,
|
|
7991
9061
|
TrackDrawer as InstrumentDrawer,
|
|
7992
9062
|
LevelMeter,
|
|
9063
|
+
MIN_NOTE_DURATION_BEATS,
|
|
7993
9064
|
Modal,
|
|
7994
9065
|
OffsetScrubber,
|
|
7995
9066
|
PLUGIN_SDK_VERSION,
|
|
@@ -8001,6 +9072,8 @@ export {
|
|
|
8001
9072
|
RESIZE_HANDLE_PX,
|
|
8002
9073
|
ROW_HEIGHT,
|
|
8003
9074
|
SLIDER_UNITY,
|
|
9075
|
+
STYLE_RULES,
|
|
9076
|
+
SUBMIT_ENSEMBLE_TOOL_NAME,
|
|
8004
9077
|
SamplePackCTACard,
|
|
8005
9078
|
ScrollingWaveform,
|
|
8006
9079
|
SorceryProgressBar,
|
|
@@ -8013,6 +9086,7 @@ export {
|
|
|
8013
9086
|
TransitionDesigner,
|
|
8014
9087
|
VolumeSlider,
|
|
8015
9088
|
WaveformView,
|
|
9089
|
+
analyzeEnsemble,
|
|
8016
9090
|
analyzeWavPeak,
|
|
8017
9091
|
asAudioEffect,
|
|
8018
9092
|
asCrossfadeMeta,
|
|
@@ -8020,8 +9094,11 @@ export {
|
|
|
8020
9094
|
asTransitionDesignerDraft,
|
|
8021
9095
|
buildCrossfadeInpaintPrompt,
|
|
8022
9096
|
buildCrossfadeVolumeCurves,
|
|
9097
|
+
buildEnsembleSystemPrompt,
|
|
8023
9098
|
buildFadeVolumeCurve,
|
|
8024
9099
|
buildRowSlots,
|
|
9100
|
+
buildSubmitEnsembleParameters,
|
|
9101
|
+
buildViolationRetrySuffix,
|
|
8025
9102
|
calculateTimeBasedTarget,
|
|
8026
9103
|
cellToPx,
|
|
8027
9104
|
centerScrollTop,
|
|
@@ -8030,15 +9107,21 @@ export {
|
|
|
8030
9107
|
dbIdsFromKeys,
|
|
8031
9108
|
dbToSlider,
|
|
8032
9109
|
defaultFadeGesture,
|
|
9110
|
+
defaultVoiceSpecs,
|
|
9111
|
+
describeViolations,
|
|
8033
9112
|
drawWaveform,
|
|
9113
|
+
enforceVoice,
|
|
9114
|
+
foldPitchToRegister,
|
|
8034
9115
|
formatConcurrentTracks,
|
|
8035
9116
|
hashString,
|
|
8036
9117
|
moveItem,
|
|
9118
|
+
nearestPitchWithPc,
|
|
8037
9119
|
newTrackState,
|
|
8038
9120
|
normalizeSlots,
|
|
8039
9121
|
padPair,
|
|
8040
9122
|
padSlots,
|
|
8041
9123
|
parseCrossfadePairs,
|
|
9124
|
+
parseEnsembleArgs,
|
|
8042
9125
|
parseFades,
|
|
8043
9126
|
parseLLMNoteResponse,
|
|
8044
9127
|
parseTrackGroups,
|
|
@@ -8055,6 +9138,7 @@ export {
|
|
|
8055
9138
|
sliderToDb,
|
|
8056
9139
|
slotsEqual,
|
|
8057
9140
|
soundIdentity,
|
|
9141
|
+
splitFadeEntries,
|
|
8058
9142
|
synthesizeCuePoints,
|
|
8059
9143
|
tokenizePrompt,
|
|
8060
9144
|
trackDataKey,
|