@signalsandsorcery/plugin-sdk 2.34.1 → 2.35.2
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 +831 -74
- package/dist/index.d.ts +831 -74
- package/dist/index.js +2713 -81
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2702 -82
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4126,9 +4126,276 @@ function TransitionDesigner({
|
|
|
4126
4126
|
] });
|
|
4127
4127
|
}
|
|
4128
4128
|
|
|
4129
|
-
// src/components/
|
|
4130
|
-
import {
|
|
4129
|
+
// src/components/PanelMasterStrip.tsx
|
|
4130
|
+
import { useMemo as useMemo6, useState as useState12 } from "react";
|
|
4131
4131
|
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
4132
|
+
function PanelMasterStrip({
|
|
4133
|
+
bus,
|
|
4134
|
+
availableFx = [],
|
|
4135
|
+
fxLoading = false,
|
|
4136
|
+
soloedOut = false,
|
|
4137
|
+
disabled = false,
|
|
4138
|
+
fxPickerOpen,
|
|
4139
|
+
onToggleFxPicker,
|
|
4140
|
+
onRefreshFx,
|
|
4141
|
+
onVolumeChange,
|
|
4142
|
+
onMuteToggle,
|
|
4143
|
+
onSoloToggle,
|
|
4144
|
+
onAddFx,
|
|
4145
|
+
onRemoveFx,
|
|
4146
|
+
onToggleFxEnabled,
|
|
4147
|
+
onShowFxEditor
|
|
4148
|
+
}) {
|
|
4149
|
+
const [search, setSearch] = useState12("");
|
|
4150
|
+
const filtered = useMemo6(() => {
|
|
4151
|
+
const q = search.trim().toLowerCase();
|
|
4152
|
+
if (!q) return availableFx;
|
|
4153
|
+
return availableFx.filter(
|
|
4154
|
+
(fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
|
|
4155
|
+
);
|
|
4156
|
+
}, [availableFx, search]);
|
|
4157
|
+
return /* @__PURE__ */ jsxs14(
|
|
4158
|
+
"div",
|
|
4159
|
+
{
|
|
4160
|
+
"data-testid": "panel-master-strip",
|
|
4161
|
+
className: `flex flex-col gap-1 px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt/50 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
|
|
4162
|
+
children: [
|
|
4163
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
4164
|
+
/* @__PURE__ */ jsx18(
|
|
4165
|
+
"span",
|
|
4166
|
+
{
|
|
4167
|
+
className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
|
|
4168
|
+
title: "Panel mix bus \u2014 volume, mute/solo and FX applied to this panel's summed output",
|
|
4169
|
+
children: "BUS"
|
|
4170
|
+
}
|
|
4171
|
+
),
|
|
4172
|
+
/* @__PURE__ */ jsx18("div", { className: "w-24", children: /* @__PURE__ */ jsx18(
|
|
4173
|
+
VolumeSlider,
|
|
4174
|
+
{
|
|
4175
|
+
value: dbToSlider(bus.volume),
|
|
4176
|
+
onChange: (sliderValue) => onVolumeChange(sliderToDb(sliderValue)),
|
|
4177
|
+
disabled
|
|
4178
|
+
}
|
|
4179
|
+
) }),
|
|
4180
|
+
/* @__PURE__ */ jsx18(
|
|
4181
|
+
"button",
|
|
4182
|
+
{
|
|
4183
|
+
"data-testid": "bus-mute-button",
|
|
4184
|
+
onClick: onMuteToggle,
|
|
4185
|
+
disabled,
|
|
4186
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${bus.muted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"} disabled:opacity-50`,
|
|
4187
|
+
title: bus.muted ? "Unmute panel bus" : "Mute panel bus (silences the whole panel)",
|
|
4188
|
+
children: "M"
|
|
4189
|
+
}
|
|
4190
|
+
),
|
|
4191
|
+
/* @__PURE__ */ jsx18(
|
|
4192
|
+
"button",
|
|
4193
|
+
{
|
|
4194
|
+
"data-testid": "bus-solo-button",
|
|
4195
|
+
onClick: onSoloToggle,
|
|
4196
|
+
disabled,
|
|
4197
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${bus.soloed ? "bg-amber-500 text-black" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"} disabled:opacity-50`,
|
|
4198
|
+
title: bus.soloed ? "Unsolo panel bus" : "Solo this panel (silences other panels/tracks in scope)",
|
|
4199
|
+
children: "S"
|
|
4200
|
+
}
|
|
4201
|
+
),
|
|
4202
|
+
/* @__PURE__ */ jsx18("div", { className: "flex items-center gap-1 flex-1 min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs14(
|
|
4203
|
+
"span",
|
|
4204
|
+
{
|
|
4205
|
+
"data-testid": `bus-fx-chip-${fx.index}`,
|
|
4206
|
+
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"}`,
|
|
4207
|
+
title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
|
|
4208
|
+
children: [
|
|
4209
|
+
/* @__PURE__ */ jsx18(
|
|
4210
|
+
"button",
|
|
4211
|
+
{
|
|
4212
|
+
"data-testid": `bus-fx-toggle-${fx.index}`,
|
|
4213
|
+
onClick: () => onToggleFxEnabled(fx.index, !fx.enabled),
|
|
4214
|
+
disabled,
|
|
4215
|
+
className: "hover:opacity-70 disabled:opacity-50",
|
|
4216
|
+
title: fx.enabled ? `Bypass ${fx.name}` : `Enable ${fx.name}`,
|
|
4217
|
+
children: fx.enabled ? "\u25CF" : "\u25CB"
|
|
4218
|
+
}
|
|
4219
|
+
),
|
|
4220
|
+
onShowFxEditor ? /* @__PURE__ */ jsx18(
|
|
4221
|
+
"button",
|
|
4222
|
+
{
|
|
4223
|
+
"data-testid": `bus-fx-edit-${fx.index}`,
|
|
4224
|
+
onClick: () => onShowFxEditor(fx.index),
|
|
4225
|
+
disabled,
|
|
4226
|
+
className: "max-w-[80px] truncate hover:underline disabled:opacity-50",
|
|
4227
|
+
title: `Open ${fx.name} editor`,
|
|
4228
|
+
children: fx.name
|
|
4229
|
+
}
|
|
4230
|
+
) : /* @__PURE__ */ jsx18("span", { className: "max-w-[80px] truncate", children: fx.name }),
|
|
4231
|
+
/* @__PURE__ */ jsx18(
|
|
4232
|
+
"button",
|
|
4233
|
+
{
|
|
4234
|
+
"data-testid": `bus-fx-remove-${fx.index}`,
|
|
4235
|
+
onClick: () => onRemoveFx(fx.index),
|
|
4236
|
+
disabled,
|
|
4237
|
+
className: "text-sas-muted/60 hover:text-sas-danger disabled:opacity-50",
|
|
4238
|
+
title: `Remove ${fx.name} from the bus`,
|
|
4239
|
+
children: "\u2715"
|
|
4240
|
+
}
|
|
4241
|
+
)
|
|
4242
|
+
]
|
|
4243
|
+
},
|
|
4244
|
+
`${fx.index}:${fx.pluginId}`
|
|
4245
|
+
)) }),
|
|
4246
|
+
/* @__PURE__ */ jsx18(
|
|
4247
|
+
"button",
|
|
4248
|
+
{
|
|
4249
|
+
"data-testid": "bus-fx-add-button",
|
|
4250
|
+
onClick: () => onToggleFxPicker(!fxPickerOpen),
|
|
4251
|
+
disabled,
|
|
4252
|
+
className: `px-1.5 py-0.5 rounded-sm border text-xs whitespace-nowrap transition-colors ${fxPickerOpen ? "border-sas-accent text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"} disabled:opacity-50`,
|
|
4253
|
+
title: "Add an FX plugin to the panel bus",
|
|
4254
|
+
children: "FX +"
|
|
4255
|
+
}
|
|
4256
|
+
)
|
|
4257
|
+
] }),
|
|
4258
|
+
fxPickerOpen && /* @__PURE__ */ jsxs14("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
|
|
4259
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
4260
|
+
/* @__PURE__ */ jsx18(
|
|
4261
|
+
"input",
|
|
4262
|
+
{
|
|
4263
|
+
type: "text",
|
|
4264
|
+
value: search,
|
|
4265
|
+
onChange: (e) => setSearch(e.target.value),
|
|
4266
|
+
placeholder: "Search FX...",
|
|
4267
|
+
className: "sas-input flex-1 px-2 py-1 text-xs"
|
|
4268
|
+
}
|
|
4269
|
+
),
|
|
4270
|
+
onRefreshFx && /* @__PURE__ */ jsx18(
|
|
4271
|
+
"button",
|
|
4272
|
+
{
|
|
4273
|
+
onClick: () => onRefreshFx(),
|
|
4274
|
+
disabled: fxLoading,
|
|
4275
|
+
className: "px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50",
|
|
4276
|
+
title: "Re-scan plugins",
|
|
4277
|
+
children: fxLoading ? "..." : "Refresh"
|
|
4278
|
+
}
|
|
4279
|
+
)
|
|
4280
|
+
] }),
|
|
4281
|
+
fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs14("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
|
|
4282
|
+
filtered.map((fx) => /* @__PURE__ */ jsxs14(
|
|
4283
|
+
"button",
|
|
4284
|
+
{
|
|
4285
|
+
"data-testid": `bus-fx-pick-${fx.pluginId}`,
|
|
4286
|
+
onClick: () => onAddFx(fx.pluginId),
|
|
4287
|
+
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",
|
|
4288
|
+
title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
|
|
4289
|
+
children: [
|
|
4290
|
+
/* @__PURE__ */ jsx18("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
|
|
4291
|
+
/* @__PURE__ */ jsx18("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
|
|
4292
|
+
]
|
|
4293
|
+
},
|
|
4294
|
+
fx.pluginId
|
|
4295
|
+
)),
|
|
4296
|
+
filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
|
|
4297
|
+
] })
|
|
4298
|
+
] })
|
|
4299
|
+
]
|
|
4300
|
+
}
|
|
4301
|
+
);
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
// src/hooks/usePanelBus.ts
|
|
4305
|
+
import { useCallback as useCallback9, useEffect as useEffect10, useRef as useRef11, useState as useState13 } from "react";
|
|
4306
|
+
function usePanelBus(host, activeSceneId) {
|
|
4307
|
+
const supported = typeof host.getPanelBusState === "function";
|
|
4308
|
+
const [bus, setBus] = useState13(null);
|
|
4309
|
+
const [availableFx, setAvailableFx] = useState13([]);
|
|
4310
|
+
const [fxLoading, setFxLoading] = useState13(false);
|
|
4311
|
+
const [fxPickerOpen, setFxPickerOpen] = useState13(false);
|
|
4312
|
+
const fxLoadedRef = useRef11(false);
|
|
4313
|
+
const loadSeqRef = useRef11(0);
|
|
4314
|
+
const reload = useCallback9(async () => {
|
|
4315
|
+
if (!supported || !activeSceneId || !host.getPanelBusState) {
|
|
4316
|
+
setBus(null);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
const seq = ++loadSeqRef.current;
|
|
4320
|
+
try {
|
|
4321
|
+
const state = await host.getPanelBusState(activeSceneId);
|
|
4322
|
+
if (loadSeqRef.current === seq) setBus(state);
|
|
4323
|
+
} catch {
|
|
4324
|
+
}
|
|
4325
|
+
}, [host, activeSceneId, supported]);
|
|
4326
|
+
useEffect10(() => {
|
|
4327
|
+
setBus(null);
|
|
4328
|
+
setFxPickerOpen(false);
|
|
4329
|
+
void reload();
|
|
4330
|
+
}, [reload]);
|
|
4331
|
+
const loadFxList = useCallback9(
|
|
4332
|
+
async (force) => {
|
|
4333
|
+
if (!supported || !host.getAvailableFx) return;
|
|
4334
|
+
if (fxLoadedRef.current && !force) return;
|
|
4335
|
+
setFxLoading(true);
|
|
4336
|
+
try {
|
|
4337
|
+
const list = await host.getAvailableFx();
|
|
4338
|
+
setAvailableFx(list);
|
|
4339
|
+
fxLoadedRef.current = true;
|
|
4340
|
+
} catch {
|
|
4341
|
+
} finally {
|
|
4342
|
+
setFxLoading(false);
|
|
4343
|
+
}
|
|
4344
|
+
},
|
|
4345
|
+
[host, supported]
|
|
4346
|
+
);
|
|
4347
|
+
const openPicker = useCallback9(
|
|
4348
|
+
(open) => {
|
|
4349
|
+
setFxPickerOpen(open);
|
|
4350
|
+
if (open) void loadFxList(false);
|
|
4351
|
+
},
|
|
4352
|
+
[loadFxList]
|
|
4353
|
+
);
|
|
4354
|
+
const mutate = useCallback9(
|
|
4355
|
+
(fn) => {
|
|
4356
|
+
if (!fn || !activeSceneId) return;
|
|
4357
|
+
void (async () => {
|
|
4358
|
+
try {
|
|
4359
|
+
await fn();
|
|
4360
|
+
} catch {
|
|
4361
|
+
}
|
|
4362
|
+
await reload();
|
|
4363
|
+
})();
|
|
4364
|
+
},
|
|
4365
|
+
[activeSceneId, reload]
|
|
4366
|
+
);
|
|
4367
|
+
return {
|
|
4368
|
+
supported,
|
|
4369
|
+
bus,
|
|
4370
|
+
availableFx,
|
|
4371
|
+
fxLoading,
|
|
4372
|
+
fxPickerOpen,
|
|
4373
|
+
setFxPickerOpen: openPicker,
|
|
4374
|
+
refreshFx: () => void loadFxList(true),
|
|
4375
|
+
reload,
|
|
4376
|
+
onVolumeChange: (volumeDb) => mutate(host.setPanelBusVolume && (() => host.setPanelBusVolume(activeSceneId, volumeDb))),
|
|
4377
|
+
onMuteToggle: () => mutate(
|
|
4378
|
+
host.setPanelBusMute && (() => host.setPanelBusMute(activeSceneId, !(bus?.muted ?? false)))
|
|
4379
|
+
),
|
|
4380
|
+
onSoloToggle: () => mutate(
|
|
4381
|
+
host.setPanelBusSolo && (() => host.setPanelBusSolo(activeSceneId, !(bus?.soloed ?? false)))
|
|
4382
|
+
),
|
|
4383
|
+
onAddFx: (pluginId) => mutate(host.loadPanelBusFx && (async () => {
|
|
4384
|
+
await host.loadPanelBusFx(activeSceneId, pluginId);
|
|
4385
|
+
})),
|
|
4386
|
+
onRemoveFx: (fxIndex) => mutate(host.removePanelBusFx && (() => host.removePanelBusFx(activeSceneId, fxIndex))),
|
|
4387
|
+
onToggleFxEnabled: (fxIndex, enabled) => mutate(
|
|
4388
|
+
host.setPanelBusFxEnabled && (() => host.setPanelBusFxEnabled(activeSceneId, fxIndex, enabled))
|
|
4389
|
+
),
|
|
4390
|
+
onShowFxEditor: (fxIndex) => mutate(
|
|
4391
|
+
host.showPanelBusFxEditor && (() => host.showPanelBusFxEditor(activeSceneId, fxIndex))
|
|
4392
|
+
)
|
|
4393
|
+
};
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
// src/components/DownloadPackButton.tsx
|
|
4397
|
+
import { useCallback as useCallback10, useEffect as useEffect11, useState as useState14 } from "react";
|
|
4398
|
+
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
4132
4399
|
function formatSize(bytes) {
|
|
4133
4400
|
if (!bytes || bytes <= 0) return "";
|
|
4134
4401
|
const gb = bytes / 1024 ** 3;
|
|
@@ -4144,10 +4411,10 @@ var DownloadPackButton = ({
|
|
|
4144
4411
|
variant = "compact",
|
|
4145
4412
|
onDownloadComplete
|
|
4146
4413
|
}) => {
|
|
4147
|
-
const [status, setStatus] =
|
|
4148
|
-
const [progress, setProgress] =
|
|
4149
|
-
const [errorMessage, setErrorMessage] =
|
|
4150
|
-
|
|
4414
|
+
const [status, setStatus] = useState14("idle");
|
|
4415
|
+
const [progress, setProgress] = useState14(0);
|
|
4416
|
+
const [errorMessage, setErrorMessage] = useState14(null);
|
|
4417
|
+
useEffect11(() => {
|
|
4151
4418
|
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
4152
4419
|
setStatus(p.status);
|
|
4153
4420
|
setProgress(p.progress);
|
|
@@ -4162,7 +4429,7 @@ var DownloadPackButton = ({
|
|
|
4162
4429
|
});
|
|
4163
4430
|
return unsub;
|
|
4164
4431
|
}, [host, packId, onDownloadComplete]);
|
|
4165
|
-
const handleClick =
|
|
4432
|
+
const handleClick = useCallback10(async () => {
|
|
4166
4433
|
if (status !== "idle" && status !== "error") return;
|
|
4167
4434
|
try {
|
|
4168
4435
|
setStatus("downloading");
|
|
@@ -4216,8 +4483,8 @@ var DownloadPackButton = ({
|
|
|
4216
4483
|
} else {
|
|
4217
4484
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
4218
4485
|
}
|
|
4219
|
-
return /* @__PURE__ */
|
|
4220
|
-
/* @__PURE__ */
|
|
4486
|
+
return /* @__PURE__ */ jsxs15("div", { children: [
|
|
4487
|
+
/* @__PURE__ */ jsx19(
|
|
4221
4488
|
"button",
|
|
4222
4489
|
{
|
|
4223
4490
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -4228,12 +4495,12 @@ var DownloadPackButton = ({
|
|
|
4228
4495
|
children: buttonLabel
|
|
4229
4496
|
}
|
|
4230
4497
|
),
|
|
4231
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */
|
|
4498
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
4232
4499
|
] });
|
|
4233
4500
|
};
|
|
4234
4501
|
|
|
4235
4502
|
// src/components/SamplePackCTACard.tsx
|
|
4236
|
-
import { jsx as
|
|
4503
|
+
import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
4237
4504
|
var SamplePackCTACard = ({
|
|
4238
4505
|
host,
|
|
4239
4506
|
pack,
|
|
@@ -4241,7 +4508,7 @@ var SamplePackCTACard = ({
|
|
|
4241
4508
|
onDownloadComplete
|
|
4242
4509
|
}) => {
|
|
4243
4510
|
if (status === "checking") {
|
|
4244
|
-
return /* @__PURE__ */
|
|
4511
|
+
return /* @__PURE__ */ jsx20(
|
|
4245
4512
|
"div",
|
|
4246
4513
|
{
|
|
4247
4514
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -4252,16 +4519,16 @@ var SamplePackCTACard = ({
|
|
|
4252
4519
|
}
|
|
4253
4520
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
4254
4521
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
4255
|
-
return /* @__PURE__ */
|
|
4522
|
+
return /* @__PURE__ */ jsxs16(
|
|
4256
4523
|
"div",
|
|
4257
4524
|
{
|
|
4258
4525
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
4259
4526
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
4260
4527
|
children: [
|
|
4261
|
-
/* @__PURE__ */
|
|
4262
|
-
/* @__PURE__ */
|
|
4263
|
-
/* @__PURE__ */
|
|
4264
|
-
/* @__PURE__ */
|
|
4528
|
+
/* @__PURE__ */ jsx20("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
4529
|
+
/* @__PURE__ */ jsx20("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
4530
|
+
/* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
4531
|
+
/* @__PURE__ */ jsx20(
|
|
4265
4532
|
DownloadPackButton,
|
|
4266
4533
|
{
|
|
4267
4534
|
host,
|
|
@@ -4278,7 +4545,7 @@ var SamplePackCTACard = ({
|
|
|
4278
4545
|
};
|
|
4279
4546
|
|
|
4280
4547
|
// src/components/WaveformView.tsx
|
|
4281
|
-
import { useEffect as
|
|
4548
|
+
import { useEffect as useEffect12, useRef as useRef12, useState as useState15 } from "react";
|
|
4282
4549
|
|
|
4283
4550
|
// src/components/waveform.ts
|
|
4284
4551
|
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
@@ -4341,7 +4608,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
4341
4608
|
}
|
|
4342
4609
|
|
|
4343
4610
|
// src/components/WaveformView.tsx
|
|
4344
|
-
import { jsx as
|
|
4611
|
+
import { jsx as jsx21 } from "react/jsx-runtime";
|
|
4345
4612
|
var WaveformView = ({
|
|
4346
4613
|
host,
|
|
4347
4614
|
filePath,
|
|
@@ -4350,9 +4617,9 @@ var WaveformView = ({
|
|
|
4350
4617
|
fillStyle,
|
|
4351
4618
|
targetSamples
|
|
4352
4619
|
}) => {
|
|
4353
|
-
const canvasRef =
|
|
4354
|
-
const [peaks, setPeaks] =
|
|
4355
|
-
|
|
4620
|
+
const canvasRef = useRef12(null);
|
|
4621
|
+
const [peaks, setPeaks] = useState15(null);
|
|
4622
|
+
useEffect12(() => {
|
|
4356
4623
|
let cancelled = false;
|
|
4357
4624
|
let audioContext = null;
|
|
4358
4625
|
(async () => {
|
|
@@ -4378,7 +4645,7 @@ var WaveformView = ({
|
|
|
4378
4645
|
cancelled = true;
|
|
4379
4646
|
};
|
|
4380
4647
|
}, [host, filePath, bins, targetSamples]);
|
|
4381
|
-
|
|
4648
|
+
useEffect12(() => {
|
|
4382
4649
|
if (!peaks) return;
|
|
4383
4650
|
const canvas = canvasRef.current;
|
|
4384
4651
|
if (!canvas) return;
|
|
@@ -4389,7 +4656,7 @@ var WaveformView = ({
|
|
|
4389
4656
|
observer.observe(canvas);
|
|
4390
4657
|
return () => observer.disconnect();
|
|
4391
4658
|
}, [peaks, fillStyle]);
|
|
4392
|
-
return /* @__PURE__ */
|
|
4659
|
+
return /* @__PURE__ */ jsx21(
|
|
4393
4660
|
"canvas",
|
|
4394
4661
|
{
|
|
4395
4662
|
ref: canvasRef,
|
|
@@ -4400,8 +4667,8 @@ var WaveformView = ({
|
|
|
4400
4667
|
};
|
|
4401
4668
|
|
|
4402
4669
|
// src/components/ScrollingWaveform.tsx
|
|
4403
|
-
import { useEffect as
|
|
4404
|
-
import { jsx as
|
|
4670
|
+
import { useEffect as useEffect13, useRef as useRef13 } from "react";
|
|
4671
|
+
import { jsx as jsx22 } from "react/jsx-runtime";
|
|
4405
4672
|
var ScrollingWaveform = ({
|
|
4406
4673
|
getPeakDb,
|
|
4407
4674
|
active,
|
|
@@ -4409,11 +4676,11 @@ var ScrollingWaveform = ({
|
|
|
4409
4676
|
className,
|
|
4410
4677
|
fillStyle
|
|
4411
4678
|
}) => {
|
|
4412
|
-
const canvasRef =
|
|
4413
|
-
const ringRef =
|
|
4414
|
-
const writeIdxRef =
|
|
4415
|
-
const rafRef =
|
|
4416
|
-
|
|
4679
|
+
const canvasRef = useRef13(null);
|
|
4680
|
+
const ringRef = useRef13(new Float32Array(columns));
|
|
4681
|
+
const writeIdxRef = useRef13(0);
|
|
4682
|
+
const rafRef = useRef13(null);
|
|
4683
|
+
useEffect13(() => {
|
|
4417
4684
|
if (ringRef.current.length !== columns) {
|
|
4418
4685
|
const next = new Float32Array(columns);
|
|
4419
4686
|
const prev = ringRef.current;
|
|
@@ -4425,7 +4692,7 @@ var ScrollingWaveform = ({
|
|
|
4425
4692
|
writeIdxRef.current = writeIdxRef.current % columns;
|
|
4426
4693
|
}
|
|
4427
4694
|
}, [columns]);
|
|
4428
|
-
|
|
4695
|
+
useEffect13(() => {
|
|
4429
4696
|
if (!active) {
|
|
4430
4697
|
if (rafRef.current !== null) {
|
|
4431
4698
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -4477,7 +4744,7 @@ var ScrollingWaveform = ({
|
|
|
4477
4744
|
}
|
|
4478
4745
|
};
|
|
4479
4746
|
}, [active, getPeakDb, fillStyle]);
|
|
4480
|
-
return /* @__PURE__ */
|
|
4747
|
+
return /* @__PURE__ */ jsx22(
|
|
4481
4748
|
"canvas",
|
|
4482
4749
|
{
|
|
4483
4750
|
ref: canvasRef,
|
|
@@ -4488,8 +4755,8 @@ var ScrollingWaveform = ({
|
|
|
4488
4755
|
};
|
|
4489
4756
|
|
|
4490
4757
|
// src/components/OffsetScrubber.tsx
|
|
4491
|
-
import { useCallback as
|
|
4492
|
-
import { jsx as
|
|
4758
|
+
import { useCallback as useCallback11, useEffect as useEffect14, useMemo as useMemo7, useRef as useRef14, useState as useState16 } from "react";
|
|
4759
|
+
import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
4493
4760
|
var SLIDER_HEIGHT_PX = 28;
|
|
4494
4761
|
var TICK_HEIGHT_PX = 14;
|
|
4495
4762
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -4502,40 +4769,40 @@ function OffsetScrubber({
|
|
|
4502
4769
|
onChange,
|
|
4503
4770
|
disabled = false
|
|
4504
4771
|
}) {
|
|
4505
|
-
const trackRef =
|
|
4506
|
-
const [draftOffset, setDraftOffset] =
|
|
4507
|
-
const [isDragging, setIsDragging] =
|
|
4508
|
-
|
|
4772
|
+
const trackRef = useRef14(null);
|
|
4773
|
+
const [draftOffset, setDraftOffset] = useState16(offsetSamples);
|
|
4774
|
+
const [isDragging, setIsDragging] = useState16(false);
|
|
4775
|
+
useEffect14(() => {
|
|
4509
4776
|
if (!isDragging) setDraftOffset(offsetSamples);
|
|
4510
4777
|
}, [offsetSamples, isDragging]);
|
|
4511
4778
|
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
4512
4779
|
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
4513
|
-
const beatsForRange =
|
|
4780
|
+
const beatsForRange = useMemo7(() => {
|
|
4514
4781
|
return Math.round(60 / projectBpm * sampleRate);
|
|
4515
4782
|
}, [projectBpm, sampleRate]);
|
|
4516
4783
|
const rangeSamples = beatsForRange * meter;
|
|
4517
|
-
const sampleToFraction =
|
|
4784
|
+
const sampleToFraction = useCallback11(
|
|
4518
4785
|
(sample) => {
|
|
4519
4786
|
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
4520
4787
|
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
4521
4788
|
},
|
|
4522
4789
|
[rangeSamples]
|
|
4523
4790
|
);
|
|
4524
|
-
const fractionToSample =
|
|
4791
|
+
const fractionToSample = useCallback11(
|
|
4525
4792
|
(fraction) => {
|
|
4526
4793
|
const clamped = Math.max(0, Math.min(1, fraction));
|
|
4527
4794
|
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
4528
4795
|
},
|
|
4529
4796
|
[rangeSamples]
|
|
4530
4797
|
);
|
|
4531
|
-
const snapTargets =
|
|
4798
|
+
const snapTargets = useMemo7(() => {
|
|
4532
4799
|
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
4533
4800
|
const downbeat = cuePoints.beats[0];
|
|
4534
4801
|
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
4535
4802
|
const negatives = positives.slice(1).map((p) => -p);
|
|
4536
4803
|
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
4537
4804
|
}, [cuePoints]);
|
|
4538
|
-
const snapToBeat =
|
|
4805
|
+
const snapToBeat = useCallback11(
|
|
4539
4806
|
(sample) => {
|
|
4540
4807
|
if (snapTargets.length === 0) return sample;
|
|
4541
4808
|
let best = snapTargets[0];
|
|
@@ -4551,7 +4818,7 @@ function OffsetScrubber({
|
|
|
4551
4818
|
},
|
|
4552
4819
|
[snapTargets]
|
|
4553
4820
|
);
|
|
4554
|
-
const handlePointerDown =
|
|
4821
|
+
const handlePointerDown = useCallback11(
|
|
4555
4822
|
(e) => {
|
|
4556
4823
|
if (disabled || !cuePoints) return;
|
|
4557
4824
|
e.preventDefault();
|
|
@@ -4585,7 +4852,7 @@ function OffsetScrubber({
|
|
|
4585
4852
|
},
|
|
4586
4853
|
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
4587
4854
|
);
|
|
4588
|
-
const handleResetToZero =
|
|
4855
|
+
const handleResetToZero = useCallback11(() => {
|
|
4589
4856
|
if (disabled) return;
|
|
4590
4857
|
setDraftOffset(0);
|
|
4591
4858
|
onChange(0);
|
|
@@ -4593,7 +4860,7 @@ function OffsetScrubber({
|
|
|
4593
4860
|
const thumbFraction = sampleToFraction(draftOffset);
|
|
4594
4861
|
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
4595
4862
|
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
4596
|
-
const ticks =
|
|
4863
|
+
const ticks = useMemo7(() => {
|
|
4597
4864
|
if (!cuePoints) return [];
|
|
4598
4865
|
const downbeat = cuePoints.beats[0] ?? 0;
|
|
4599
4866
|
return cuePoints.beats.map((b, i) => {
|
|
@@ -4604,9 +4871,9 @@ function OffsetScrubber({
|
|
|
4604
4871
|
});
|
|
4605
4872
|
}, [cuePoints, sampleToFraction]);
|
|
4606
4873
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
4607
|
-
return /* @__PURE__ */
|
|
4608
|
-
/* @__PURE__ */
|
|
4609
|
-
/* @__PURE__ */
|
|
4874
|
+
return /* @__PURE__ */ jsxs17("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
4875
|
+
/* @__PURE__ */ jsx23("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
4876
|
+
/* @__PURE__ */ jsxs17(
|
|
4610
4877
|
"div",
|
|
4611
4878
|
{
|
|
4612
4879
|
ref: trackRef,
|
|
@@ -4622,7 +4889,7 @@ function OffsetScrubber({
|
|
|
4622
4889
|
"aria-valuenow": draftOffset,
|
|
4623
4890
|
"aria-disabled": isDisabled,
|
|
4624
4891
|
children: [
|
|
4625
|
-
/* @__PURE__ */
|
|
4892
|
+
/* @__PURE__ */ jsx23(
|
|
4626
4893
|
"div",
|
|
4627
4894
|
{
|
|
4628
4895
|
"aria-hidden": "true",
|
|
@@ -4630,7 +4897,7 @@ function OffsetScrubber({
|
|
|
4630
4897
|
style: { left: "50%" }
|
|
4631
4898
|
}
|
|
4632
4899
|
),
|
|
4633
|
-
ticks.map((t) => /* @__PURE__ */
|
|
4900
|
+
ticks.map((t) => /* @__PURE__ */ jsx23(
|
|
4634
4901
|
"div",
|
|
4635
4902
|
{
|
|
4636
4903
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -4645,7 +4912,7 @@ function OffsetScrubber({
|
|
|
4645
4912
|
},
|
|
4646
4913
|
t.i
|
|
4647
4914
|
)),
|
|
4648
|
-
/* @__PURE__ */
|
|
4915
|
+
/* @__PURE__ */ jsx23(
|
|
4649
4916
|
"div",
|
|
4650
4917
|
{
|
|
4651
4918
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -4662,7 +4929,7 @@ function OffsetScrubber({
|
|
|
4662
4929
|
]
|
|
4663
4930
|
}
|
|
4664
4931
|
),
|
|
4665
|
-
/* @__PURE__ */
|
|
4932
|
+
/* @__PURE__ */ jsx23(
|
|
4666
4933
|
"span",
|
|
4667
4934
|
{
|
|
4668
4935
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -4670,7 +4937,7 @@ function OffsetScrubber({
|
|
|
4670
4937
|
children: formatOffset(draftOffset, sampleRate)
|
|
4671
4938
|
}
|
|
4672
4939
|
),
|
|
4673
|
-
/* @__PURE__ */
|
|
4940
|
+
/* @__PURE__ */ jsx23(
|
|
4674
4941
|
"button",
|
|
4675
4942
|
{
|
|
4676
4943
|
type: "button",
|
|
@@ -4682,7 +4949,7 @@ function OffsetScrubber({
|
|
|
4682
4949
|
children: "\u2316"
|
|
4683
4950
|
}
|
|
4684
4951
|
),
|
|
4685
|
-
bpmMismatch && /* @__PURE__ */
|
|
4952
|
+
bpmMismatch && /* @__PURE__ */ jsx23(
|
|
4686
4953
|
"span",
|
|
4687
4954
|
{
|
|
4688
4955
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -4753,14 +5020,17 @@ function synthesizeCuePoints({
|
|
|
4753
5020
|
};
|
|
4754
5021
|
}
|
|
4755
5022
|
|
|
5023
|
+
// src/panel-core/useGeneratorPanelCore.tsx
|
|
5024
|
+
import { useState as useState21, useEffect as useEffect17, useCallback as useCallback15, useRef as useRef18, useMemo as useMemo9 } from "react";
|
|
5025
|
+
|
|
4756
5026
|
// src/hooks/useSceneState.ts
|
|
4757
|
-
import { useState as
|
|
5027
|
+
import { useState as useState17, useCallback as useCallback12, useRef as useRef15 } from "react";
|
|
4758
5028
|
function useSceneState(activeSceneId, initialValue) {
|
|
4759
|
-
const [stateMap, setStateMap] =
|
|
4760
|
-
const activeSceneIdRef =
|
|
5029
|
+
const [stateMap, setStateMap] = useState17(() => /* @__PURE__ */ new Map());
|
|
5030
|
+
const activeSceneIdRef = useRef15(activeSceneId);
|
|
4761
5031
|
activeSceneIdRef.current = activeSceneId;
|
|
4762
5032
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
4763
|
-
const setForCurrentScene =
|
|
5033
|
+
const setForCurrentScene = useCallback12((value) => {
|
|
4764
5034
|
const sid = activeSceneIdRef.current;
|
|
4765
5035
|
if (sid === null) return;
|
|
4766
5036
|
setStateMap((prev) => {
|
|
@@ -4771,7 +5041,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
4771
5041
|
return newMap;
|
|
4772
5042
|
});
|
|
4773
5043
|
}, [initialValue]);
|
|
4774
|
-
const setForScene =
|
|
5044
|
+
const setForScene = useCallback12((sceneId, value) => {
|
|
4775
5045
|
setStateMap((prev) => {
|
|
4776
5046
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
4777
5047
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -4784,10 +5054,10 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
4784
5054
|
}
|
|
4785
5055
|
|
|
4786
5056
|
// src/hooks/useAnySolo.ts
|
|
4787
|
-
import { useEffect as
|
|
5057
|
+
import { useEffect as useEffect15, useState as useState18 } from "react";
|
|
4788
5058
|
function useAnySolo(host) {
|
|
4789
|
-
const [anySolo, setAnySolo] =
|
|
4790
|
-
|
|
5059
|
+
const [anySolo, setAnySolo] = useState18(false);
|
|
5060
|
+
useEffect15(() => {
|
|
4791
5061
|
let active = true;
|
|
4792
5062
|
const refresh = () => {
|
|
4793
5063
|
host.isAnySoloActive().then((v) => {
|
|
@@ -4806,7 +5076,7 @@ function useAnySolo(host) {
|
|
|
4806
5076
|
}
|
|
4807
5077
|
|
|
4808
5078
|
// src/hooks/useSoundHistory.ts
|
|
4809
|
-
import { useCallback as
|
|
5079
|
+
import { useCallback as useCallback13, useMemo as useMemo8, useRef as useRef16, useState as useState19 } from "react";
|
|
4810
5080
|
var EMPTY = { entries: [], cursor: -1 };
|
|
4811
5081
|
function sameDescriptor(a, b) {
|
|
4812
5082
|
if (a === b) return true;
|
|
@@ -4818,14 +5088,14 @@ function sameDescriptor(a, b) {
|
|
|
4818
5088
|
}
|
|
4819
5089
|
function useSoundHistory(applySound, opts = {}) {
|
|
4820
5090
|
const max = Math.max(2, opts.max ?? 24);
|
|
4821
|
-
const applyRef =
|
|
5091
|
+
const applyRef = useRef16(applySound);
|
|
4822
5092
|
applyRef.current = applySound;
|
|
4823
|
-
const onChangeRef =
|
|
5093
|
+
const onChangeRef = useRef16(opts.onChange);
|
|
4824
5094
|
onChangeRef.current = opts.onChange;
|
|
4825
|
-
const dataRef =
|
|
4826
|
-
const [, setVersion] =
|
|
4827
|
-
const bump =
|
|
4828
|
-
const commit =
|
|
5095
|
+
const dataRef = useRef16({});
|
|
5096
|
+
const [, setVersion] = useState19(0);
|
|
5097
|
+
const bump = useCallback13(() => setVersion((v) => v + 1), []);
|
|
5098
|
+
const commit = useCallback13(
|
|
4829
5099
|
(trackId, next, notify) => {
|
|
4830
5100
|
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
4831
5101
|
bump();
|
|
@@ -4833,7 +5103,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4833
5103
|
},
|
|
4834
5104
|
[bump]
|
|
4835
5105
|
);
|
|
4836
|
-
const record =
|
|
5106
|
+
const record = useCallback13(
|
|
4837
5107
|
(trackId, descriptor, label) => {
|
|
4838
5108
|
const h = dataRef.current[trackId];
|
|
4839
5109
|
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
@@ -4848,7 +5118,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4848
5118
|
},
|
|
4849
5119
|
[max, commit]
|
|
4850
5120
|
);
|
|
4851
|
-
const restoreTo =
|
|
5121
|
+
const restoreTo = useCallback13(
|
|
4852
5122
|
async (trackId, index) => {
|
|
4853
5123
|
const h = dataRef.current[trackId];
|
|
4854
5124
|
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
@@ -4858,7 +5128,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4858
5128
|
},
|
|
4859
5129
|
[commit]
|
|
4860
5130
|
);
|
|
4861
|
-
const undo =
|
|
5131
|
+
const undo = useCallback13(
|
|
4862
5132
|
(trackId) => {
|
|
4863
5133
|
const h = dataRef.current[trackId];
|
|
4864
5134
|
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
@@ -4866,7 +5136,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4866
5136
|
},
|
|
4867
5137
|
[restoreTo]
|
|
4868
5138
|
);
|
|
4869
|
-
const toggleFavorite =
|
|
5139
|
+
const toggleFavorite = useCallback13(
|
|
4870
5140
|
(trackId, index) => {
|
|
4871
5141
|
const h = dataRef.current[trackId];
|
|
4872
5142
|
if (!h || index < 0 || index >= h.entries.length) return;
|
|
@@ -4875,7 +5145,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4875
5145
|
},
|
|
4876
5146
|
[commit]
|
|
4877
5147
|
);
|
|
4878
|
-
const restore =
|
|
5148
|
+
const restore = useCallback13(
|
|
4879
5149
|
(trackId, state) => {
|
|
4880
5150
|
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
4881
5151
|
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
@@ -4884,15 +5154,15 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4884
5154
|
},
|
|
4885
5155
|
[commit]
|
|
4886
5156
|
);
|
|
4887
|
-
const list =
|
|
5157
|
+
const list = useCallback13(
|
|
4888
5158
|
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
4889
5159
|
[]
|
|
4890
5160
|
);
|
|
4891
|
-
const canUndo =
|
|
5161
|
+
const canUndo = useCallback13((trackId) => {
|
|
4892
5162
|
const h = dataRef.current[trackId];
|
|
4893
5163
|
return !!h && h.cursor > 0;
|
|
4894
5164
|
}, []);
|
|
4895
|
-
const clear =
|
|
5165
|
+
const clear = useCallback13(
|
|
4896
5166
|
(trackId) => {
|
|
4897
5167
|
if (dataRef.current[trackId]) {
|
|
4898
5168
|
const next = { ...dataRef.current };
|
|
@@ -4904,18 +5174,2356 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4904
5174
|
},
|
|
4905
5175
|
[bump]
|
|
4906
5176
|
);
|
|
4907
|
-
const reset =
|
|
5177
|
+
const reset = useCallback13(() => {
|
|
4908
5178
|
dataRef.current = {};
|
|
4909
5179
|
bump();
|
|
4910
5180
|
}, [bump]);
|
|
4911
|
-
return
|
|
5181
|
+
return useMemo8(
|
|
4912
5182
|
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
4913
5183
|
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
4914
5184
|
);
|
|
4915
5185
|
}
|
|
4916
5186
|
|
|
5187
|
+
// src/panel-core/track-state.ts
|
|
5188
|
+
function newTrackState(handle, overrides = {}) {
|
|
5189
|
+
return {
|
|
5190
|
+
handle,
|
|
5191
|
+
prompt: "",
|
|
5192
|
+
role: "",
|
|
5193
|
+
runtimeState: { id: handle.id, muted: false, solo: false, volume: 0.75, pan: 0 },
|
|
5194
|
+
fxDetailState: { ...EMPTY_FX_DETAIL_STATE },
|
|
5195
|
+
drawerOpen: false,
|
|
5196
|
+
drawerTab: "fx",
|
|
5197
|
+
editorStage: false,
|
|
5198
|
+
isGenerating: false,
|
|
5199
|
+
error: null,
|
|
5200
|
+
hasMidi: false,
|
|
5201
|
+
generationProgress: 0,
|
|
5202
|
+
editNotes: [],
|
|
5203
|
+
editBars: 4,
|
|
5204
|
+
editBpm: 120,
|
|
5205
|
+
instrumentPluginId: handle.instrumentPluginId ?? null,
|
|
5206
|
+
instrumentName: handle.instrumentName ?? null,
|
|
5207
|
+
instrumentMissing: false,
|
|
5208
|
+
shuffleHistory: /* @__PURE__ */ new Set(),
|
|
5209
|
+
...overrides
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
// src/panel-core/panel-helpers.ts
|
|
5214
|
+
function trackDataKey(dbId, suffix) {
|
|
5215
|
+
return `track:${dbId}:${suffix}`;
|
|
5216
|
+
}
|
|
5217
|
+
function pluginFxToToggleFx(sdkState) {
|
|
5218
|
+
const result = { ...EMPTY_FX_DETAIL_STATE };
|
|
5219
|
+
for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
|
|
5220
|
+
const sdkCat = sdkState[category];
|
|
5221
|
+
if (sdkCat) {
|
|
5222
|
+
result[category] = {
|
|
5223
|
+
enabled: sdkCat.enabled,
|
|
5224
|
+
presetIndex: sdkCat.presetIndex,
|
|
5225
|
+
dryWet: sdkCat.dryWet
|
|
5226
|
+
};
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
return result;
|
|
5230
|
+
}
|
|
5231
|
+
function parseLLMNoteResponse(content) {
|
|
5232
|
+
try {
|
|
5233
|
+
let jsonStr = content.trim();
|
|
5234
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
5235
|
+
if (fenceMatch) {
|
|
5236
|
+
jsonStr = fenceMatch[1].trim();
|
|
5237
|
+
}
|
|
5238
|
+
const parsed = JSON.parse(jsonStr);
|
|
5239
|
+
if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
|
|
5240
|
+
return null;
|
|
5241
|
+
}
|
|
5242
|
+
const obj = parsed;
|
|
5243
|
+
if (!Array.isArray(obj.notes)) {
|
|
5244
|
+
return null;
|
|
5245
|
+
}
|
|
5246
|
+
const validNotes = [];
|
|
5247
|
+
for (const raw of obj.notes) {
|
|
5248
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
5249
|
+
const note = raw;
|
|
5250
|
+
const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
|
|
5251
|
+
const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
|
|
5252
|
+
const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
|
|
5253
|
+
const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
|
|
5254
|
+
if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
|
|
5255
|
+
validNotes.push({
|
|
5256
|
+
pitch: Math.round(pitch),
|
|
5257
|
+
startBeat,
|
|
5258
|
+
durationBeats,
|
|
5259
|
+
velocity: Math.round(velocity)
|
|
5260
|
+
});
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
const role = typeof obj.role === "string" ? obj.role : void 0;
|
|
5264
|
+
return { notes: validNotes, role };
|
|
5265
|
+
} catch {
|
|
5266
|
+
return null;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// src/panel-core/group-meta.ts
|
|
5271
|
+
function parseTrackGroups(sceneData, spec) {
|
|
5272
|
+
const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
|
|
5273
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5274
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
5275
|
+
const match = pattern.exec(key);
|
|
5276
|
+
if (!match) continue;
|
|
5277
|
+
const meta = spec.asMeta(val);
|
|
5278
|
+
if (!meta) continue;
|
|
5279
|
+
const groupId = spec.groupIdOf(meta);
|
|
5280
|
+
const list = groups.get(groupId) ?? [];
|
|
5281
|
+
list.push({ dbId: match[1], meta });
|
|
5282
|
+
groups.set(groupId, list);
|
|
5283
|
+
}
|
|
5284
|
+
const out = [];
|
|
5285
|
+
for (const [groupId, members] of groups) {
|
|
5286
|
+
if (spec.sortMembers) members.sort(spec.sortMembers);
|
|
5287
|
+
out.push({ groupId, members });
|
|
5288
|
+
}
|
|
5289
|
+
return out;
|
|
5290
|
+
}
|
|
5291
|
+
function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
|
|
5292
|
+
const byDbId = /* @__PURE__ */ new Map();
|
|
5293
|
+
for (const t of tracks) byDbId.set(getDbId(t), t);
|
|
5294
|
+
const resolved = [];
|
|
5295
|
+
const memberDbIds = /* @__PURE__ */ new Set();
|
|
5296
|
+
const staleMemberDbIds = [];
|
|
5297
|
+
for (const parsed of parsedGroups) {
|
|
5298
|
+
const live = { groupId: parsed.groupId, members: [] };
|
|
5299
|
+
for (const member of parsed.members) {
|
|
5300
|
+
const track = byDbId.get(member.dbId);
|
|
5301
|
+
if (track) live.members.push({ dbId: member.dbId, meta: member.meta, track });
|
|
5302
|
+
else staleMemberDbIds.push(member.dbId);
|
|
5303
|
+
}
|
|
5304
|
+
if (live.members.length === 0) continue;
|
|
5305
|
+
const complete = opts.isComplete ? opts.isComplete(live, parsed) : live.members.length === parsed.members.length;
|
|
5306
|
+
if (!complete) continue;
|
|
5307
|
+
resolved.push(live);
|
|
5308
|
+
for (const m of live.members) memberDbIds.add(m.dbId);
|
|
5309
|
+
}
|
|
5310
|
+
return { resolved, memberDbIds, staleMemberDbIds };
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
// src/panel-core/useTransitionOps.ts
|
|
5314
|
+
import { useCallback as useCallback14, useEffect as useEffect16, useRef as useRef17, useState as useState20 } from "react";
|
|
5315
|
+
function useTransitionOps({
|
|
5316
|
+
host,
|
|
5317
|
+
adapter,
|
|
5318
|
+
activeSceneId,
|
|
5319
|
+
isConnected,
|
|
5320
|
+
isAuthenticated,
|
|
5321
|
+
sceneContext,
|
|
5322
|
+
tracks,
|
|
5323
|
+
setTracks,
|
|
5324
|
+
loadTracks,
|
|
5325
|
+
setCrossfadePairsMeta,
|
|
5326
|
+
setFadesMeta,
|
|
5327
|
+
resolvedCrossfadePairs,
|
|
5328
|
+
resolvedFades
|
|
5329
|
+
}) {
|
|
5330
|
+
const { identity } = adapter;
|
|
5331
|
+
const appliedFadeAutomationRef = useRef17(/* @__PURE__ */ new Set());
|
|
5332
|
+
const applyCrossfadeAutomation = useCallback14(
|
|
5333
|
+
async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
|
|
5334
|
+
if (host.setTrackVolumeAutomation) {
|
|
5335
|
+
const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
|
|
5336
|
+
await host.setTrackVolumeAutomation(originTrackId, curves.origin).catch(() => {
|
|
5337
|
+
});
|
|
5338
|
+
await host.setTrackVolumeAutomation(targetTrackId, curves.target).catch(() => {
|
|
5339
|
+
});
|
|
5340
|
+
} else {
|
|
5341
|
+
await host.setTrackVolume(originTrackId, EQUAL_POWER_GAIN).catch(() => {
|
|
5342
|
+
});
|
|
5343
|
+
await host.setTrackVolume(targetTrackId, EQUAL_POWER_GAIN).catch(() => {
|
|
5344
|
+
});
|
|
5345
|
+
}
|
|
5346
|
+
},
|
|
5347
|
+
[host]
|
|
5348
|
+
);
|
|
5349
|
+
const applyFadeAutomation = useCallback14(
|
|
5350
|
+
async (trackId, direction, bars, bpm, sliderPos, gesture) => {
|
|
5351
|
+
if (!host.setTrackVolumeAutomation) return;
|
|
5352
|
+
const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
|
|
5353
|
+
await host.setTrackVolumeAutomation(trackId, points).catch(() => {
|
|
5354
|
+
});
|
|
5355
|
+
},
|
|
5356
|
+
[host]
|
|
5357
|
+
);
|
|
5358
|
+
const [isCreatingCrossfade, setIsCreatingCrossfade] = useState20(false);
|
|
5359
|
+
const handleCreateCrossfade = useCallback14(
|
|
5360
|
+
async (origin, target) => {
|
|
5361
|
+
const scene = activeSceneId;
|
|
5362
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
5363
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
5364
|
+
if (!scene) throw new Error("No active scene.");
|
|
5365
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
5366
|
+
if (!isAuthenticated) throw new Error("Please sign in to generate the bridge.");
|
|
5367
|
+
if (tracks.length + 2 > identity.maxTracks) {
|
|
5368
|
+
throw new Error("Not enough track slots for a crossfade.");
|
|
5369
|
+
}
|
|
5370
|
+
setIsCreatingCrossfade(true);
|
|
5371
|
+
const created = [];
|
|
5372
|
+
try {
|
|
5373
|
+
const role = target.role ?? origin.role ?? "";
|
|
5374
|
+
const mc = await host.getMusicalContext();
|
|
5375
|
+
const [originMidi, targetMidi, originKey, targetKey] = await Promise.all([
|
|
5376
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(origin.dbId) : Promise.resolve({ clips: [] }),
|
|
5377
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(target.dbId) : Promise.resolve({ clips: [] }),
|
|
5378
|
+
host.getSceneKey ? host.getSceneKey(fromSceneId) : Promise.resolve(null),
|
|
5379
|
+
host.getSceneKey ? host.getSceneKey(toSceneId) : Promise.resolve(null)
|
|
5380
|
+
]);
|
|
5381
|
+
const userPrompt = buildCrossfadeInpaintPrompt({
|
|
5382
|
+
role,
|
|
5383
|
+
bars: mc.bars,
|
|
5384
|
+
originName: origin.name,
|
|
5385
|
+
targetName: target.name,
|
|
5386
|
+
originKey: originKey ? `${originKey.key} ${originKey.mode}` : null,
|
|
5387
|
+
targetKey: targetKey ? `${targetKey.key} ${targetKey.mode}` : null,
|
|
5388
|
+
originNotes: originMidi.clips[0]?.notes ?? [],
|
|
5389
|
+
targetNotes: targetMidi.clips[0]?.notes ?? []
|
|
5390
|
+
});
|
|
5391
|
+
const llm = await host.generateWithLLM({
|
|
5392
|
+
system: adapter.buildSystemPrompt(host.getValidRoles()),
|
|
5393
|
+
user: userPrompt,
|
|
5394
|
+
responseFormat: "json"
|
|
5395
|
+
});
|
|
5396
|
+
const parsed = adapter.parseNotesResponse(llm.content);
|
|
5397
|
+
if (!parsed || parsed.notes.length === 0) {
|
|
5398
|
+
throw new Error("The bridge generator returned no notes.");
|
|
5399
|
+
}
|
|
5400
|
+
const notes = await host.postProcessMidi(parsed.notes, {
|
|
5401
|
+
quantize: true,
|
|
5402
|
+
removeOverlaps: true
|
|
5403
|
+
});
|
|
5404
|
+
const clip = {
|
|
5405
|
+
startTime: 0,
|
|
5406
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5407
|
+
tempo: mc.bpm,
|
|
5408
|
+
notes
|
|
5409
|
+
};
|
|
5410
|
+
const top = await host.createTrack({
|
|
5411
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-xf-o`,
|
|
5412
|
+
...adapter.createTrackOptions()
|
|
5413
|
+
});
|
|
5414
|
+
created.push(top);
|
|
5415
|
+
const bottom = await host.createTrack({
|
|
5416
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-xf-t`,
|
|
5417
|
+
...adapter.createTrackOptions()
|
|
5418
|
+
});
|
|
5419
|
+
created.push(bottom);
|
|
5420
|
+
if (role) {
|
|
5421
|
+
await host.setTrackRole(top.id, role).catch(() => {
|
|
5422
|
+
});
|
|
5423
|
+
await host.setTrackRole(bottom.id, role).catch(() => {
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
await host.writeMidiClip(top.id, clip);
|
|
5427
|
+
await host.writeMidiClip(bottom.id, clip);
|
|
5428
|
+
const copySound = async (newTrackId, sourceDbId) => {
|
|
5429
|
+
if (!host.getTrackSound) return "default";
|
|
5430
|
+
const snap = await host.getTrackSound(sourceDbId);
|
|
5431
|
+
if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) return "default";
|
|
5432
|
+
return adapter.sound.copySnapshot(newTrackId, snap);
|
|
5433
|
+
};
|
|
5434
|
+
const originLabel = await copySound(top.id, origin.dbId);
|
|
5435
|
+
const targetLabel = await copySound(bottom.id, target.dbId);
|
|
5436
|
+
await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
|
|
5437
|
+
const groupId = top.dbId;
|
|
5438
|
+
const originMeta = {
|
|
5439
|
+
groupId,
|
|
5440
|
+
slot: "origin",
|
|
5441
|
+
partnerDbId: bottom.dbId,
|
|
5442
|
+
sourceTrackDbId: origin.dbId,
|
|
5443
|
+
sourceSceneId: fromSceneId,
|
|
5444
|
+
sourceName: origin.name,
|
|
5445
|
+
soundLabel: originLabel,
|
|
5446
|
+
sliderPos: 0.5
|
|
5447
|
+
};
|
|
5448
|
+
const targetMeta = {
|
|
5449
|
+
groupId,
|
|
5450
|
+
slot: "target",
|
|
5451
|
+
partnerDbId: top.dbId,
|
|
5452
|
+
sourceTrackDbId: target.dbId,
|
|
5453
|
+
sourceSceneId: toSceneId,
|
|
5454
|
+
sourceName: target.name,
|
|
5455
|
+
soundLabel: targetLabel,
|
|
5456
|
+
sliderPos: 0.5
|
|
5457
|
+
};
|
|
5458
|
+
await host.setSceneData(scene, `track:${top.dbId}:crossfade`, originMeta);
|
|
5459
|
+
await host.setSceneData(scene, `track:${bottom.dbId}:crossfade`, targetMeta);
|
|
5460
|
+
await loadTracks(true);
|
|
5461
|
+
host.showToast("success", "Crossfade created", `${origin.name} \u2192 ${target.name}`);
|
|
5462
|
+
} catch (err) {
|
|
5463
|
+
for (const h of [...created].reverse()) {
|
|
5464
|
+
try {
|
|
5465
|
+
await host.deleteTrack(h.id);
|
|
5466
|
+
} catch {
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
5470
|
+
} finally {
|
|
5471
|
+
setIsCreatingCrossfade(false);
|
|
5472
|
+
}
|
|
5473
|
+
},
|
|
5474
|
+
[
|
|
5475
|
+
host,
|
|
5476
|
+
adapter,
|
|
5477
|
+
identity,
|
|
5478
|
+
activeSceneId,
|
|
5479
|
+
isConnected,
|
|
5480
|
+
isAuthenticated,
|
|
5481
|
+
tracks.length,
|
|
5482
|
+
sceneContext,
|
|
5483
|
+
applyCrossfadeAutomation,
|
|
5484
|
+
loadTracks
|
|
5485
|
+
]
|
|
5486
|
+
);
|
|
5487
|
+
const [isCreatingFade, setIsCreatingFade] = useState20(false);
|
|
5488
|
+
const handleCreateFade = useCallback14(
|
|
5489
|
+
async (selection, direction, gesture) => {
|
|
5490
|
+
const scene = activeSceneId;
|
|
5491
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
5492
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
5493
|
+
if (!scene) throw new Error("No active scene.");
|
|
5494
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
5495
|
+
if (!isAuthenticated) throw new Error("Please sign in to generate the fade.");
|
|
5496
|
+
if (tracks.length + 1 > identity.maxTracks) {
|
|
5497
|
+
throw new Error("Not enough track slots for a fade.");
|
|
5498
|
+
}
|
|
5499
|
+
setIsCreatingFade(true);
|
|
5500
|
+
const created = [];
|
|
5501
|
+
try {
|
|
5502
|
+
const role = selection.role ?? "";
|
|
5503
|
+
const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
|
|
5504
|
+
const mc = await host.getMusicalContext();
|
|
5505
|
+
const [srcMidi, srcKey] = await Promise.all([
|
|
5506
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(selection.dbId) : Promise.resolve({ clips: [] }),
|
|
5507
|
+
host.getSceneKey ? host.getSceneKey(sourceSceneId) : Promise.resolve(null)
|
|
5508
|
+
]);
|
|
5509
|
+
const srcNotes = srcMidi.clips[0]?.notes ?? [];
|
|
5510
|
+
const keyStr = srcKey ? `${srcKey.key} ${srcKey.mode}` : null;
|
|
5511
|
+
const userPrompt = buildCrossfadeInpaintPrompt({
|
|
5512
|
+
role,
|
|
5513
|
+
bars: mc.bars,
|
|
5514
|
+
originName: direction === "out" ? selection.name : "silence",
|
|
5515
|
+
targetName: direction === "in" ? selection.name : "silence",
|
|
5516
|
+
originKey: direction === "out" ? keyStr : null,
|
|
5517
|
+
targetKey: direction === "in" ? keyStr : null,
|
|
5518
|
+
originNotes: direction === "out" ? srcNotes : [],
|
|
5519
|
+
targetNotes: direction === "in" ? srcNotes : []
|
|
5520
|
+
});
|
|
5521
|
+
const llm = await host.generateWithLLM({
|
|
5522
|
+
system: adapter.buildSystemPrompt(host.getValidRoles()),
|
|
5523
|
+
user: userPrompt,
|
|
5524
|
+
responseFormat: "json"
|
|
5525
|
+
});
|
|
5526
|
+
const parsed = adapter.parseNotesResponse(llm.content);
|
|
5527
|
+
if (!parsed || parsed.notes.length === 0) {
|
|
5528
|
+
throw new Error("The fade generator returned no notes.");
|
|
5529
|
+
}
|
|
5530
|
+
const notes = await host.postProcessMidi(parsed.notes, {
|
|
5531
|
+
quantize: true,
|
|
5532
|
+
removeOverlaps: true
|
|
5533
|
+
});
|
|
5534
|
+
const clip = {
|
|
5535
|
+
startTime: 0,
|
|
5536
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5537
|
+
tempo: mc.bpm,
|
|
5538
|
+
notes
|
|
5539
|
+
};
|
|
5540
|
+
const track = await host.createTrack({
|
|
5541
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}`,
|
|
5542
|
+
...adapter.createTrackOptions()
|
|
5543
|
+
});
|
|
5544
|
+
created.push(track);
|
|
5545
|
+
if (role) await host.setTrackRole(track.id, role).catch(() => {
|
|
5546
|
+
});
|
|
5547
|
+
await host.writeMidiClip(track.id, clip);
|
|
5548
|
+
let soundLabel = "default";
|
|
5549
|
+
if (host.getTrackSound) {
|
|
5550
|
+
const snap = await host.getTrackSound(selection.dbId);
|
|
5551
|
+
if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
|
|
5552
|
+
soundLabel = await adapter.sound.copySnapshot(track.id, snap);
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
|
|
5556
|
+
appliedFadeAutomationRef.current.add(track.id);
|
|
5557
|
+
const meta = {
|
|
5558
|
+
direction,
|
|
5559
|
+
gesture,
|
|
5560
|
+
sourceTrackDbId: selection.dbId,
|
|
5561
|
+
sourceSceneId,
|
|
5562
|
+
sourceName: selection.name,
|
|
5563
|
+
soundLabel,
|
|
5564
|
+
sliderPos: 0.5
|
|
5565
|
+
};
|
|
5566
|
+
await host.setSceneData(scene, `track:${track.dbId}:fade`, meta);
|
|
5567
|
+
await loadTracks(true);
|
|
5568
|
+
host.showToast(
|
|
5569
|
+
"success",
|
|
5570
|
+
direction === "in" ? "Fade in created" : "Fade out created",
|
|
5571
|
+
selection.name
|
|
5572
|
+
);
|
|
5573
|
+
} catch (err) {
|
|
5574
|
+
for (const h of [...created].reverse()) {
|
|
5575
|
+
try {
|
|
5576
|
+
await host.deleteTrack(h.id);
|
|
5577
|
+
} catch {
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
5581
|
+
} finally {
|
|
5582
|
+
setIsCreatingFade(false);
|
|
5583
|
+
}
|
|
5584
|
+
},
|
|
5585
|
+
[
|
|
5586
|
+
host,
|
|
5587
|
+
adapter,
|
|
5588
|
+
identity,
|
|
5589
|
+
activeSceneId,
|
|
5590
|
+
isConnected,
|
|
5591
|
+
isAuthenticated,
|
|
5592
|
+
tracks.length,
|
|
5593
|
+
sceneContext,
|
|
5594
|
+
applyFadeAutomation,
|
|
5595
|
+
loadTracks
|
|
5596
|
+
]
|
|
5597
|
+
);
|
|
5598
|
+
const handleCrossfadeMute = useCallback14(
|
|
5599
|
+
(pair) => {
|
|
5600
|
+
const newMuted = !pair.origin.runtimeState.muted;
|
|
5601
|
+
for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
|
|
5602
|
+
setTracks(
|
|
5603
|
+
(prev) => prev.map(
|
|
5604
|
+
(t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
|
|
5605
|
+
)
|
|
5606
|
+
);
|
|
5607
|
+
host.setTrackMute(id, newMuted).catch(() => {
|
|
5608
|
+
});
|
|
5609
|
+
}
|
|
5610
|
+
},
|
|
5611
|
+
[host, setTracks]
|
|
5612
|
+
);
|
|
5613
|
+
const handleCrossfadeSolo = useCallback14(
|
|
5614
|
+
(pair) => {
|
|
5615
|
+
const newSolo = !pair.origin.runtimeState.solo;
|
|
5616
|
+
for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
|
|
5617
|
+
setTracks(
|
|
5618
|
+
(prev) => prev.map(
|
|
5619
|
+
(t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
|
|
5620
|
+
)
|
|
5621
|
+
);
|
|
5622
|
+
host.setTrackSolo(id, newSolo).catch(() => {
|
|
5623
|
+
});
|
|
5624
|
+
}
|
|
5625
|
+
},
|
|
5626
|
+
[host, setTracks]
|
|
5627
|
+
);
|
|
5628
|
+
const handleCrossfadeDelete = useCallback14(
|
|
5629
|
+
async (pair) => {
|
|
5630
|
+
try {
|
|
5631
|
+
for (const member of [pair.origin, pair.target]) {
|
|
5632
|
+
await host.deleteTrack(member.handle.id);
|
|
5633
|
+
if (activeSceneId) {
|
|
5634
|
+
await host.deleteSceneData(activeSceneId, `track:${member.handle.dbId}:crossfade`);
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
setCrossfadePairsMeta((prev) => prev.filter((p) => p.groupId !== pair.groupId));
|
|
5638
|
+
setTracks(
|
|
5639
|
+
(prev) => prev.filter(
|
|
5640
|
+
(t) => t.handle.id !== pair.origin.handle.id && t.handle.id !== pair.target.handle.id
|
|
5641
|
+
)
|
|
5642
|
+
);
|
|
5643
|
+
host.showToast("success", "Crossfade removed");
|
|
5644
|
+
} catch (err) {
|
|
5645
|
+
host.showToast(
|
|
5646
|
+
"error",
|
|
5647
|
+
"Failed to delete crossfade",
|
|
5648
|
+
err instanceof Error ? err.message : String(err)
|
|
5649
|
+
);
|
|
5650
|
+
}
|
|
5651
|
+
},
|
|
5652
|
+
[host, activeSceneId, setCrossfadePairsMeta, setTracks]
|
|
5653
|
+
);
|
|
5654
|
+
const crossfadeSliderTimers = useRef17({});
|
|
5655
|
+
const handleCrossfadeSlider = useCallback14(
|
|
5656
|
+
(pair, pos) => {
|
|
5657
|
+
setCrossfadePairsMeta(
|
|
5658
|
+
(prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
|
|
5659
|
+
);
|
|
5660
|
+
if (crossfadeSliderTimers.current[pair.groupId]) {
|
|
5661
|
+
clearTimeout(crossfadeSliderTimers.current[pair.groupId]);
|
|
5662
|
+
}
|
|
5663
|
+
crossfadeSliderTimers.current[pair.groupId] = setTimeout(() => {
|
|
5664
|
+
void (async () => {
|
|
5665
|
+
const mc = await host.getMusicalContext();
|
|
5666
|
+
await applyCrossfadeAutomation(
|
|
5667
|
+
pair.origin.handle.id,
|
|
5668
|
+
pair.target.handle.id,
|
|
5669
|
+
mc.bars,
|
|
5670
|
+
mc.bpm,
|
|
5671
|
+
pos
|
|
5672
|
+
);
|
|
5673
|
+
if (activeSceneId) {
|
|
5674
|
+
const sceneData = await host.getAllSceneData(activeSceneId);
|
|
5675
|
+
for (const dbId of [pair.originDbId, pair.targetDbId]) {
|
|
5676
|
+
const meta = asCrossfadeMeta(sceneData[`track:${dbId}:crossfade`]);
|
|
5677
|
+
if (meta) {
|
|
5678
|
+
host.setSceneData(activeSceneId, `track:${dbId}:crossfade`, { ...meta, sliderPos: pos }).catch(() => {
|
|
5679
|
+
});
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
})();
|
|
5684
|
+
}, 200);
|
|
5685
|
+
},
|
|
5686
|
+
[host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
|
|
5687
|
+
);
|
|
5688
|
+
const handleFadeDelete = useCallback14(
|
|
5689
|
+
async (fade) => {
|
|
5690
|
+
try {
|
|
5691
|
+
await host.deleteTrack(fade.track.handle.id);
|
|
5692
|
+
if (activeSceneId) {
|
|
5693
|
+
await host.deleteSceneData(activeSceneId, `track:${fade.dbId}:fade`);
|
|
5694
|
+
}
|
|
5695
|
+
setFadesMeta((prev) => prev.filter((f) => f.dbId !== fade.dbId));
|
|
5696
|
+
setTracks((prev) => prev.filter((t) => t.handle.id !== fade.track.handle.id));
|
|
5697
|
+
host.showToast("success", "Fade removed");
|
|
5698
|
+
} catch (err) {
|
|
5699
|
+
host.showToast(
|
|
5700
|
+
"error",
|
|
5701
|
+
"Failed to delete fade",
|
|
5702
|
+
err instanceof Error ? err.message : String(err)
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5705
|
+
},
|
|
5706
|
+
[host, activeSceneId, setFadesMeta, setTracks]
|
|
5707
|
+
);
|
|
5708
|
+
const fadeSliderTimers = useRef17({});
|
|
5709
|
+
const handleFadeSlider = useCallback14(
|
|
5710
|
+
(fade, pos) => {
|
|
5711
|
+
setFadesMeta(
|
|
5712
|
+
(prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
|
|
5713
|
+
);
|
|
5714
|
+
if (fadeSliderTimers.current[fade.dbId]) clearTimeout(fadeSliderTimers.current[fade.dbId]);
|
|
5715
|
+
fadeSliderTimers.current[fade.dbId] = setTimeout(() => {
|
|
5716
|
+
void (async () => {
|
|
5717
|
+
const mc = await host.getMusicalContext();
|
|
5718
|
+
await applyFadeAutomation(
|
|
5719
|
+
fade.track.handle.id,
|
|
5720
|
+
fade.meta.direction,
|
|
5721
|
+
mc.bars,
|
|
5722
|
+
mc.bpm,
|
|
5723
|
+
pos,
|
|
5724
|
+
fade.meta.gesture
|
|
5725
|
+
);
|
|
5726
|
+
if (activeSceneId) {
|
|
5727
|
+
const sceneData = await host.getAllSceneData(activeSceneId);
|
|
5728
|
+
const meta = asFadeMeta(sceneData[`track:${fade.dbId}:fade`]);
|
|
5729
|
+
if (meta) {
|
|
5730
|
+
host.setSceneData(activeSceneId, `track:${fade.dbId}:fade`, { ...meta, sliderPos: pos }).catch(() => {
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
})();
|
|
5735
|
+
}, 200);
|
|
5736
|
+
},
|
|
5737
|
+
[host, activeSceneId, applyFadeAutomation, setFadesMeta]
|
|
5738
|
+
);
|
|
5739
|
+
const lastResyncKeyRef = useRef17("");
|
|
5740
|
+
useEffect16(() => {
|
|
5741
|
+
if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
const resyncKey = [
|
|
5745
|
+
...resolvedCrossfadePairs.map(
|
|
5746
|
+
(p) => `${p.origin.handle.dbId}<${p.originSourceDbId}|${p.target.handle.dbId}<${p.targetSourceDbId}`
|
|
5747
|
+
),
|
|
5748
|
+
...resolvedFades.map((f) => `${f.track.handle.dbId}<${f.meta.sourceTrackDbId}`)
|
|
5749
|
+
].join(",");
|
|
5750
|
+
if (resyncKey === lastResyncKeyRef.current) return;
|
|
5751
|
+
lastResyncKeyRef.current = resyncKey;
|
|
5752
|
+
let cancelled = false;
|
|
5753
|
+
const reapplyIfDrifted = async (layerTrackId, layerDbId, sourceDbId) => {
|
|
5754
|
+
if (!host.getTrackSound || cancelled) return;
|
|
5755
|
+
const [sourceSnap, layerSnap] = await Promise.all([
|
|
5756
|
+
host.getTrackSound(sourceDbId),
|
|
5757
|
+
host.getTrackSound(layerDbId)
|
|
5758
|
+
]);
|
|
5759
|
+
if (cancelled || !sourceSnap || sourceSnap.kind !== adapter.sound.acceptedSnapshotKind) {
|
|
5760
|
+
return;
|
|
5761
|
+
}
|
|
5762
|
+
if (soundIdentity(sourceSnap) === soundIdentity(layerSnap)) return;
|
|
5763
|
+
try {
|
|
5764
|
+
await adapter.sound.copySnapshot(layerTrackId, sourceSnap);
|
|
5765
|
+
} catch {
|
|
5766
|
+
}
|
|
5767
|
+
};
|
|
5768
|
+
void (async () => {
|
|
5769
|
+
for (const pair of resolvedCrossfadePairs) {
|
|
5770
|
+
await reapplyIfDrifted(pair.origin.handle.id, pair.origin.handle.dbId, pair.originSourceDbId);
|
|
5771
|
+
await reapplyIfDrifted(pair.target.handle.id, pair.target.handle.dbId, pair.targetSourceDbId);
|
|
5772
|
+
}
|
|
5773
|
+
for (const fade of resolvedFades) {
|
|
5774
|
+
await reapplyIfDrifted(fade.track.handle.id, fade.track.handle.dbId, fade.meta.sourceTrackDbId);
|
|
5775
|
+
}
|
|
5776
|
+
})();
|
|
5777
|
+
return () => {
|
|
5778
|
+
cancelled = true;
|
|
5779
|
+
};
|
|
5780
|
+
}, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
|
|
5781
|
+
useEffect16(() => {
|
|
5782
|
+
if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
|
|
5783
|
+
void (async () => {
|
|
5784
|
+
const mc = await host.getMusicalContext();
|
|
5785
|
+
for (const fade of resolvedFades) {
|
|
5786
|
+
const id = fade.track.handle.id;
|
|
5787
|
+
if (appliedFadeAutomationRef.current.has(id)) continue;
|
|
5788
|
+
appliedFadeAutomationRef.current.add(id);
|
|
5789
|
+
await applyFadeAutomation(
|
|
5790
|
+
id,
|
|
5791
|
+
fade.meta.direction,
|
|
5792
|
+
mc.bars,
|
|
5793
|
+
mc.bpm,
|
|
5794
|
+
fade.meta.sliderPos,
|
|
5795
|
+
fade.meta.gesture
|
|
5796
|
+
);
|
|
5797
|
+
}
|
|
5798
|
+
})();
|
|
5799
|
+
}, [resolvedFades, host, applyFadeAutomation]);
|
|
5800
|
+
return {
|
|
5801
|
+
isCreatingCrossfade,
|
|
5802
|
+
isCreatingFade,
|
|
5803
|
+
handleCreateCrossfade,
|
|
5804
|
+
handleCreateFade,
|
|
5805
|
+
handleCrossfadeMute,
|
|
5806
|
+
handleCrossfadeSolo,
|
|
5807
|
+
handleCrossfadeDelete,
|
|
5808
|
+
handleCrossfadeSlider,
|
|
5809
|
+
handleFadeDelete,
|
|
5810
|
+
handleFadeSlider
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
// src/panel-core/useGeneratorPanelCore.tsx
|
|
5815
|
+
import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
5816
|
+
var EMPTY_PLACEHOLDERS = [];
|
|
5817
|
+
function useGeneratorPanelCore({
|
|
5818
|
+
ui,
|
|
5819
|
+
adapter
|
|
5820
|
+
}) {
|
|
5821
|
+
const {
|
|
5822
|
+
host,
|
|
5823
|
+
activeSceneId,
|
|
5824
|
+
isAuthenticated,
|
|
5825
|
+
isConnected,
|
|
5826
|
+
onHeaderContent,
|
|
5827
|
+
onLoading,
|
|
5828
|
+
sceneContext,
|
|
5829
|
+
onOpenContract,
|
|
5830
|
+
onExpandSelf,
|
|
5831
|
+
isExpanded
|
|
5832
|
+
} = ui;
|
|
5833
|
+
const { identity, features } = adapter;
|
|
5834
|
+
const logTag = identity.logTag;
|
|
5835
|
+
const adapterRef = useRef18(adapter);
|
|
5836
|
+
useEffect17(() => {
|
|
5837
|
+
if (adapterRef.current !== adapter) {
|
|
5838
|
+
adapterRef.current = adapter;
|
|
5839
|
+
console.warn(
|
|
5840
|
+
`[${logTag}] GeneratorPanelAdapter identity changed between renders \u2014 wrap it in useMemo(() => createAdapter(host), [host]) to avoid load loops.`
|
|
5841
|
+
);
|
|
5842
|
+
}
|
|
5843
|
+
}, [adapter, logTag]);
|
|
5844
|
+
const supportsMeters = typeof host.getTrackLevels === "function";
|
|
5845
|
+
const trackLevels = useTrackLevels(host, isExpanded);
|
|
5846
|
+
const [tracks, setTracks] = useState21([]);
|
|
5847
|
+
const [isLoadingTracks, setIsLoadingTracks] = useState21(false);
|
|
5848
|
+
const [importOpen, setImportOpen] = useState21(false);
|
|
5849
|
+
const [soundImportTarget, setSoundImportTarget] = useState21(null);
|
|
5850
|
+
const [designerView, setDesignerView] = useState21(false);
|
|
5851
|
+
const [transitionSourceTotal, setTransitionSourceTotal] = useState21(0);
|
|
5852
|
+
const [crossfadePairsMeta, setCrossfadePairsMeta] = useState21([]);
|
|
5853
|
+
const [fadesMeta, setFadesMeta] = useState21([]);
|
|
5854
|
+
const [genericGroupMetas, setGenericGroupMetas] = useState21({});
|
|
5855
|
+
const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
|
|
5856
|
+
const [placeholders, , setPlaceholdersForScene] = useSceneState(
|
|
5857
|
+
activeSceneId,
|
|
5858
|
+
EMPTY_PLACEHOLDERS
|
|
5859
|
+
);
|
|
5860
|
+
const saveTimeoutRefs = useRef18({});
|
|
5861
|
+
const editLoadStartedRef = useRef18(/* @__PURE__ */ new Set());
|
|
5862
|
+
const [availableInstruments, setAvailableInstruments] = useState21([]);
|
|
5863
|
+
const [instrumentsLoading, setInstrumentsLoading] = useState21(false);
|
|
5864
|
+
const engineToDbIdRef = useRef18(/* @__PURE__ */ new Map());
|
|
5865
|
+
const tracksLoadedForSceneRef = useRef18(null);
|
|
5866
|
+
const persistSoundHistory = useCallback15(
|
|
5867
|
+
(trackId, state) => {
|
|
5868
|
+
if (!activeSceneId) return;
|
|
5869
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
5870
|
+
host.setSceneData(activeSceneId, trackDataKey(dbId, "soundHistory"), state).catch(() => {
|
|
5871
|
+
});
|
|
5872
|
+
},
|
|
5873
|
+
[host, activeSceneId]
|
|
5874
|
+
);
|
|
5875
|
+
const soundHistory = useSoundHistory(adapter.sound.applySound, {
|
|
5876
|
+
max: adapter.sound.historyMax,
|
|
5877
|
+
onChange: persistSoundHistory
|
|
5878
|
+
});
|
|
5879
|
+
const anySolo = useAnySolo(host);
|
|
5880
|
+
const reorder = useTrackReorder({
|
|
5881
|
+
host,
|
|
5882
|
+
items: tracks,
|
|
5883
|
+
setItems: setTracks,
|
|
5884
|
+
getId: (t) => t.handle.dbId
|
|
5885
|
+
});
|
|
5886
|
+
const loadTracks = useCallback15(
|
|
5887
|
+
async (incremental = false) => {
|
|
5888
|
+
const sceneAtStart = activeSceneId;
|
|
5889
|
+
if (!sceneAtStart) {
|
|
5890
|
+
setTracks([]);
|
|
5891
|
+
setCrossfadePairsMeta([]);
|
|
5892
|
+
setFadesMeta([]);
|
|
5893
|
+
setGenericGroupMetas({});
|
|
5894
|
+
tracksLoadedForSceneRef.current = null;
|
|
5895
|
+
setIsLoadingTracks(false);
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
if (!incremental && tracksLoadedForSceneRef.current !== sceneAtStart) {
|
|
5899
|
+
setTracks([]);
|
|
5900
|
+
}
|
|
5901
|
+
tracksLoadedForSceneRef.current = sceneAtStart;
|
|
5902
|
+
if (!incremental) soundHistory.reset();
|
|
5903
|
+
const isStale = () => tracksLoadedForSceneRef.current !== sceneAtStart;
|
|
5904
|
+
if (!incremental) setIsLoadingTracks(true);
|
|
5905
|
+
try {
|
|
5906
|
+
await host.adoptSceneTracks();
|
|
5907
|
+
if (isStale()) return;
|
|
5908
|
+
const handles = await host.getPluginTracks();
|
|
5909
|
+
if (isStale()) return;
|
|
5910
|
+
const sceneData = await host.getAllSceneData(sceneAtStart);
|
|
5911
|
+
if (isStale()) return;
|
|
5912
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
5913
|
+
for (const h of handles) {
|
|
5914
|
+
idMap.set(h.id, h.dbId);
|
|
5915
|
+
}
|
|
5916
|
+
engineToDbIdRef.current = idMap;
|
|
5917
|
+
const trackStates = [];
|
|
5918
|
+
for (const handle of handles) {
|
|
5919
|
+
let runtimeState = {
|
|
5920
|
+
id: handle.id,
|
|
5921
|
+
muted: false,
|
|
5922
|
+
solo: false,
|
|
5923
|
+
volume: 0.75,
|
|
5924
|
+
pan: 0
|
|
5925
|
+
};
|
|
5926
|
+
let hasMidi = false;
|
|
5927
|
+
try {
|
|
5928
|
+
const info = await host.getTrackInfo(handle.id);
|
|
5929
|
+
runtimeState = {
|
|
5930
|
+
id: handle.id,
|
|
5931
|
+
muted: info.muted,
|
|
5932
|
+
solo: info.soloed,
|
|
5933
|
+
volume: info.volume,
|
|
5934
|
+
pan: info.pan
|
|
5935
|
+
};
|
|
5936
|
+
hasMidi = info.hasMidi;
|
|
5937
|
+
} catch {
|
|
5938
|
+
}
|
|
5939
|
+
let fxDetailState = newTrackState(handle).fxDetailState;
|
|
5940
|
+
try {
|
|
5941
|
+
const fxState = await host.getTrackFxState(handle.id);
|
|
5942
|
+
fxDetailState = pluginFxToToggleFx(fxState);
|
|
5943
|
+
} catch {
|
|
5944
|
+
}
|
|
5945
|
+
const promptKey = trackDataKey(handle.dbId, "prompt");
|
|
5946
|
+
let prompt = typeof sceneData[promptKey] === "string" ? sceneData[promptKey] : "";
|
|
5947
|
+
if (!prompt && handle.prompt) {
|
|
5948
|
+
prompt = handle.prompt;
|
|
5949
|
+
host.setSceneData(sceneAtStart, promptKey, prompt).catch(() => {
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5952
|
+
if (!hasMidi && handle.role) {
|
|
5953
|
+
hasMidi = true;
|
|
5954
|
+
}
|
|
5955
|
+
let instrumentMissing = false;
|
|
5956
|
+
if (handle.instrumentPluginId) {
|
|
5957
|
+
try {
|
|
5958
|
+
const instrDescriptor = await host.getTrackInstrument(handle.id);
|
|
5959
|
+
if (instrDescriptor?.missing) {
|
|
5960
|
+
instrumentMissing = true;
|
|
5961
|
+
}
|
|
5962
|
+
} catch {
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
trackStates.push(
|
|
5966
|
+
newTrackState(handle, {
|
|
5967
|
+
prompt,
|
|
5968
|
+
role: handle.role ?? "",
|
|
5969
|
+
runtimeState,
|
|
5970
|
+
fxDetailState,
|
|
5971
|
+
hasMidi,
|
|
5972
|
+
instrumentMissing
|
|
5973
|
+
})
|
|
5974
|
+
);
|
|
5975
|
+
}
|
|
5976
|
+
if (isStale()) return;
|
|
5977
|
+
setTracks((prev) => {
|
|
5978
|
+
const prevByDbId = new Map(prev.map((p) => [p.handle.dbId, p]));
|
|
5979
|
+
return trackStates.map((ts) => {
|
|
5980
|
+
const carry = prevByDbId.get(ts.handle.dbId);
|
|
5981
|
+
return carry ? { ...ts, editNotes: carry.editNotes, editBars: carry.editBars, editBpm: carry.editBpm } : ts;
|
|
5982
|
+
});
|
|
5983
|
+
});
|
|
5984
|
+
for (const ts of trackStates) {
|
|
5985
|
+
const persisted = sceneData[trackDataKey(ts.handle.dbId, "soundHistory")];
|
|
5986
|
+
if (persisted && typeof persisted === "object") {
|
|
5987
|
+
soundHistory.restore(ts.handle.id, persisted);
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
if (!isStale()) {
|
|
5991
|
+
setCrossfadePairsMeta(parseCrossfadePairs(sceneData));
|
|
5992
|
+
setFadesMeta(parseFades(sceneData));
|
|
5993
|
+
if (adapter.groupExtensions && adapter.groupExtensions.length > 0) {
|
|
5994
|
+
const map = {};
|
|
5995
|
+
for (const ext of adapter.groupExtensions) {
|
|
5996
|
+
map[ext.metaKey] = parseTrackGroups(sceneData, ext);
|
|
5997
|
+
}
|
|
5998
|
+
setGenericGroupMetas(map);
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
} catch (error) {
|
|
6002
|
+
console.error(`[${logTag}] Failed to load tracks:`, error);
|
|
6003
|
+
} finally {
|
|
6004
|
+
if (tracksLoadedForSceneRef.current === sceneAtStart) {
|
|
6005
|
+
setIsLoadingTracks(false);
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
},
|
|
6009
|
+
[host, activeSceneId, soundHistory, adapter, logTag]
|
|
6010
|
+
);
|
|
6011
|
+
useEffect17(() => {
|
|
6012
|
+
loadTracks();
|
|
6013
|
+
}, [loadTracks]);
|
|
6014
|
+
useEffect17(() => {
|
|
6015
|
+
const map = /* @__PURE__ */ new Map();
|
|
6016
|
+
for (const t of tracks) {
|
|
6017
|
+
map.set(t.handle.id, t.handle.dbId);
|
|
6018
|
+
}
|
|
6019
|
+
engineToDbIdRef.current = map;
|
|
6020
|
+
}, [tracks]);
|
|
6021
|
+
const loadedCompletedIdsRef = useRef18(/* @__PURE__ */ new Set());
|
|
6022
|
+
useEffect17(() => {
|
|
6023
|
+
if (placeholders.length === 0) {
|
|
6024
|
+
loadedCompletedIdsRef.current.clear();
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
const newCompleted = placeholders.filter(
|
|
6028
|
+
(ph) => ph.status === "completed" && !loadedCompletedIdsRef.current.has(ph.id)
|
|
6029
|
+
);
|
|
6030
|
+
if (newCompleted.length > 0) {
|
|
6031
|
+
for (const ph of newCompleted) {
|
|
6032
|
+
loadedCompletedIdsRef.current.add(ph.id);
|
|
6033
|
+
}
|
|
6034
|
+
console.log(
|
|
6035
|
+
`[${logTag}] ${newCompleted.length} track(s) completed, reloading. IDs:`,
|
|
6036
|
+
newCompleted.map((ph) => ph.id)
|
|
6037
|
+
);
|
|
6038
|
+
loadTracks(true);
|
|
6039
|
+
}
|
|
6040
|
+
}, [placeholders, loadTracks, logTag]);
|
|
6041
|
+
const adoptAndLoad = useCallback15(() => {
|
|
6042
|
+
loadTracks(true);
|
|
6043
|
+
}, [loadTracks]);
|
|
6044
|
+
useEffect17(() => {
|
|
6045
|
+
const unsub = host.onEngineReady(() => {
|
|
6046
|
+
adoptAndLoad();
|
|
6047
|
+
});
|
|
6048
|
+
return unsub;
|
|
6049
|
+
}, [host, adoptAndLoad]);
|
|
6050
|
+
useEffect17(() => {
|
|
6051
|
+
if (typeof host.onAfterAgentMutation !== "function") return;
|
|
6052
|
+
let timer = null;
|
|
6053
|
+
const unsub = host.onAfterAgentMutation(() => {
|
|
6054
|
+
if (timer) clearTimeout(timer);
|
|
6055
|
+
timer = setTimeout(() => {
|
|
6056
|
+
timer = null;
|
|
6057
|
+
loadTracks(true);
|
|
6058
|
+
}, 500);
|
|
6059
|
+
});
|
|
6060
|
+
return () => {
|
|
6061
|
+
unsub?.();
|
|
6062
|
+
if (timer) clearTimeout(timer);
|
|
6063
|
+
};
|
|
6064
|
+
}, [host, loadTracks]);
|
|
6065
|
+
useEffect17(() => {
|
|
6066
|
+
const unsub = host.onTrackStateChange((trackId, state) => {
|
|
6067
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
|
|
6068
|
+
});
|
|
6069
|
+
return unsub;
|
|
6070
|
+
}, [host]);
|
|
6071
|
+
useEffect17(() => {
|
|
6072
|
+
if (!features.bulkComposePlaceholders) return;
|
|
6073
|
+
console.log(`[${logTag}] Subscribing to composeProgress`);
|
|
6074
|
+
const unsub = host.onComposeProgress((event) => {
|
|
6075
|
+
const targetScene = event.sceneId;
|
|
6076
|
+
if (!targetScene) return;
|
|
6077
|
+
console.log(
|
|
6078
|
+
`[${logTag}] composeProgress event:`,
|
|
6079
|
+
event.phase,
|
|
6080
|
+
"sceneId:",
|
|
6081
|
+
targetScene,
|
|
6082
|
+
"placeholders:",
|
|
6083
|
+
event.placeholders?.length ?? "none"
|
|
6084
|
+
);
|
|
6085
|
+
switch (event.phase) {
|
|
6086
|
+
case "planning":
|
|
6087
|
+
setIsComposingForScene(targetScene, true);
|
|
6088
|
+
setPlaceholdersForScene(targetScene, []);
|
|
6089
|
+
break;
|
|
6090
|
+
case "generating":
|
|
6091
|
+
setIsComposingForScene(targetScene, false);
|
|
6092
|
+
if (event.placeholders) {
|
|
6093
|
+
setPlaceholdersForScene(targetScene, event.placeholders);
|
|
6094
|
+
}
|
|
6095
|
+
break;
|
|
6096
|
+
case "complete":
|
|
6097
|
+
case "error":
|
|
6098
|
+
setIsComposingForScene(targetScene, false);
|
|
6099
|
+
setPlaceholdersForScene(targetScene, EMPTY_PLACEHOLDERS);
|
|
6100
|
+
break;
|
|
6101
|
+
}
|
|
6102
|
+
});
|
|
6103
|
+
return unsub;
|
|
6104
|
+
}, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
|
|
6105
|
+
useEffect17(() => {
|
|
6106
|
+
const refs = saveTimeoutRefs;
|
|
6107
|
+
return () => {
|
|
6108
|
+
for (const timeout of Object.values(refs.current)) {
|
|
6109
|
+
clearTimeout(timeout);
|
|
6110
|
+
}
|
|
6111
|
+
};
|
|
6112
|
+
}, []);
|
|
6113
|
+
const isAddingTrackRef = useRef18(false);
|
|
6114
|
+
const [isAddingTrack, setIsAddingTrack] = useState21(false);
|
|
6115
|
+
const handleAddTrack = useCallback15(async () => {
|
|
6116
|
+
if (isAddingTrackRef.current) return;
|
|
6117
|
+
if (!activeSceneId) {
|
|
6118
|
+
host.showToast("warning", "Select SCENE");
|
|
6119
|
+
return;
|
|
6120
|
+
}
|
|
6121
|
+
if (!isConnected) {
|
|
6122
|
+
host.showToast("warning", "Systems not connected");
|
|
6123
|
+
return;
|
|
6124
|
+
}
|
|
6125
|
+
if (!isAuthenticated) {
|
|
6126
|
+
host.showToast("warning", "Sign In Required", "Please sign in to add tracks");
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
if (tracks.length >= identity.maxTracks) return;
|
|
6130
|
+
isAddingTrackRef.current = true;
|
|
6131
|
+
setIsAddingTrack(true);
|
|
6132
|
+
try {
|
|
6133
|
+
const handle = await host.createTrack({
|
|
6134
|
+
name: `${identity.trackNamePrefix}-${Date.now()}`,
|
|
6135
|
+
...adapter.createTrackOptions()
|
|
6136
|
+
});
|
|
6137
|
+
setTracks((prev) => [...prev, newTrackState(handle)]);
|
|
6138
|
+
onExpandSelf?.();
|
|
6139
|
+
setTimeout(() => {
|
|
6140
|
+
const inputs = document.querySelectorAll(
|
|
6141
|
+
`[data-testid="${identity.familyKey}-section"] [data-testid="sdk-prompt-input"]`
|
|
6142
|
+
);
|
|
6143
|
+
if (inputs.length > 0) {
|
|
6144
|
+
inputs[inputs.length - 1].focus();
|
|
6145
|
+
}
|
|
6146
|
+
}, 350);
|
|
6147
|
+
} catch (error) {
|
|
6148
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
6149
|
+
host.showToast("error", "Failed to create track", msg);
|
|
6150
|
+
} finally {
|
|
6151
|
+
isAddingTrackRef.current = false;
|
|
6152
|
+
setIsAddingTrack(false);
|
|
6153
|
+
}
|
|
6154
|
+
}, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
|
|
6155
|
+
const handlePortTrack = useCallback15(
|
|
6156
|
+
async (sel) => {
|
|
6157
|
+
if (!activeSceneId) {
|
|
6158
|
+
host.showToast("warning", "Select SCENE");
|
|
6159
|
+
return;
|
|
6160
|
+
}
|
|
6161
|
+
if (!isConnected) {
|
|
6162
|
+
host.showToast("warning", "Systems not connected");
|
|
6163
|
+
return;
|
|
6164
|
+
}
|
|
6165
|
+
if (tracks.length >= identity.maxTracks) {
|
|
6166
|
+
host.showToast("warning", "Track limit reached");
|
|
6167
|
+
return;
|
|
6168
|
+
}
|
|
6169
|
+
if (!host.readImportableTrackMidi) return;
|
|
6170
|
+
let handle = null;
|
|
6171
|
+
try {
|
|
6172
|
+
handle = await host.createTrack({
|
|
6173
|
+
name: `${identity.trackNamePrefix}-${Date.now()}`,
|
|
6174
|
+
...adapter.createTrackOptions()
|
|
6175
|
+
});
|
|
6176
|
+
if (sel.role) {
|
|
6177
|
+
try {
|
|
6178
|
+
await host.setTrackRole(handle.id, sel.role);
|
|
6179
|
+
} catch {
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6182
|
+
const midi = await host.readImportableTrackMidi(sel.sourceTrackDbId);
|
|
6183
|
+
const notes = midi.clips[0]?.notes ?? [];
|
|
6184
|
+
if (notes.length > 0) {
|
|
6185
|
+
const mc = await host.getMusicalContext();
|
|
6186
|
+
await host.writeMidiClip(handle.id, {
|
|
6187
|
+
startTime: 0,
|
|
6188
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
6189
|
+
tempo: mc.bpm,
|
|
6190
|
+
notes
|
|
6191
|
+
});
|
|
6192
|
+
}
|
|
6193
|
+
await adapter.applyPortedTrackSound(handle, sel.role);
|
|
6194
|
+
host.showToast(
|
|
6195
|
+
"success",
|
|
6196
|
+
`Imported to ${identity.familyKey}`,
|
|
6197
|
+
notes.length ? `${sel.trackName} \u2192 ${identity.familyKey}` : `${sel.trackName} (no MIDI yet)`
|
|
6198
|
+
);
|
|
6199
|
+
await loadTracks(true);
|
|
6200
|
+
} catch (err) {
|
|
6201
|
+
if (handle) {
|
|
6202
|
+
try {
|
|
6203
|
+
await host.deleteTrack(handle.id);
|
|
6204
|
+
} catch {
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
|
|
6208
|
+
}
|
|
6209
|
+
},
|
|
6210
|
+
[host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
|
|
6211
|
+
);
|
|
6212
|
+
const handleSoundImportPick = useCallback15(
|
|
6213
|
+
async (sel) => {
|
|
6214
|
+
const target = soundImportTarget;
|
|
6215
|
+
if (!target || !host.getTrackSound) {
|
|
6216
|
+
setSoundImportTarget(null);
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
const noun = adapter.sound.importNoun;
|
|
6220
|
+
const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
|
|
6221
|
+
try {
|
|
6222
|
+
const snap = await host.getTrackSound(sel.sourceTrackDbId);
|
|
6223
|
+
if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
|
|
6224
|
+
host.showToast(
|
|
6225
|
+
"error",
|
|
6226
|
+
`No ${noun} to import`,
|
|
6227
|
+
`${sel.trackName} has no ${identity.familyKey} ${noun}.`
|
|
6228
|
+
);
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6231
|
+
const descriptor = adapter.sound.descriptorFromSnapshot(snap);
|
|
6232
|
+
await adapter.sound.applySound(target.handle.id, descriptor);
|
|
6233
|
+
soundHistory.record(target.handle.id, descriptor, snap.label);
|
|
6234
|
+
host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
|
|
6235
|
+
} catch (err) {
|
|
6236
|
+
host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
|
|
6237
|
+
} finally {
|
|
6238
|
+
setSoundImportTarget(null);
|
|
6239
|
+
}
|
|
6240
|
+
},
|
|
6241
|
+
[soundImportTarget, host, adapter, identity.familyKey, soundHistory]
|
|
6242
|
+
);
|
|
6243
|
+
const [isExportingMidi, setIsExportingMidi] = useState21(false);
|
|
6244
|
+
const handleExportMidi = useCallback15(async () => {
|
|
6245
|
+
if (isExportingMidi) return;
|
|
6246
|
+
setIsExportingMidi(true);
|
|
6247
|
+
try {
|
|
6248
|
+
const result = await host.exportTracksAsMidiBundle({
|
|
6249
|
+
defaultName: identity.exportDefaultName ?? "midi-tracks"
|
|
6250
|
+
});
|
|
6251
|
+
if (result.success) {
|
|
6252
|
+
const filename = result.filePath.split("/").pop() || result.filePath;
|
|
6253
|
+
const skippedNote = result.skippedCount > 0 ? ` (${result.skippedCount} empty track${result.skippedCount === 1 ? "" : "s"} skipped)` : "";
|
|
6254
|
+
host.showToast(
|
|
6255
|
+
"success",
|
|
6256
|
+
"MIDI exported",
|
|
6257
|
+
`${result.trackCount} track${result.trackCount === 1 ? "" : "s"} \u2192 ${filename}${skippedNote}`
|
|
6258
|
+
);
|
|
6259
|
+
} else if (!("canceled" in result && result.canceled)) {
|
|
6260
|
+
const errMsg = "error" in result ? result.error : "Unknown error";
|
|
6261
|
+
host.showToast("error", "Export failed", errMsg);
|
|
6262
|
+
}
|
|
6263
|
+
} catch (error) {
|
|
6264
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
6265
|
+
host.showToast("error", "Export failed", msg);
|
|
6266
|
+
} finally {
|
|
6267
|
+
setIsExportingMidi(false);
|
|
6268
|
+
}
|
|
6269
|
+
}, [host, identity.exportDefaultName, isExportingMidi]);
|
|
6270
|
+
const isBulkActive = !!(isComposing || placeholders.length > 0);
|
|
6271
|
+
const needsContract = !sceneContext?.hasContract;
|
|
6272
|
+
const xfFromId = sceneContext?.transitionFromSceneId ?? null;
|
|
6273
|
+
const xfToId = sceneContext?.transitionToSceneId ?? null;
|
|
6274
|
+
const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
|
|
6275
|
+
useEffect17(() => {
|
|
6276
|
+
if (!canCrossfade) setDesignerView(false);
|
|
6277
|
+
}, [canCrossfade]);
|
|
6278
|
+
useEffect17(() => {
|
|
6279
|
+
if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
|
|
6280
|
+
setTransitionSourceTotal(0);
|
|
6281
|
+
return;
|
|
6282
|
+
}
|
|
6283
|
+
let cancelled = false;
|
|
6284
|
+
void Promise.all([host.listSceneFamilyTracks(xfFromId), host.listSceneFamilyTracks(xfToId)]).then(([a, b]) => {
|
|
6285
|
+
if (!cancelled) setTransitionSourceTotal(a.length + b.length);
|
|
6286
|
+
}).catch(() => {
|
|
6287
|
+
if (!cancelled) setTransitionSourceTotal(0);
|
|
6288
|
+
});
|
|
6289
|
+
return () => {
|
|
6290
|
+
cancelled = true;
|
|
6291
|
+
};
|
|
6292
|
+
}, [canCrossfade, xfFromId, xfToId, host]);
|
|
6293
|
+
const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
|
|
6294
|
+
useEffect17(() => {
|
|
6295
|
+
if (!onHeaderContent) return;
|
|
6296
|
+
const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
|
|
6297
|
+
onHeaderContent(
|
|
6298
|
+
/* @__PURE__ */ jsxs18("div", { className: "flex gap-1 items-center", children: [
|
|
6299
|
+
features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx24(
|
|
6300
|
+
"button",
|
|
6301
|
+
{
|
|
6302
|
+
"data-testid": `import-from-scene-${identity.familyKey}-button`,
|
|
6303
|
+
onClick: (e) => {
|
|
6304
|
+
e.stopPropagation();
|
|
6305
|
+
onExpandSelf?.();
|
|
6306
|
+
setImportOpen(true);
|
|
6307
|
+
},
|
|
6308
|
+
disabled: !activeSceneId || needsContract,
|
|
6309
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${!activeSceneId || needsContract ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
6310
|
+
children: identity.importTrackLabel ?? "Import Track"
|
|
6311
|
+
}
|
|
6312
|
+
),
|
|
6313
|
+
(!canCrossfade || !designerView) && /* @__PURE__ */ jsx24(
|
|
6314
|
+
"button",
|
|
6315
|
+
{
|
|
6316
|
+
"data-testid": `add-${identity.familyKey}-track-button`,
|
|
6317
|
+
onClick: (e) => {
|
|
6318
|
+
e.stopPropagation();
|
|
6319
|
+
if (needsContract) {
|
|
6320
|
+
onOpenContract?.();
|
|
6321
|
+
return;
|
|
6322
|
+
}
|
|
6323
|
+
handleAddTrack();
|
|
6324
|
+
},
|
|
6325
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${addDisabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-accent/10 border-sas-accent/30 text-sas-accent hover:bg-sas-accent/20"}`,
|
|
6326
|
+
children: identity.addTrackLabel ?? "Add Track"
|
|
6327
|
+
}
|
|
6328
|
+
),
|
|
6329
|
+
canCrossfade && /* @__PURE__ */ jsxs18(
|
|
6330
|
+
"button",
|
|
6331
|
+
{
|
|
6332
|
+
"data-testid": `${identity.familyKey}-view-toggle`,
|
|
6333
|
+
onClick: (e) => {
|
|
6334
|
+
e.stopPropagation();
|
|
6335
|
+
if (!designerView) {
|
|
6336
|
+
if (needsContract) {
|
|
6337
|
+
onOpenContract?.();
|
|
6338
|
+
return;
|
|
6339
|
+
}
|
|
6340
|
+
onExpandSelf?.();
|
|
6341
|
+
}
|
|
6342
|
+
setDesignerView((v) => !v);
|
|
6343
|
+
},
|
|
6344
|
+
disabled: !designerView && needsContract,
|
|
6345
|
+
title: designerView ? "Back to the track list" : "Open the transition designer",
|
|
6346
|
+
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",
|
|
6347
|
+
children: [
|
|
6348
|
+
transitionSourceTotal > 0 && /* @__PURE__ */ jsx24(
|
|
6349
|
+
"span",
|
|
6350
|
+
{
|
|
6351
|
+
className: "absolute inset-y-0 left-0 bg-sas-accent/25",
|
|
6352
|
+
style: { width: `${Math.min(100, transitionDone / transitionSourceTotal * 100)}%` },
|
|
6353
|
+
"aria-hidden": true
|
|
6354
|
+
}
|
|
6355
|
+
),
|
|
6356
|
+
/* @__PURE__ */ jsxs18("span", { className: "relative", children: [
|
|
6357
|
+
"\u21C4 ",
|
|
6358
|
+
designerView ? "Transition" : "Tracks",
|
|
6359
|
+
transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
|
|
6360
|
+
] })
|
|
6361
|
+
]
|
|
6362
|
+
}
|
|
6363
|
+
)
|
|
6364
|
+
] })
|
|
6365
|
+
);
|
|
6366
|
+
return () => {
|
|
6367
|
+
onHeaderContent(null);
|
|
6368
|
+
};
|
|
6369
|
+
}, [
|
|
6370
|
+
onHeaderContent,
|
|
6371
|
+
needsContract,
|
|
6372
|
+
isConnected,
|
|
6373
|
+
activeSceneId,
|
|
6374
|
+
tracks.length,
|
|
6375
|
+
isAddingTrack,
|
|
6376
|
+
handleAddTrack,
|
|
6377
|
+
onOpenContract,
|
|
6378
|
+
host,
|
|
6379
|
+
canCrossfade,
|
|
6380
|
+
designerView,
|
|
6381
|
+
transitionDone,
|
|
6382
|
+
transitionSourceTotal,
|
|
6383
|
+
onExpandSelf,
|
|
6384
|
+
identity,
|
|
6385
|
+
features.importTracks
|
|
6386
|
+
]);
|
|
6387
|
+
useEffect17(() => {
|
|
6388
|
+
if (!onLoading) return;
|
|
6389
|
+
const anyGenerating = tracks.some((t) => t.isGenerating);
|
|
6390
|
+
onLoading(isLoadingTracks || anyGenerating || isBulkActive);
|
|
6391
|
+
return () => {
|
|
6392
|
+
onLoading(false);
|
|
6393
|
+
};
|
|
6394
|
+
}, [onLoading, isLoadingTracks, tracks, isBulkActive]);
|
|
6395
|
+
const handleDeleteTrack = useCallback15(
|
|
6396
|
+
async (trackId) => {
|
|
6397
|
+
try {
|
|
6398
|
+
await host.deleteTrack(trackId);
|
|
6399
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
6400
|
+
if (activeSceneId) {
|
|
6401
|
+
await host.deleteSceneData(activeSceneId, trackDataKey(dbId, "prompt"));
|
|
6402
|
+
}
|
|
6403
|
+
setTracks((prev) => prev.filter((t) => t.handle.id !== trackId));
|
|
6404
|
+
} catch (error) {
|
|
6405
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
6406
|
+
host.showToast("error", "Failed to delete track", msg);
|
|
6407
|
+
}
|
|
6408
|
+
},
|
|
6409
|
+
[host, activeSceneId]
|
|
6410
|
+
);
|
|
6411
|
+
const handlePromptChange = useCallback15(
|
|
6412
|
+
(trackId, prompt) => {
|
|
6413
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
|
|
6414
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
6415
|
+
if (saveTimeoutRefs.current[trackId]) {
|
|
6416
|
+
clearTimeout(saveTimeoutRefs.current[trackId]);
|
|
6417
|
+
}
|
|
6418
|
+
saveTimeoutRefs.current[trackId] = setTimeout(() => {
|
|
6419
|
+
if (activeSceneId) {
|
|
6420
|
+
host.setSceneData(activeSceneId, trackDataKey(dbId, "prompt"), prompt).catch(() => {
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
}, 500);
|
|
6424
|
+
},
|
|
6425
|
+
[host, activeSceneId]
|
|
6426
|
+
);
|
|
6427
|
+
const resolvedGenericGroups = useMemo9(() => {
|
|
6428
|
+
const out = {};
|
|
6429
|
+
for (const ext of adapter.groupExtensions ?? []) {
|
|
6430
|
+
out[ext.metaKey] = resolveTrackGroups(
|
|
6431
|
+
genericGroupMetas[ext.metaKey] ?? [],
|
|
6432
|
+
tracks,
|
|
6433
|
+
(t) => t.handle.dbId,
|
|
6434
|
+
{
|
|
6435
|
+
isComplete: ext.isComplete
|
|
6436
|
+
}
|
|
6437
|
+
);
|
|
6438
|
+
}
|
|
6439
|
+
return out;
|
|
6440
|
+
}, [adapter, genericGroupMetas, tracks]);
|
|
6441
|
+
const genericGroupMemberDbIds = useMemo9(() => {
|
|
6442
|
+
const s = /* @__PURE__ */ new Set();
|
|
6443
|
+
for (const r of Object.values(resolvedGenericGroups)) {
|
|
6444
|
+
for (const dbId of r.memberDbIds) s.add(dbId);
|
|
6445
|
+
}
|
|
6446
|
+
return s;
|
|
6447
|
+
}, [resolvedGenericGroups]);
|
|
6448
|
+
const engineToDbId = useCallback15(
|
|
6449
|
+
(trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
|
|
6450
|
+
[]
|
|
6451
|
+
);
|
|
6452
|
+
const updateTrack = useCallback15(
|
|
6453
|
+
(trackId, patch) => {
|
|
6454
|
+
setTracks(
|
|
6455
|
+
(prev) => prev.map(
|
|
6456
|
+
(t) => t.handle.id === trackId ? typeof patch === "function" ? patch(t) : { ...t, ...patch } : t
|
|
6457
|
+
)
|
|
6458
|
+
);
|
|
6459
|
+
},
|
|
6460
|
+
[]
|
|
6461
|
+
);
|
|
6462
|
+
const markEditLoaded = useCallback15((trackId) => {
|
|
6463
|
+
editLoadStartedRef.current.add(trackId);
|
|
6464
|
+
}, []);
|
|
6465
|
+
const tracksRef = useRef18(tracks);
|
|
6466
|
+
useEffect17(() => {
|
|
6467
|
+
tracksRef.current = tracks;
|
|
6468
|
+
}, [tracks]);
|
|
6469
|
+
const resolvedGenericGroupsRef = useRef18(resolvedGenericGroups);
|
|
6470
|
+
useEffect17(() => {
|
|
6471
|
+
resolvedGenericGroupsRef.current = resolvedGenericGroups;
|
|
6472
|
+
}, [resolvedGenericGroups]);
|
|
6473
|
+
const makeServices = useCallback15(() => {
|
|
6474
|
+
return {
|
|
6475
|
+
host,
|
|
6476
|
+
activeSceneId,
|
|
6477
|
+
tracks: tracksRef.current,
|
|
6478
|
+
updateTrack,
|
|
6479
|
+
setTracks,
|
|
6480
|
+
reloadTracks: loadTracks,
|
|
6481
|
+
soundHistory,
|
|
6482
|
+
engineToDbId,
|
|
6483
|
+
trackDataKey,
|
|
6484
|
+
markEditLoaded,
|
|
6485
|
+
createFamilyTrack: (nameSuffix = "") => host.createTrack({
|
|
6486
|
+
name: `${identity.trackNamePrefix}-${Date.now()}${nameSuffix}`,
|
|
6487
|
+
...adapter.createTrackOptions()
|
|
6488
|
+
}),
|
|
6489
|
+
resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
|
|
6490
|
+
};
|
|
6491
|
+
}, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
|
|
6492
|
+
const handleGenerate = useCallback15(
|
|
6493
|
+
async (trackId) => {
|
|
6494
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6495
|
+
if (!track || !track.prompt.trim()) return;
|
|
6496
|
+
if (!isAuthenticated) {
|
|
6497
|
+
host.showToast("warning", "Sign In Required", "Please sign in to generate MIDI");
|
|
6498
|
+
return;
|
|
6499
|
+
}
|
|
6500
|
+
setTracks(
|
|
6501
|
+
(prev) => prev.map(
|
|
6502
|
+
(t) => t.handle.id === trackId ? { ...t, isGenerating: true, error: null, generationProgress: 0 } : t
|
|
6503
|
+
)
|
|
6504
|
+
);
|
|
6505
|
+
try {
|
|
6506
|
+
await adapter.generation.generate(track, makeServices());
|
|
6507
|
+
} catch (error) {
|
|
6508
|
+
const msg = error instanceof Error ? error.message : "Generation failed";
|
|
6509
|
+
setTracks(
|
|
6510
|
+
(prev) => prev.map(
|
|
6511
|
+
(t) => t.handle.id === trackId ? { ...t, isGenerating: false, error: msg, generationProgress: 0 } : t
|
|
6512
|
+
)
|
|
6513
|
+
);
|
|
6514
|
+
host.showToast("error", "Generation failed", msg);
|
|
6515
|
+
}
|
|
6516
|
+
},
|
|
6517
|
+
[host, adapter, tracks, isAuthenticated, makeServices]
|
|
6518
|
+
);
|
|
6519
|
+
const handleMuteToggle = useCallback15(
|
|
6520
|
+
(trackId) => {
|
|
6521
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6522
|
+
if (!track) return;
|
|
6523
|
+
const newMuted = !track.runtimeState.muted;
|
|
6524
|
+
setTracks(
|
|
6525
|
+
(prev) => prev.map(
|
|
6526
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
|
|
6527
|
+
)
|
|
6528
|
+
);
|
|
6529
|
+
host.setTrackMute(trackId, newMuted).catch(() => {
|
|
6530
|
+
setTracks(
|
|
6531
|
+
(prev) => prev.map(
|
|
6532
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: !newMuted } } : t
|
|
6533
|
+
)
|
|
6534
|
+
);
|
|
6535
|
+
});
|
|
6536
|
+
},
|
|
6537
|
+
[host, tracks]
|
|
6538
|
+
);
|
|
6539
|
+
const handleSoloToggle = useCallback15(
|
|
6540
|
+
(trackId) => {
|
|
6541
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6542
|
+
if (!track) return;
|
|
6543
|
+
const newSolo = !track.runtimeState.solo;
|
|
6544
|
+
setTracks(
|
|
6545
|
+
(prev) => prev.map(
|
|
6546
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
|
|
6547
|
+
)
|
|
6548
|
+
);
|
|
6549
|
+
host.setTrackSolo(trackId, newSolo).catch(() => {
|
|
6550
|
+
setTracks(
|
|
6551
|
+
(prev) => prev.map(
|
|
6552
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: !newSolo } } : t
|
|
6553
|
+
)
|
|
6554
|
+
);
|
|
6555
|
+
});
|
|
6556
|
+
},
|
|
6557
|
+
[host, tracks]
|
|
6558
|
+
);
|
|
6559
|
+
const handleVolumeChange = useCallback15(
|
|
6560
|
+
(trackId, volume) => {
|
|
6561
|
+
setTracks(
|
|
6562
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
|
|
6563
|
+
);
|
|
6564
|
+
host.setTrackVolume(trackId, volume).catch(() => {
|
|
6565
|
+
});
|
|
6566
|
+
},
|
|
6567
|
+
[host]
|
|
6568
|
+
);
|
|
6569
|
+
const handlePanChange = useCallback15(
|
|
6570
|
+
(trackId, pan) => {
|
|
6571
|
+
setTracks(
|
|
6572
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
|
|
6573
|
+
);
|
|
6574
|
+
host.setTrackPan(trackId, pan).catch(() => {
|
|
6575
|
+
});
|
|
6576
|
+
},
|
|
6577
|
+
[host]
|
|
6578
|
+
);
|
|
6579
|
+
const handleShuffle = useCallback15(
|
|
6580
|
+
async (trackId) => {
|
|
6581
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6582
|
+
if (!track) return;
|
|
6583
|
+
if (soundHistory.list(trackId).entries.length === 0) {
|
|
6584
|
+
try {
|
|
6585
|
+
const cap = await adapter.sound.captureSoundDescriptor(trackId);
|
|
6586
|
+
if (cap) soundHistory.record(trackId, cap.descriptor, adapter.sound.previousSoundLabel);
|
|
6587
|
+
} catch {
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
try {
|
|
6591
|
+
let result;
|
|
6592
|
+
let nextHistory;
|
|
6593
|
+
try {
|
|
6594
|
+
result = await adapter.shuffle.shuffle(track, Array.from(track.shuffleHistory));
|
|
6595
|
+
nextHistory = new Set(track.shuffleHistory);
|
|
6596
|
+
} catch (firstErr) {
|
|
6597
|
+
if (adapter.shuffle.isExhaustedError(firstErr)) {
|
|
6598
|
+
nextHistory = /* @__PURE__ */ new Set();
|
|
6599
|
+
result = await adapter.shuffle.shuffle(track, []);
|
|
6600
|
+
} else {
|
|
6601
|
+
throw firstErr;
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
nextHistory.add(result.appliedName);
|
|
6605
|
+
setTracks(
|
|
6606
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, shuffleHistory: nextHistory } : t)
|
|
6607
|
+
);
|
|
6608
|
+
try {
|
|
6609
|
+
const cap = await adapter.sound.captureSoundDescriptor(trackId);
|
|
6610
|
+
if (cap) soundHistory.record(trackId, cap.descriptor, result.appliedName);
|
|
6611
|
+
} catch {
|
|
6612
|
+
}
|
|
6613
|
+
console.log(`[${logTag}] Sound shuffled: ${result.appliedName} (history ${nextHistory.size})`);
|
|
6614
|
+
} catch (error) {
|
|
6615
|
+
const msg = error instanceof Error ? error.message : "Shuffle failed";
|
|
6616
|
+
host.showToast("error", "Shuffle failed", msg);
|
|
6617
|
+
}
|
|
6618
|
+
},
|
|
6619
|
+
[host, adapter, tracks, soundHistory, logTag]
|
|
6620
|
+
);
|
|
6621
|
+
const handleCopy = useCallback15(
|
|
6622
|
+
async (trackId) => {
|
|
6623
|
+
try {
|
|
6624
|
+
const newHandle = await host.duplicateTrack(trackId);
|
|
6625
|
+
await loadTracks();
|
|
6626
|
+
host.showToast("success", "Track duplicated", newHandle.name);
|
|
6627
|
+
} catch (error) {
|
|
6628
|
+
const msg = error instanceof Error ? error.message : "Copy failed";
|
|
6629
|
+
host.showToast("error", "Copy failed", msg);
|
|
6630
|
+
}
|
|
6631
|
+
},
|
|
6632
|
+
[host, loadTracks]
|
|
6633
|
+
);
|
|
6634
|
+
const handleFxToggle = useCallback15(
|
|
6635
|
+
(trackId, category, enabled) => {
|
|
6636
|
+
setTracks(
|
|
6637
|
+
(prev) => prev.map(
|
|
6638
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], enabled } } } : t
|
|
6639
|
+
)
|
|
6640
|
+
);
|
|
6641
|
+
host.toggleTrackFx(trackId, category, enabled).catch(() => {
|
|
6642
|
+
setTracks(
|
|
6643
|
+
(prev) => prev.map(
|
|
6644
|
+
(t) => t.handle.id === trackId ? {
|
|
6645
|
+
...t,
|
|
6646
|
+
fxDetailState: {
|
|
6647
|
+
...t.fxDetailState,
|
|
6648
|
+
[category]: { ...t.fxDetailState[category], enabled: !enabled }
|
|
6649
|
+
}
|
|
6650
|
+
} : t
|
|
6651
|
+
)
|
|
6652
|
+
);
|
|
6653
|
+
});
|
|
6654
|
+
},
|
|
6655
|
+
[host]
|
|
6656
|
+
);
|
|
6657
|
+
const handleFxPresetChange = useCallback15(
|
|
6658
|
+
(trackId, category, presetIndex) => {
|
|
6659
|
+
setTracks(
|
|
6660
|
+
(prev) => prev.map(
|
|
6661
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], presetIndex } } } : t
|
|
6662
|
+
)
|
|
6663
|
+
);
|
|
6664
|
+
host.setTrackFxPreset(trackId, category, presetIndex).then((result) => {
|
|
6665
|
+
if (result.dryWet !== void 0) {
|
|
6666
|
+
setTracks(
|
|
6667
|
+
(prev) => prev.map(
|
|
6668
|
+
(t) => t.handle.id === trackId ? {
|
|
6669
|
+
...t,
|
|
6670
|
+
fxDetailState: {
|
|
6671
|
+
...t.fxDetailState,
|
|
6672
|
+
[category]: { ...t.fxDetailState[category], dryWet: result.dryWet }
|
|
6673
|
+
}
|
|
6674
|
+
} : t
|
|
6675
|
+
)
|
|
6676
|
+
);
|
|
6677
|
+
}
|
|
6678
|
+
}).catch(() => {
|
|
6679
|
+
});
|
|
6680
|
+
},
|
|
6681
|
+
[host]
|
|
6682
|
+
);
|
|
6683
|
+
const handleFxDryWetChange = useCallback15(
|
|
6684
|
+
(trackId, category, value) => {
|
|
6685
|
+
setTracks(
|
|
6686
|
+
(prev) => prev.map(
|
|
6687
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], dryWet: value } } } : t
|
|
6688
|
+
)
|
|
6689
|
+
);
|
|
6690
|
+
host.setTrackFxDryWet(trackId, category, value).catch(() => {
|
|
6691
|
+
});
|
|
6692
|
+
},
|
|
6693
|
+
[host]
|
|
6694
|
+
);
|
|
6695
|
+
const toggleFxDrawer = useCallback15(
|
|
6696
|
+
(trackId) => {
|
|
6697
|
+
setTracks(
|
|
6698
|
+
(prev) => prev.map((t) => {
|
|
6699
|
+
if (t.handle.id !== trackId) return t;
|
|
6700
|
+
const onFx = t.drawerOpen && t.drawerTab === "fx";
|
|
6701
|
+
return { ...t, drawerOpen: !onFx, drawerTab: "fx", editorStage: false };
|
|
6702
|
+
})
|
|
6703
|
+
);
|
|
6704
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6705
|
+
const wasOnFx = !!track && track.drawerOpen && track.drawerTab === "fx";
|
|
6706
|
+
if (track && !wasOnFx) {
|
|
6707
|
+
host.getTrackFxState(trackId).then((fxState) => {
|
|
6708
|
+
setTracks(
|
|
6709
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
|
|
6710
|
+
);
|
|
6711
|
+
}).catch(() => {
|
|
6712
|
+
});
|
|
6713
|
+
}
|
|
6714
|
+
},
|
|
6715
|
+
[host, tracks]
|
|
6716
|
+
);
|
|
6717
|
+
const loadEditNotes = useCallback15(
|
|
6718
|
+
async (trackId) => {
|
|
6719
|
+
try {
|
|
6720
|
+
const mc = await host.getMusicalContext();
|
|
6721
|
+
let notes = [];
|
|
6722
|
+
if (typeof host.readMidiNotes === "function") {
|
|
6723
|
+
const result = await host.readMidiNotes(trackId);
|
|
6724
|
+
notes = result.clips[0]?.notes ?? [];
|
|
6725
|
+
}
|
|
6726
|
+
setTracks(
|
|
6727
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes, editBars: mc.bars, editBpm: mc.bpm } : t)
|
|
6728
|
+
);
|
|
6729
|
+
} catch (err) {
|
|
6730
|
+
console.warn(`[${logTag}] Failed to load MIDI for editing:`, err);
|
|
6731
|
+
}
|
|
6732
|
+
},
|
|
6733
|
+
[host, logTag]
|
|
6734
|
+
);
|
|
6735
|
+
const handleNotesChange = useCallback15(
|
|
6736
|
+
(trackId, notes) => {
|
|
6737
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
|
|
6738
|
+
const key = `edit:${trackId}`;
|
|
6739
|
+
if (saveTimeoutRefs.current[key]) {
|
|
6740
|
+
clearTimeout(saveTimeoutRefs.current[key]);
|
|
6741
|
+
}
|
|
6742
|
+
saveTimeoutRefs.current[key] = setTimeout(() => {
|
|
6743
|
+
void (async () => {
|
|
6744
|
+
try {
|
|
6745
|
+
if (notes.length === 0) {
|
|
6746
|
+
await host.clearMidi(trackId);
|
|
6747
|
+
} else {
|
|
6748
|
+
const mc = await host.getMusicalContext();
|
|
6749
|
+
await host.writeMidiClip(trackId, {
|
|
6750
|
+
startTime: 0,
|
|
6751
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
6752
|
+
tempo: mc.bpm,
|
|
6753
|
+
notes
|
|
6754
|
+
});
|
|
6755
|
+
}
|
|
6756
|
+
} catch (err) {
|
|
6757
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6758
|
+
host.showToast("error", "Failed to save edit", msg);
|
|
6759
|
+
}
|
|
6760
|
+
})();
|
|
6761
|
+
}, 300);
|
|
6762
|
+
},
|
|
6763
|
+
[host]
|
|
6764
|
+
);
|
|
6765
|
+
const handleTabChange = useCallback15(
|
|
6766
|
+
(trackId, tab) => {
|
|
6767
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
|
|
6768
|
+
if (tab === "fx") {
|
|
6769
|
+
host.getTrackFxState(trackId).then((fxState) => {
|
|
6770
|
+
setTracks(
|
|
6771
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
|
|
6772
|
+
);
|
|
6773
|
+
}).catch(() => {
|
|
6774
|
+
});
|
|
6775
|
+
} else if (tab === "pick" && availableInstruments.length === 0 && !instrumentsLoading) {
|
|
6776
|
+
setInstrumentsLoading(true);
|
|
6777
|
+
host.getAvailableInstruments().then((instruments) => {
|
|
6778
|
+
setAvailableInstruments(instruments);
|
|
6779
|
+
}).catch(() => {
|
|
6780
|
+
}).finally(() => {
|
|
6781
|
+
setInstrumentsLoading(false);
|
|
6782
|
+
});
|
|
6783
|
+
} else if (tab === "edit" && !editLoadStartedRef.current.has(trackId)) {
|
|
6784
|
+
editLoadStartedRef.current.add(trackId);
|
|
6785
|
+
void loadEditNotes(trackId);
|
|
6786
|
+
}
|
|
6787
|
+
},
|
|
6788
|
+
[host, availableInstruments.length, instrumentsLoading, loadEditNotes]
|
|
6789
|
+
);
|
|
6790
|
+
const handleProgressChange = useCallback15((trackId, pct) => {
|
|
6791
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
|
|
6792
|
+
}, []);
|
|
6793
|
+
const handleToggleDrawer = useCallback15((trackId) => {
|
|
6794
|
+
setTracks(
|
|
6795
|
+
(prev) => prev.map((t) => {
|
|
6796
|
+
if (t.handle.id !== trackId) return t;
|
|
6797
|
+
const onSound = t.drawerOpen && t.drawerTab !== "fx";
|
|
6798
|
+
return { ...t, drawerOpen: !onSound, drawerTab: "history", editorStage: false };
|
|
6799
|
+
})
|
|
6800
|
+
);
|
|
6801
|
+
}, []);
|
|
6802
|
+
const handleInstrumentSelect = useCallback15(
|
|
6803
|
+
async (trackId, pluginId) => {
|
|
6804
|
+
const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
|
|
6805
|
+
if (isDefaultInstrument) {
|
|
6806
|
+
setTracks(
|
|
6807
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: false, editorStage: false } : t)
|
|
6808
|
+
);
|
|
6809
|
+
try {
|
|
6810
|
+
await host.setTrackInstrument(trackId, pluginId);
|
|
6811
|
+
const descriptor = await host.getTrackInstrument(trackId);
|
|
6812
|
+
setTracks(
|
|
6813
|
+
(prev) => prev.map(
|
|
6814
|
+
(t) => t.handle.id === trackId ? {
|
|
6815
|
+
...t,
|
|
6816
|
+
instrumentPluginId: descriptor?.pluginId ?? null,
|
|
6817
|
+
instrumentName: descriptor?.name ?? null,
|
|
6818
|
+
instrumentMissing: descriptor?.missing ?? false
|
|
6819
|
+
} : t
|
|
6820
|
+
)
|
|
6821
|
+
);
|
|
6822
|
+
} catch (err) {
|
|
6823
|
+
const msg = err instanceof Error ? err.message : "Failed to load instrument";
|
|
6824
|
+
host.showToast("error", "Instrument load failed", msg);
|
|
6825
|
+
}
|
|
6826
|
+
return;
|
|
6827
|
+
}
|
|
6828
|
+
setTracks(
|
|
6829
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerTab: "pick", editorStage: true } : t)
|
|
6830
|
+
);
|
|
6831
|
+
try {
|
|
6832
|
+
await host.setTrackInstrument(trackId, pluginId);
|
|
6833
|
+
const descriptor = await host.getTrackInstrument(trackId);
|
|
6834
|
+
setTracks(
|
|
6835
|
+
(prev) => prev.map(
|
|
6836
|
+
(t) => t.handle.id === trackId ? {
|
|
6837
|
+
...t,
|
|
6838
|
+
instrumentPluginId: descriptor?.pluginId ?? null,
|
|
6839
|
+
instrumentName: descriptor?.name ?? null,
|
|
6840
|
+
instrumentMissing: descriptor?.missing ?? false
|
|
6841
|
+
} : t
|
|
6842
|
+
)
|
|
6843
|
+
);
|
|
6844
|
+
} catch (err) {
|
|
6845
|
+
const msg = err instanceof Error ? err.message : "Failed to load instrument";
|
|
6846
|
+
console.error(`[${logTag}] Failed to set instrument:`, err);
|
|
6847
|
+
host.showToast("error", "Instrument load failed", msg);
|
|
6848
|
+
setTracks(
|
|
6849
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
|
|
6850
|
+
);
|
|
6851
|
+
}
|
|
6852
|
+
},
|
|
6853
|
+
[host, identity.defaultInstrumentPluginId, logTag]
|
|
6854
|
+
);
|
|
6855
|
+
const handleShowEditor = useCallback15(
|
|
6856
|
+
async (trackId) => {
|
|
6857
|
+
try {
|
|
6858
|
+
await host.showInstrumentEditor(trackId);
|
|
6859
|
+
} catch (err) {
|
|
6860
|
+
const msg = err instanceof Error ? err.message : "Failed to open editor";
|
|
6861
|
+
host.showToast("error", "Editor failed", msg);
|
|
6862
|
+
}
|
|
6863
|
+
},
|
|
6864
|
+
[host]
|
|
6865
|
+
);
|
|
6866
|
+
const handleBackToInstruments = useCallback15((trackId) => {
|
|
6867
|
+
setTracks(
|
|
6868
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
|
|
6869
|
+
);
|
|
6870
|
+
}, []);
|
|
6871
|
+
const handleRefreshInstruments = useCallback15(() => {
|
|
6872
|
+
setInstrumentsLoading(true);
|
|
6873
|
+
host.getAvailableInstruments().then((instruments) => {
|
|
6874
|
+
setAvailableInstruments(instruments);
|
|
6875
|
+
}).catch(() => {
|
|
6876
|
+
}).finally(() => {
|
|
6877
|
+
setInstrumentsLoading(false);
|
|
6878
|
+
});
|
|
6879
|
+
}, [host]);
|
|
6880
|
+
const onAuditionNote = useCallback15(
|
|
6881
|
+
(trackId, pitch, velocity, ms) => {
|
|
6882
|
+
void host.auditionNote(trackId, pitch, velocity, ms);
|
|
6883
|
+
},
|
|
6884
|
+
[host]
|
|
6885
|
+
);
|
|
6886
|
+
const { resolvedCrossfadePairs, crossfadeMemberDbIds } = useMemo9(() => {
|
|
6887
|
+
const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
|
|
6888
|
+
const pairs = [];
|
|
6889
|
+
const members = /* @__PURE__ */ new Set();
|
|
6890
|
+
for (const p of crossfadePairsMeta) {
|
|
6891
|
+
const origin = byDbId.get(p.originDbId);
|
|
6892
|
+
const target = byDbId.get(p.targetDbId);
|
|
6893
|
+
if (origin && target) {
|
|
6894
|
+
pairs.push({ ...p, origin, target });
|
|
6895
|
+
members.add(p.originDbId);
|
|
6896
|
+
members.add(p.targetDbId);
|
|
6897
|
+
}
|
|
6898
|
+
}
|
|
6899
|
+
return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
|
|
6900
|
+
}, [tracks, crossfadePairsMeta]);
|
|
6901
|
+
const { resolvedFades, fadeMemberDbIds } = useMemo9(() => {
|
|
6902
|
+
const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
|
|
6903
|
+
const list = [];
|
|
6904
|
+
const members = /* @__PURE__ */ new Set();
|
|
6905
|
+
for (const f of fadesMeta) {
|
|
6906
|
+
const track = byDbId.get(f.dbId);
|
|
6907
|
+
if (track) {
|
|
6908
|
+
list.push({ ...f, track });
|
|
6909
|
+
members.add(f.dbId);
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
return { resolvedFades: list, fadeMemberDbIds: members };
|
|
6913
|
+
}, [tracks, fadesMeta]);
|
|
6914
|
+
const transition = useTransitionOps({
|
|
6915
|
+
host,
|
|
6916
|
+
adapter,
|
|
6917
|
+
activeSceneId,
|
|
6918
|
+
isConnected,
|
|
6919
|
+
isAuthenticated,
|
|
6920
|
+
sceneContext,
|
|
6921
|
+
tracks,
|
|
6922
|
+
setTracks,
|
|
6923
|
+
loadTracks,
|
|
6924
|
+
setCrossfadePairsMeta,
|
|
6925
|
+
setFadesMeta,
|
|
6926
|
+
resolvedCrossfadePairs,
|
|
6927
|
+
resolvedFades
|
|
6928
|
+
});
|
|
6929
|
+
const setGroupMute = useCallback15(
|
|
6930
|
+
(trackIds, muted) => {
|
|
6931
|
+
for (const id of trackIds) {
|
|
6932
|
+
setTracks(
|
|
6933
|
+
(prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted } } : t)
|
|
6934
|
+
);
|
|
6935
|
+
host.setTrackMute(id, muted).catch(() => {
|
|
6936
|
+
});
|
|
6937
|
+
}
|
|
6938
|
+
},
|
|
6939
|
+
[host]
|
|
6940
|
+
);
|
|
6941
|
+
const setGroupSolo = useCallback15(
|
|
6942
|
+
(trackIds, solo) => {
|
|
6943
|
+
for (const id of trackIds) {
|
|
6944
|
+
setTracks(
|
|
6945
|
+
(prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo } } : t)
|
|
6946
|
+
);
|
|
6947
|
+
host.setTrackSolo(id, solo).catch(() => {
|
|
6948
|
+
});
|
|
6949
|
+
}
|
|
6950
|
+
},
|
|
6951
|
+
[host]
|
|
6952
|
+
);
|
|
6953
|
+
const deleteGroup = useCallback15(
|
|
6954
|
+
async (members, cleanupKeySuffixes) => {
|
|
6955
|
+
for (const member of members) {
|
|
6956
|
+
try {
|
|
6957
|
+
await host.deleteTrack(member.engineId);
|
|
6958
|
+
} catch {
|
|
6959
|
+
}
|
|
6960
|
+
if (activeSceneId) {
|
|
6961
|
+
for (const suffix of cleanupKeySuffixes) {
|
|
6962
|
+
await host.deleteSceneData(activeSceneId, trackDataKey(member.dbId, suffix)).catch(() => {
|
|
6963
|
+
});
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
const gone = new Set(members.map((m) => m.engineId));
|
|
6968
|
+
setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
|
|
6969
|
+
await loadTracks(true);
|
|
6970
|
+
},
|
|
6971
|
+
[host, activeSceneId, loadTracks]
|
|
6972
|
+
);
|
|
6973
|
+
const handlers = useMemo9(
|
|
6974
|
+
() => ({
|
|
6975
|
+
promptChange: handlePromptChange,
|
|
6976
|
+
generate: (trackId) => {
|
|
6977
|
+
void handleGenerate(trackId);
|
|
6978
|
+
},
|
|
6979
|
+
shuffle: (trackId) => {
|
|
6980
|
+
void handleShuffle(trackId);
|
|
6981
|
+
},
|
|
6982
|
+
copy: (trackId) => {
|
|
6983
|
+
void handleCopy(trackId);
|
|
6984
|
+
},
|
|
6985
|
+
delete: (trackId) => {
|
|
6986
|
+
void handleDeleteTrack(trackId);
|
|
6987
|
+
},
|
|
6988
|
+
muteToggle: handleMuteToggle,
|
|
6989
|
+
soloToggle: handleSoloToggle,
|
|
6990
|
+
volumeChange: handleVolumeChange,
|
|
6991
|
+
panChange: handlePanChange,
|
|
6992
|
+
tabChange: handleTabChange,
|
|
6993
|
+
toggleDrawer: handleToggleDrawer,
|
|
6994
|
+
toggleFxDrawer,
|
|
6995
|
+
notesChange: handleNotesChange,
|
|
6996
|
+
progressChange: handleProgressChange
|
|
6997
|
+
}),
|
|
6998
|
+
[
|
|
6999
|
+
handlePromptChange,
|
|
7000
|
+
handleGenerate,
|
|
7001
|
+
handleShuffle,
|
|
7002
|
+
handleCopy,
|
|
7003
|
+
handleDeleteTrack,
|
|
7004
|
+
handleMuteToggle,
|
|
7005
|
+
handleSoloToggle,
|
|
7006
|
+
handleVolumeChange,
|
|
7007
|
+
handlePanChange,
|
|
7008
|
+
handleTabChange,
|
|
7009
|
+
handleToggleDrawer,
|
|
7010
|
+
toggleFxDrawer,
|
|
7011
|
+
handleNotesChange,
|
|
7012
|
+
handleProgressChange
|
|
7013
|
+
]
|
|
7014
|
+
);
|
|
7015
|
+
return {
|
|
7016
|
+
ui,
|
|
7017
|
+
adapter,
|
|
7018
|
+
tracks,
|
|
7019
|
+
setTracks,
|
|
7020
|
+
isLoadingTracks,
|
|
7021
|
+
loadTracks,
|
|
7022
|
+
engineToDbId,
|
|
7023
|
+
supportsMeters,
|
|
7024
|
+
trackLevels,
|
|
7025
|
+
anySolo,
|
|
7026
|
+
reorder,
|
|
7027
|
+
soundHistory,
|
|
7028
|
+
isComposing,
|
|
7029
|
+
placeholders,
|
|
7030
|
+
isAddingTrack,
|
|
7031
|
+
isExportingMidi,
|
|
7032
|
+
designerView,
|
|
7033
|
+
canCrossfade,
|
|
7034
|
+
needsContract,
|
|
7035
|
+
xfFromId,
|
|
7036
|
+
xfToId,
|
|
7037
|
+
importOpen,
|
|
7038
|
+
setImportOpen,
|
|
7039
|
+
soundImportTarget,
|
|
7040
|
+
setSoundImportTarget,
|
|
7041
|
+
handleSoundImportPick,
|
|
7042
|
+
handlePortTrack,
|
|
7043
|
+
transition,
|
|
7044
|
+
crossfadePairsMeta,
|
|
7045
|
+
fadesMeta,
|
|
7046
|
+
resolvedCrossfadePairs,
|
|
7047
|
+
crossfadeMemberDbIds,
|
|
7048
|
+
resolvedFades,
|
|
7049
|
+
fadeMemberDbIds,
|
|
7050
|
+
resolvedGenericGroups,
|
|
7051
|
+
genericGroupMemberDbIds,
|
|
7052
|
+
availableInstruments,
|
|
7053
|
+
instrumentsLoading,
|
|
7054
|
+
handlers,
|
|
7055
|
+
handleGenerate,
|
|
7056
|
+
handleShuffle,
|
|
7057
|
+
handleAddTrack,
|
|
7058
|
+
handleDeleteTrack,
|
|
7059
|
+
handleExportMidi,
|
|
7060
|
+
handlePromptChange,
|
|
7061
|
+
handleMuteToggle,
|
|
7062
|
+
handleSoloToggle,
|
|
7063
|
+
handleVolumeChange,
|
|
7064
|
+
handlePanChange,
|
|
7065
|
+
handleTabChange,
|
|
7066
|
+
handleToggleDrawer,
|
|
7067
|
+
toggleFxDrawer,
|
|
7068
|
+
handleNotesChange,
|
|
7069
|
+
handleProgressChange,
|
|
7070
|
+
handleCopy,
|
|
7071
|
+
handleFxToggle,
|
|
7072
|
+
handleFxPresetChange,
|
|
7073
|
+
handleFxDryWetChange,
|
|
7074
|
+
handleInstrumentSelect,
|
|
7075
|
+
handleShowEditor,
|
|
7076
|
+
handleBackToInstruments,
|
|
7077
|
+
handleRefreshInstruments,
|
|
7078
|
+
onAuditionNote,
|
|
7079
|
+
makeServices,
|
|
7080
|
+
setGroupMute,
|
|
7081
|
+
setGroupSolo,
|
|
7082
|
+
deleteGroup
|
|
7083
|
+
};
|
|
7084
|
+
}
|
|
7085
|
+
|
|
7086
|
+
// src/panel-core/GeneratorPanelShell.tsx
|
|
7087
|
+
import React21, { useCallback as useCallback16 } from "react";
|
|
7088
|
+
import { Fragment as Fragment6, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
7089
|
+
function GeneratorPanelShell({ core, slots }) {
|
|
7090
|
+
const {
|
|
7091
|
+
ui,
|
|
7092
|
+
adapter,
|
|
7093
|
+
tracks,
|
|
7094
|
+
isLoadingTracks,
|
|
7095
|
+
supportsMeters,
|
|
7096
|
+
trackLevels,
|
|
7097
|
+
anySolo,
|
|
7098
|
+
reorder,
|
|
7099
|
+
soundHistory,
|
|
7100
|
+
isComposing,
|
|
7101
|
+
placeholders,
|
|
7102
|
+
designerView,
|
|
7103
|
+
canCrossfade,
|
|
7104
|
+
xfFromId,
|
|
7105
|
+
xfToId,
|
|
7106
|
+
importOpen,
|
|
7107
|
+
setImportOpen,
|
|
7108
|
+
soundImportTarget,
|
|
7109
|
+
setSoundImportTarget,
|
|
7110
|
+
handleSoundImportPick,
|
|
7111
|
+
handlePortTrack,
|
|
7112
|
+
transition,
|
|
7113
|
+
crossfadePairsMeta,
|
|
7114
|
+
fadesMeta,
|
|
7115
|
+
resolvedCrossfadePairs,
|
|
7116
|
+
crossfadeMemberDbIds,
|
|
7117
|
+
resolvedFades,
|
|
7118
|
+
fadeMemberDbIds,
|
|
7119
|
+
resolvedGenericGroups,
|
|
7120
|
+
genericGroupMemberDbIds,
|
|
7121
|
+
availableInstruments,
|
|
7122
|
+
instrumentsLoading,
|
|
7123
|
+
handlers,
|
|
7124
|
+
isExportingMidi,
|
|
7125
|
+
handleExportMidi,
|
|
7126
|
+
handleFxToggle,
|
|
7127
|
+
handleFxPresetChange,
|
|
7128
|
+
handleFxDryWetChange,
|
|
7129
|
+
handleInstrumentSelect,
|
|
7130
|
+
handleShowEditor,
|
|
7131
|
+
handleBackToInstruments,
|
|
7132
|
+
handleRefreshInstruments,
|
|
7133
|
+
onAuditionNote,
|
|
7134
|
+
loadTracks,
|
|
7135
|
+
makeServices,
|
|
7136
|
+
setGroupMute,
|
|
7137
|
+
setGroupSolo,
|
|
7138
|
+
deleteGroup
|
|
7139
|
+
} = core;
|
|
7140
|
+
const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
|
|
7141
|
+
const panelBus = usePanelBus(host, activeSceneId);
|
|
7142
|
+
const { identity, features } = adapter;
|
|
7143
|
+
const buildRowProps = useCallback16(
|
|
7144
|
+
(track, drag) => {
|
|
7145
|
+
const id = track.handle.id;
|
|
7146
|
+
const pickerProps = features.instrumentPicker ? {
|
|
7147
|
+
instrumentName: track.instrumentName,
|
|
7148
|
+
instrumentMissing: track.instrumentMissing,
|
|
7149
|
+
onToggleDrawer: () => handlers.toggleDrawer(id),
|
|
7150
|
+
availableInstruments,
|
|
7151
|
+
currentInstrumentPluginId: track.instrumentPluginId,
|
|
7152
|
+
onInstrumentSelect: (pluginId) => handleInstrumentSelect(id, pluginId),
|
|
7153
|
+
instrumentsLoading,
|
|
7154
|
+
onRefreshInstruments: handleRefreshInstruments,
|
|
7155
|
+
editorStage: track.editorStage,
|
|
7156
|
+
onShowEditor: () => handleShowEditor(id),
|
|
7157
|
+
onBackToInstruments: () => handleBackToInstruments(id)
|
|
7158
|
+
} : {};
|
|
7159
|
+
const importSoundProps = features.importTracks ? {
|
|
7160
|
+
onImportSound: () => setSoundImportTarget(track),
|
|
7161
|
+
importSoundLabel: adapter.sound.importSoundLabel
|
|
7162
|
+
} : {};
|
|
7163
|
+
const props = {
|
|
7164
|
+
...drag ? { drag } : {},
|
|
7165
|
+
track: { id, name: track.handle.name, role: track.role },
|
|
7166
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7167
|
+
prompt: track.prompt,
|
|
7168
|
+
runtimeState: {
|
|
7169
|
+
muted: track.runtimeState.muted,
|
|
7170
|
+
solo: track.runtimeState.solo,
|
|
7171
|
+
volume: track.runtimeState.volume,
|
|
7172
|
+
pan: track.runtimeState.pan
|
|
7173
|
+
},
|
|
7174
|
+
soloedOut: anySolo && !track.runtimeState.solo,
|
|
7175
|
+
fxDetailState: track.fxDetailState,
|
|
7176
|
+
drawerOpen: track.drawerOpen,
|
|
7177
|
+
drawerTab: track.drawerTab,
|
|
7178
|
+
onTabChange: (tab) => handlers.tabChange(id, tab),
|
|
7179
|
+
isGenerating: track.isGenerating,
|
|
7180
|
+
isAuthenticated,
|
|
7181
|
+
error: track.error,
|
|
7182
|
+
hasMidi: track.hasMidi,
|
|
7183
|
+
generationProgress: track.generationProgress,
|
|
7184
|
+
estimatedGenerationMs: identity.estimatedGenerationMs,
|
|
7185
|
+
onPromptChange: (prompt) => handlers.promptChange(id, prompt),
|
|
7186
|
+
onGenerate: () => handlers.generate(id),
|
|
7187
|
+
onShuffle: () => handlers.shuffle(id),
|
|
7188
|
+
onCopy: () => handlers.copy(id),
|
|
7189
|
+
onDelete: () => handlers.delete(id),
|
|
7190
|
+
onMuteToggle: () => handlers.muteToggle(id),
|
|
7191
|
+
onSoloToggle: () => handlers.soloToggle(id),
|
|
7192
|
+
onVolumeChange: (vol) => handlers.volumeChange(id, vol),
|
|
7193
|
+
onPanChange: (pan) => handlers.panChange(id, pan),
|
|
7194
|
+
onFxToggle: (cat, enabled) => handleFxToggle(id, cat, enabled),
|
|
7195
|
+
onFxPresetChange: (cat, idx) => handleFxPresetChange(id, cat, idx),
|
|
7196
|
+
onFxDryWetChange: (cat, val) => handleFxDryWetChange(id, cat, val),
|
|
7197
|
+
onToggleFxDrawer: () => handlers.toggleFxDrawer(id),
|
|
7198
|
+
onProgressChange: (pct) => handlers.progressChange(id, pct),
|
|
7199
|
+
accentColor: identity.accentColor,
|
|
7200
|
+
...pickerProps,
|
|
7201
|
+
soundHistory: soundHistory.list(id).entries,
|
|
7202
|
+
soundHistoryCursor: soundHistory.list(id).cursor,
|
|
7203
|
+
onRestoreSound: (i) => {
|
|
7204
|
+
void soundHistory.restoreTo(id, i);
|
|
7205
|
+
},
|
|
7206
|
+
onToggleFavorite: (i) => soundHistory.toggleFavorite(id, i),
|
|
7207
|
+
...importSoundProps,
|
|
7208
|
+
editNotes: track.editNotes,
|
|
7209
|
+
onNotesChange: (notes) => handlers.notesChange(id, notes),
|
|
7210
|
+
editBars: track.editBars,
|
|
7211
|
+
editBpm: track.editBpm,
|
|
7212
|
+
editSnap: 0.25,
|
|
7213
|
+
onAuditionNote: (pitch, vel, ms) => onAuditionNote(id, pitch, vel, ms)
|
|
7214
|
+
};
|
|
7215
|
+
return adapter.mapTrackRowProps ? adapter.mapTrackRowProps(track, props) : props;
|
|
7216
|
+
},
|
|
7217
|
+
[
|
|
7218
|
+
features.instrumentPicker,
|
|
7219
|
+
features.importTracks,
|
|
7220
|
+
adapter,
|
|
7221
|
+
supportsMeters,
|
|
7222
|
+
trackLevels,
|
|
7223
|
+
anySolo,
|
|
7224
|
+
isAuthenticated,
|
|
7225
|
+
identity,
|
|
7226
|
+
handlers,
|
|
7227
|
+
availableInstruments,
|
|
7228
|
+
instrumentsLoading,
|
|
7229
|
+
handleInstrumentSelect,
|
|
7230
|
+
handleRefreshInstruments,
|
|
7231
|
+
handleShowEditor,
|
|
7232
|
+
handleBackToInstruments,
|
|
7233
|
+
setSoundImportTarget,
|
|
7234
|
+
soundHistory,
|
|
7235
|
+
handleFxToggle,
|
|
7236
|
+
handleFxPresetChange,
|
|
7237
|
+
handleFxDryWetChange,
|
|
7238
|
+
onAuditionNote
|
|
7239
|
+
]
|
|
7240
|
+
);
|
|
7241
|
+
if (!activeSceneId) {
|
|
7242
|
+
return /* @__PURE__ */ jsx25(
|
|
7243
|
+
"div",
|
|
7244
|
+
{
|
|
7245
|
+
"data-testid": `no-scene-placeholder-${identity.familyKey}`,
|
|
7246
|
+
className: "flex items-center justify-center py-8",
|
|
7247
|
+
children: /* @__PURE__ */ jsx25(
|
|
7248
|
+
"button",
|
|
7249
|
+
{
|
|
7250
|
+
onClick: () => onSelectScene?.(),
|
|
7251
|
+
className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
|
|
7252
|
+
children: "Select a Scene"
|
|
7253
|
+
}
|
|
7254
|
+
)
|
|
7255
|
+
}
|
|
7256
|
+
);
|
|
7257
|
+
}
|
|
7258
|
+
if (!sceneContext?.hasContract) {
|
|
7259
|
+
return /* @__PURE__ */ jsx25(
|
|
7260
|
+
"div",
|
|
7261
|
+
{
|
|
7262
|
+
"data-testid": `no-contract-placeholder-${identity.familyKey}`,
|
|
7263
|
+
className: "flex items-center justify-center py-8",
|
|
7264
|
+
children: /* @__PURE__ */ jsx25(
|
|
7265
|
+
"button",
|
|
7266
|
+
{
|
|
7267
|
+
onClick: () => onOpenContract?.(),
|
|
7268
|
+
className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
|
|
7269
|
+
children: "Generate a Contract"
|
|
7270
|
+
}
|
|
7271
|
+
)
|
|
7272
|
+
}
|
|
7273
|
+
);
|
|
7274
|
+
}
|
|
7275
|
+
if (features.bulkComposePlaceholders && isComposing) {
|
|
7276
|
+
return /* @__PURE__ */ jsx25("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx25(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
|
|
7277
|
+
}
|
|
7278
|
+
const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
|
|
7279
|
+
if (activePlaceholders.length > 0) {
|
|
7280
|
+
const tracksByDbId = /* @__PURE__ */ new Map();
|
|
7281
|
+
for (const t of tracks) {
|
|
7282
|
+
tracksByDbId.set(t.handle.dbId, t);
|
|
7283
|
+
if (t.handle.id !== t.handle.dbId) {
|
|
7284
|
+
tracksByDbId.set(t.handle.id, t);
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
return /* @__PURE__ */ jsx25("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
|
|
7288
|
+
const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
|
|
7289
|
+
if (loadedTrack) {
|
|
7290
|
+
return /* @__PURE__ */ jsx25(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
|
|
7291
|
+
}
|
|
7292
|
+
return /* @__PURE__ */ jsx25(
|
|
7293
|
+
"div",
|
|
7294
|
+
{
|
|
7295
|
+
"data-testid": "bulk-placeholder-track",
|
|
7296
|
+
className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
|
|
7297
|
+
style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
|
|
7298
|
+
children: /* @__PURE__ */ jsx25(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
|
|
7299
|
+
},
|
|
7300
|
+
ph.id
|
|
7301
|
+
);
|
|
7302
|
+
}) });
|
|
7303
|
+
}
|
|
7304
|
+
const groupCtx = {
|
|
7305
|
+
services: makeServices(),
|
|
7306
|
+
anySolo,
|
|
7307
|
+
supportsMeters,
|
|
7308
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7309
|
+
handlers,
|
|
7310
|
+
renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx25(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
|
|
7311
|
+
setGroupMute,
|
|
7312
|
+
setGroupSolo,
|
|
7313
|
+
deleteGroup
|
|
7314
|
+
};
|
|
7315
|
+
return /* @__PURE__ */ jsxs19("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
|
|
7316
|
+
features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx25(
|
|
7317
|
+
ImportTrackModal,
|
|
7318
|
+
{
|
|
7319
|
+
host,
|
|
7320
|
+
open: importOpen,
|
|
7321
|
+
onClose: () => setImportOpen(false),
|
|
7322
|
+
onImported: () => {
|
|
7323
|
+
void loadTracks(true);
|
|
7324
|
+
},
|
|
7325
|
+
onPortTrack: host.readImportableTrackMidi ? handlePortTrack : void 0,
|
|
7326
|
+
testIdPrefix: `${identity.familyKey}-import`
|
|
7327
|
+
}
|
|
7328
|
+
),
|
|
7329
|
+
features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx25(
|
|
7330
|
+
ImportTrackModal,
|
|
7331
|
+
{
|
|
7332
|
+
host,
|
|
7333
|
+
mode: "sound",
|
|
7334
|
+
open: !!soundImportTarget,
|
|
7335
|
+
title: adapter.sound.importSoundLabel,
|
|
7336
|
+
onClose: () => setSoundImportTarget(null),
|
|
7337
|
+
onImported: () => {
|
|
7338
|
+
},
|
|
7339
|
+
onPick: handleSoundImportPick,
|
|
7340
|
+
testIdPrefix: `${identity.familyKey}-sound-import`
|
|
7341
|
+
}
|
|
7342
|
+
),
|
|
7343
|
+
slots?.modals,
|
|
7344
|
+
canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx25("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx25(
|
|
7345
|
+
TransitionDesigner,
|
|
7346
|
+
{
|
|
7347
|
+
host,
|
|
7348
|
+
fromSceneId: xfFromId,
|
|
7349
|
+
toSceneId: xfToId,
|
|
7350
|
+
transitionSceneId: activeSceneId ?? "",
|
|
7351
|
+
excludeSourceDbIds: [
|
|
7352
|
+
...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
|
|
7353
|
+
...fadesMeta.map((f) => f.meta.sourceTrackDbId)
|
|
7354
|
+
],
|
|
7355
|
+
onCreateCrossfade: transition.handleCreateCrossfade,
|
|
7356
|
+
onCreateFade: transition.handleCreateFade,
|
|
7357
|
+
familyLabel: identity.familyLabel,
|
|
7358
|
+
testIdPrefix: `${identity.familyKey}-transition-designer`
|
|
7359
|
+
}
|
|
7360
|
+
) }),
|
|
7361
|
+
!(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx25("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs19(Fragment6, { children: [
|
|
7362
|
+
panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx25(
|
|
7363
|
+
PanelMasterStrip,
|
|
7364
|
+
{
|
|
7365
|
+
bus: panelBus.bus,
|
|
7366
|
+
availableFx: panelBus.availableFx,
|
|
7367
|
+
fxLoading: panelBus.fxLoading,
|
|
7368
|
+
soloedOut: anySolo && !panelBus.bus.soloed,
|
|
7369
|
+
fxPickerOpen: panelBus.fxPickerOpen,
|
|
7370
|
+
onToggleFxPicker: panelBus.setFxPickerOpen,
|
|
7371
|
+
onRefreshFx: panelBus.refreshFx,
|
|
7372
|
+
onVolumeChange: panelBus.onVolumeChange,
|
|
7373
|
+
onMuteToggle: panelBus.onMuteToggle,
|
|
7374
|
+
onSoloToggle: panelBus.onSoloToggle,
|
|
7375
|
+
onAddFx: panelBus.onAddFx,
|
|
7376
|
+
onRemoveFx: panelBus.onRemoveFx,
|
|
7377
|
+
onToggleFxEnabled: panelBus.onToggleFxEnabled,
|
|
7378
|
+
onShowFxEditor: panelBus.onShowFxEditor
|
|
7379
|
+
}
|
|
7380
|
+
),
|
|
7381
|
+
slots?.beforeRows,
|
|
7382
|
+
resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx25(
|
|
7383
|
+
CrossfadeTrackRow,
|
|
7384
|
+
{
|
|
7385
|
+
accentColor: identity.transitionAccentColor,
|
|
7386
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7387
|
+
sliderPos: pair.sliderPos,
|
|
7388
|
+
origin: {
|
|
7389
|
+
trackId: pair.origin.handle.id,
|
|
7390
|
+
name: pair.origin.handle.name,
|
|
7391
|
+
role: pair.origin.role,
|
|
7392
|
+
sourceName: pair.originSourceName,
|
|
7393
|
+
soundLabel: pair.originSoundLabel,
|
|
7394
|
+
runtimeState: pair.origin.runtimeState
|
|
7395
|
+
},
|
|
7396
|
+
target: {
|
|
7397
|
+
trackId: pair.target.handle.id,
|
|
7398
|
+
name: pair.target.handle.name,
|
|
7399
|
+
role: pair.target.role,
|
|
7400
|
+
sourceName: pair.targetSourceName,
|
|
7401
|
+
soundLabel: pair.targetSoundLabel,
|
|
7402
|
+
runtimeState: pair.target.runtimeState
|
|
7403
|
+
},
|
|
7404
|
+
onMuteToggle: () => transition.handleCrossfadeMute(pair),
|
|
7405
|
+
onSoloToggle: () => transition.handleCrossfadeSolo(pair),
|
|
7406
|
+
onVolumeChange: (slot, vol) => handlers.volumeChange(
|
|
7407
|
+
slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
|
|
7408
|
+
vol
|
|
7409
|
+
),
|
|
7410
|
+
onPanChange: (slot, pan) => handlers.panChange(
|
|
7411
|
+
slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
|
|
7412
|
+
pan
|
|
7413
|
+
),
|
|
7414
|
+
onSliderChange: (pos) => transition.handleCrossfadeSlider(pair, pos),
|
|
7415
|
+
onDelete: () => transition.handleCrossfadeDelete(pair)
|
|
7416
|
+
},
|
|
7417
|
+
pair.groupId
|
|
7418
|
+
)),
|
|
7419
|
+
resolvedFades.map((fade) => /* @__PURE__ */ jsx25(
|
|
7420
|
+
FadeTrackRow,
|
|
7421
|
+
{
|
|
7422
|
+
accentColor: identity.transitionAccentColor,
|
|
7423
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7424
|
+
direction: fade.meta.direction,
|
|
7425
|
+
gesture: fade.meta.gesture,
|
|
7426
|
+
sliderPos: fade.meta.sliderPos,
|
|
7427
|
+
layer: {
|
|
7428
|
+
trackId: fade.track.handle.id,
|
|
7429
|
+
name: fade.track.handle.name,
|
|
7430
|
+
role: fade.track.role,
|
|
7431
|
+
sourceName: fade.meta.sourceName,
|
|
7432
|
+
soundLabel: fade.meta.soundLabel,
|
|
7433
|
+
runtimeState: fade.track.runtimeState
|
|
7434
|
+
},
|
|
7435
|
+
onMuteToggle: () => handlers.muteToggle(fade.track.handle.id),
|
|
7436
|
+
onSoloToggle: () => handlers.soloToggle(fade.track.handle.id),
|
|
7437
|
+
onVolumeChange: (vol) => handlers.volumeChange(fade.track.handle.id, vol),
|
|
7438
|
+
onPanChange: (pan) => handlers.panChange(fade.track.handle.id, pan),
|
|
7439
|
+
onSliderChange: (pos) => transition.handleFadeSlider(fade, pos),
|
|
7440
|
+
onDelete: () => transition.handleFadeDelete(fade)
|
|
7441
|
+
},
|
|
7442
|
+
fade.dbId
|
|
7443
|
+
)),
|
|
7444
|
+
(adapter.groupExtensions ?? []).flatMap(
|
|
7445
|
+
(ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx25(React21.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
|
|
7446
|
+
),
|
|
7447
|
+
tracks.map((track, index) => {
|
|
7448
|
+
if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
|
|
7449
|
+
return null;
|
|
7450
|
+
}
|
|
7451
|
+
return /* @__PURE__ */ jsx25(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
|
|
7452
|
+
}),
|
|
7453
|
+
slots?.afterRows
|
|
7454
|
+
] })),
|
|
7455
|
+
features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
|
|
7456
|
+
const hasAnyMidi = tracks.some((t) => t.hasMidi);
|
|
7457
|
+
const exportDisabled = isExportingMidi || !hasAnyMidi;
|
|
7458
|
+
return /* @__PURE__ */ jsx25("div", { className: "pt-2", children: /* @__PURE__ */ jsx25(
|
|
7459
|
+
"button",
|
|
7460
|
+
{
|
|
7461
|
+
"data-testid": "export-midi-tracks-button",
|
|
7462
|
+
onClick: handleExportMidi,
|
|
7463
|
+
disabled: exportDisabled,
|
|
7464
|
+
title: isExportingMidi ? "Exporting..." : !hasAnyMidi ? "Generate MIDI on at least one track first" : "Export all tracks as a ZIP of .mid files",
|
|
7465
|
+
className: `w-full px-2 py-1.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors ${exportDisabled ? "text-sas-muted/40 border-transparent hover:border-sas-accent cursor-not-allowed" : "text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent"}`,
|
|
7466
|
+
children: isExportingMidi ? "Exporting..." : "Export Tracks"
|
|
7467
|
+
}
|
|
7468
|
+
) });
|
|
7469
|
+
})()
|
|
7470
|
+
] });
|
|
7471
|
+
}
|
|
7472
|
+
|
|
7473
|
+
// src/panel-core/surge-sound-adapter.ts
|
|
7474
|
+
async function getInstrument(host, trackId) {
|
|
7475
|
+
try {
|
|
7476
|
+
const plugins = await host.getTrackPlugins(trackId);
|
|
7477
|
+
const instrument = plugins.find(
|
|
7478
|
+
(p) => !p.name.includes("Volume") && !p.name.includes("Pan") && !p.name.includes("Level")
|
|
7479
|
+
);
|
|
7480
|
+
if (!instrument) return null;
|
|
7481
|
+
return { index: instrument.index, isRaw: !instrument.name.includes("Surge") };
|
|
7482
|
+
} catch {
|
|
7483
|
+
return null;
|
|
7484
|
+
}
|
|
7485
|
+
}
|
|
7486
|
+
function createSurgeSoundAdapter(host, overrides = {}) {
|
|
7487
|
+
const applySound = async (trackId, descriptor) => {
|
|
7488
|
+
const { state, stateType } = descriptor;
|
|
7489
|
+
const inst = await getInstrument(host, trackId);
|
|
7490
|
+
if (!inst) return;
|
|
7491
|
+
if (stateType === "raw") await host.setRawPluginState(trackId, inst.index, state);
|
|
7492
|
+
else await host.setPluginState(trackId, inst.index, state);
|
|
7493
|
+
};
|
|
7494
|
+
return {
|
|
7495
|
+
applySound,
|
|
7496
|
+
captureSoundDescriptor: async (trackId) => {
|
|
7497
|
+
const inst = await getInstrument(host, trackId);
|
|
7498
|
+
if (!inst) return null;
|
|
7499
|
+
const state = inst.isRaw ? await host.getRawPluginState(trackId, inst.index) : await host.getPluginState(trackId, inst.index);
|
|
7500
|
+
return { descriptor: { state, stateType: inst.isRaw ? "raw" : "valuetree" } };
|
|
7501
|
+
},
|
|
7502
|
+
copySnapshot: async (trackId, snap) => {
|
|
7503
|
+
if (snap.kind !== "preset") return "default";
|
|
7504
|
+
await applySound(trackId, { state: snap.state, stateType: snap.stateType });
|
|
7505
|
+
await host.persistTrackPresetState?.(trackId, {
|
|
7506
|
+
state: snap.state,
|
|
7507
|
+
stateType: snap.stateType ?? "valuetree",
|
|
7508
|
+
name: snap.label
|
|
7509
|
+
}).catch(() => {
|
|
7510
|
+
});
|
|
7511
|
+
return snap.label;
|
|
7512
|
+
},
|
|
7513
|
+
descriptorFromSnapshot: (snap) => {
|
|
7514
|
+
const preset = snap;
|
|
7515
|
+
return { state: preset.state, stateType: preset.stateType };
|
|
7516
|
+
},
|
|
7517
|
+
acceptedSnapshotKind: "preset",
|
|
7518
|
+
historyMax: overrides.historyMax ?? 12,
|
|
7519
|
+
importSoundLabel: overrides.importSoundLabel ?? "Import Preset",
|
|
7520
|
+
importNoun: "preset",
|
|
7521
|
+
previousSoundLabel: "Previous preset"
|
|
7522
|
+
};
|
|
7523
|
+
}
|
|
7524
|
+
|
|
4917
7525
|
// src/constants/sdk-version.ts
|
|
4918
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
7526
|
+
var PLUGIN_SDK_VERSION = "2.37.0";
|
|
4919
7527
|
|
|
4920
7528
|
// src/utils/format-concurrent-tracks.ts
|
|
4921
7529
|
function formatConcurrentTracks(ctx) {
|
|
@@ -5081,6 +7689,7 @@ export {
|
|
|
5081
7689
|
FadeTrackRow,
|
|
5082
7690
|
FxToggleBar,
|
|
5083
7691
|
GUTTER_W,
|
|
7692
|
+
GeneratorPanelShell,
|
|
5084
7693
|
ImportTrackModal,
|
|
5085
7694
|
TrackDrawer as InstrumentDrawer,
|
|
5086
7695
|
LevelMeter,
|
|
@@ -5089,6 +7698,7 @@ export {
|
|
|
5089
7698
|
PLUGIN_SDK_VERSION,
|
|
5090
7699
|
PX_PER_BEAT,
|
|
5091
7700
|
PanSlider,
|
|
7701
|
+
PanelMasterStrip,
|
|
5092
7702
|
PianoRollEditor,
|
|
5093
7703
|
PluginError,
|
|
5094
7704
|
RESIZE_HANDLE_PX,
|
|
@@ -5118,6 +7728,7 @@ export {
|
|
|
5118
7728
|
cellToPx,
|
|
5119
7729
|
centerScrollTop,
|
|
5120
7730
|
computePeaks,
|
|
7731
|
+
createSurgeSoundAdapter,
|
|
5121
7732
|
dbIdsFromKeys,
|
|
5122
7733
|
dbToSlider,
|
|
5123
7734
|
defaultFadeGesture,
|
|
@@ -5125,16 +7736,21 @@ export {
|
|
|
5125
7736
|
formatConcurrentTracks,
|
|
5126
7737
|
hashString,
|
|
5127
7738
|
moveItem,
|
|
7739
|
+
newTrackState,
|
|
5128
7740
|
normalizeSlots,
|
|
5129
7741
|
padPair,
|
|
5130
7742
|
padSlots,
|
|
5131
7743
|
parseCrossfadePairs,
|
|
5132
7744
|
parseFades,
|
|
7745
|
+
parseLLMNoteResponse,
|
|
7746
|
+
parseTrackGroups,
|
|
5133
7747
|
pickTopKWeighted,
|
|
5134
7748
|
pitchToName,
|
|
7749
|
+
pluginFxToToggleFx,
|
|
5135
7750
|
pxToCell,
|
|
5136
7751
|
reconcileSlots,
|
|
5137
7752
|
resizeNoteDuration,
|
|
7753
|
+
resolveTrackGroups,
|
|
5138
7754
|
rowKey,
|
|
5139
7755
|
rowType,
|
|
5140
7756
|
scorePromptMatch,
|
|
@@ -5143,14 +7759,18 @@ export {
|
|
|
5143
7759
|
soundIdentity,
|
|
5144
7760
|
synthesizeCuePoints,
|
|
5145
7761
|
tokenizePrompt,
|
|
7762
|
+
trackDataKey,
|
|
5146
7763
|
transposeNotes,
|
|
5147
7764
|
useAnySolo,
|
|
7765
|
+
useGeneratorPanelCore,
|
|
7766
|
+
usePanelBus,
|
|
5148
7767
|
useSceneState,
|
|
5149
7768
|
useSoundHistory,
|
|
5150
7769
|
useTrackLevel,
|
|
5151
7770
|
useTrackLevels,
|
|
5152
7771
|
useTrackMeter,
|
|
5153
7772
|
useTrackReorder,
|
|
7773
|
+
useTransitionOps,
|
|
5154
7774
|
useTransportPlaying
|
|
5155
7775
|
};
|
|
5156
7776
|
//# sourceMappingURL=index.mjs.map
|