@signalsandsorcery/plugin-sdk 2.3.1 → 2.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -7
- package/dist/index.d.mts +2061 -60
- package/dist/index.d.ts +2061 -60
- package/dist/index.js +3559 -798
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3505 -799
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,35 +17,88 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
ConfirmDialog: () => ConfirmDialog,
|
|
34
|
+
CrossfadeModal: () => CrossfadeModal,
|
|
35
|
+
CrossfadeTrackRow: () => CrossfadeTrackRow,
|
|
23
36
|
DB_MAX: () => DB_MAX,
|
|
24
37
|
DB_MIN: () => DB_MIN,
|
|
25
38
|
DEFAULT_FX_CATEGORY_DETAIL: () => DEFAULT_FX_CATEGORY_DETAIL,
|
|
26
39
|
DEFAULT_FX_DRY_WET: () => DEFAULT_FX_DRY_WET,
|
|
40
|
+
DRAG_DEAD_ZONE: () => DRAG_DEAD_ZONE,
|
|
41
|
+
DownloadPackButton: () => DownloadPackButton,
|
|
27
42
|
EMPTY_FX_DETAIL_STATE: () => EMPTY_FX_DETAIL_STATE,
|
|
28
43
|
EMPTY_FX_STATE: () => EMPTY_FX_STATE,
|
|
44
|
+
EQUAL_POWER_GAIN: () => EQUAL_POWER_GAIN,
|
|
29
45
|
FX_CATEGORIES: () => FX_CATEGORIES,
|
|
30
46
|
FX_CHAIN_ORDER: () => FX_CHAIN_ORDER,
|
|
31
47
|
FX_DISPLAY_LABELS: () => FX_DISPLAY_LABELS,
|
|
32
48
|
FX_ENGINE_PLUGIN_NAMES: () => FX_ENGINE_PLUGIN_NAMES,
|
|
33
49
|
FX_PRESET_CONFIGS: () => FX_PRESET_CONFIGS,
|
|
34
50
|
FxToggleBar: () => FxToggleBar,
|
|
35
|
-
|
|
51
|
+
GUTTER_W: () => GUTTER_W,
|
|
52
|
+
ImportTrackModal: () => ImportTrackModal,
|
|
53
|
+
InstrumentDrawer: () => TrackDrawer,
|
|
54
|
+
LevelMeter: () => LevelMeter,
|
|
55
|
+
Modal: () => Modal,
|
|
56
|
+
OffsetScrubber: () => OffsetScrubber,
|
|
36
57
|
PLUGIN_SDK_VERSION: () => PLUGIN_SDK_VERSION,
|
|
58
|
+
PX_PER_BEAT: () => PX_PER_BEAT,
|
|
37
59
|
PanSlider: () => PanSlider,
|
|
60
|
+
PianoRollEditor: () => PianoRollEditor,
|
|
38
61
|
PluginError: () => PluginError,
|
|
62
|
+
RESIZE_HANDLE_PX: () => RESIZE_HANDLE_PX,
|
|
63
|
+
ROW_HEIGHT: () => ROW_HEIGHT,
|
|
39
64
|
SLIDER_UNITY: () => SLIDER_UNITY,
|
|
65
|
+
SamplePackCTACard: () => SamplePackCTACard,
|
|
66
|
+
ScrollingWaveform: () => ScrollingWaveform,
|
|
40
67
|
SorceryProgressBar: () => SorceryProgressBar,
|
|
68
|
+
TrackDrawer: () => TrackDrawer,
|
|
69
|
+
TrackMeterStrip: () => TrackMeterStrip,
|
|
41
70
|
TrackRow: () => TrackRow,
|
|
42
71
|
VolumeSlider: () => VolumeSlider,
|
|
72
|
+
WaveformView: () => WaveformView,
|
|
73
|
+
analyzeWavPeak: () => analyzeWavPeak,
|
|
74
|
+
asCrossfadeMeta: () => asCrossfadeMeta,
|
|
75
|
+
buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
|
|
43
76
|
calculateTimeBasedTarget: () => calculateTimeBasedTarget,
|
|
77
|
+
cellToPx: () => cellToPx,
|
|
78
|
+
centerScrollTop: () => centerScrollTop,
|
|
79
|
+
computePeaks: () => computePeaks,
|
|
44
80
|
dbToSlider: () => dbToSlider,
|
|
81
|
+
drawWaveform: () => drawWaveform,
|
|
82
|
+
formatConcurrentTracks: () => formatConcurrentTracks,
|
|
83
|
+
moveItem: () => moveItem,
|
|
84
|
+
parseCrossfadePairs: () => parseCrossfadePairs,
|
|
85
|
+
pickTopKWeighted: () => pickTopKWeighted,
|
|
86
|
+
pitchToName: () => pitchToName,
|
|
87
|
+
pxToCell: () => pxToCell,
|
|
88
|
+
resizeNoteDuration: () => resizeNoteDuration,
|
|
89
|
+
scorePromptMatch: () => scorePromptMatch,
|
|
45
90
|
sliderToDb: () => sliderToDb,
|
|
46
|
-
|
|
91
|
+
synthesizeCuePoints: () => synthesizeCuePoints,
|
|
92
|
+
tokenizePrompt: () => tokenizePrompt,
|
|
93
|
+
transposeNotes: () => transposeNotes,
|
|
94
|
+
useAnySolo: () => useAnySolo,
|
|
95
|
+
useSceneState: () => useSceneState,
|
|
96
|
+
useSoundHistory: () => useSoundHistory,
|
|
97
|
+
useTrackLevel: () => useTrackLevel,
|
|
98
|
+
useTrackLevels: () => useTrackLevels,
|
|
99
|
+
useTrackMeter: () => useTrackMeter,
|
|
100
|
+
useTrackReorder: () => useTrackReorder,
|
|
101
|
+
useTransportPlaying: () => useTransportPlaying
|
|
47
102
|
});
|
|
48
103
|
module.exports = __toCommonJS(index_exports);
|
|
49
104
|
|
|
@@ -114,414 +169,49 @@ var EMPTY_FX_DETAIL_STATE = {
|
|
|
114
169
|
};
|
|
115
170
|
|
|
116
171
|
// src/components/TrackRow.tsx
|
|
172
|
+
var import_react9 = __toESM(require("react"));
|
|
117
173
|
var import_lucide_react = require("lucide-react");
|
|
118
174
|
|
|
119
|
-
// src/components/
|
|
120
|
-
var import_react = require("react");
|
|
121
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
122
|
-
function InstrumentDrawer({
|
|
123
|
-
instruments,
|
|
124
|
-
currentPluginId,
|
|
125
|
-
isLoading,
|
|
126
|
-
onSelect,
|
|
127
|
-
onRefresh,
|
|
128
|
-
stage = "instruments",
|
|
129
|
-
onShowEditor,
|
|
130
|
-
onBackToInstruments,
|
|
131
|
-
selectedInstrumentName
|
|
132
|
-
}) {
|
|
133
|
-
const [search, setSearch] = (0, import_react.useState)("");
|
|
134
|
-
const SURGE_XT_DEFAULT_ID = "Surge XT";
|
|
135
|
-
const filtered = (0, import_react.useMemo)(() => {
|
|
136
|
-
let all = instruments.filter(
|
|
137
|
-
(i) => i.name !== "Surge XT"
|
|
138
|
-
);
|
|
139
|
-
if (search.trim()) {
|
|
140
|
-
const q = search.toLowerCase();
|
|
141
|
-
all = all.filter(
|
|
142
|
-
(i) => i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
if (currentPluginId) {
|
|
146
|
-
const selectedIdx = all.findIndex((i) => i.pluginId === currentPluginId);
|
|
147
|
-
if (selectedIdx > 0) {
|
|
148
|
-
const [selected] = all.splice(selectedIdx, 1);
|
|
149
|
-
all.unshift(selected);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return all;
|
|
153
|
-
}, [instruments, search, currentPluginId]);
|
|
154
|
-
const isDefaultSelected = currentPluginId === null;
|
|
155
|
-
const isSelected = (pluginId) => {
|
|
156
|
-
return pluginId === currentPluginId;
|
|
157
|
-
};
|
|
158
|
-
if (stage === "editor") {
|
|
159
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
160
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
161
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
162
|
-
"button",
|
|
163
|
-
{
|
|
164
|
-
onClick: () => onBackToInstruments?.(),
|
|
165
|
-
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",
|
|
166
|
-
children: "\u2190 Back"
|
|
167
|
-
}
|
|
168
|
-
),
|
|
169
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
|
|
170
|
-
] }),
|
|
171
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
172
|
-
"button",
|
|
173
|
-
{
|
|
174
|
-
onClick: () => onShowEditor?.(),
|
|
175
|
-
className: "w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors",
|
|
176
|
-
children: "Open Plugin Editor"
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
] });
|
|
180
|
-
}
|
|
181
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
182
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
183
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
184
|
-
"input",
|
|
185
|
-
{
|
|
186
|
-
type: "text",
|
|
187
|
-
value: search,
|
|
188
|
-
onChange: (e) => setSearch(e.target.value),
|
|
189
|
-
placeholder: "Search instruments...",
|
|
190
|
-
className: "sas-input flex-1 px-2 py-1 text-xs"
|
|
191
|
-
}
|
|
192
|
-
),
|
|
193
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
194
|
-
"button",
|
|
195
|
-
{
|
|
196
|
-
onClick: onRefresh,
|
|
197
|
-
disabled: isLoading,
|
|
198
|
-
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",
|
|
199
|
-
title: "Re-scan plugins",
|
|
200
|
-
children: isLoading ? "..." : "Refresh"
|
|
201
|
-
}
|
|
202
|
-
)
|
|
203
|
-
] }),
|
|
204
|
-
isLoading && instruments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
|
|
205
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
206
|
-
"button",
|
|
207
|
-
{
|
|
208
|
-
onClick: () => onSelect(SURGE_XT_DEFAULT_ID),
|
|
209
|
-
className: `flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${isDefaultSelected ? "border-sas-accent bg-sas-accent/20 text-sas-accent" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
210
|
-
title: "Surge XT \u2014 Default instrument",
|
|
211
|
-
children: [
|
|
212
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-xs font-medium truncate w-full", children: [
|
|
213
|
-
isDefaultSelected && "\u2713 ",
|
|
214
|
-
"Surge XT"
|
|
215
|
-
] }),
|
|
216
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
|
|
217
|
-
]
|
|
218
|
-
},
|
|
219
|
-
"__surge-xt-default__"
|
|
220
|
-
),
|
|
221
|
-
filtered.map((inst) => {
|
|
222
|
-
const selected = isSelected(inst.pluginId);
|
|
223
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
224
|
-
"button",
|
|
225
|
-
{
|
|
226
|
-
onClick: () => onSelect(inst.pluginId),
|
|
227
|
-
className: `flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${selected ? "border-sas-accent bg-sas-accent/20 text-sas-accent" : inst.missing ? "border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
228
|
-
title: `${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? " \u2014 MISSING" : ""}`,
|
|
229
|
-
children: [
|
|
230
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-xs font-medium truncate w-full", children: [
|
|
231
|
-
selected && "\u2713 ",
|
|
232
|
-
inst.name
|
|
233
|
-
] }),
|
|
234
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
|
|
235
|
-
]
|
|
236
|
-
},
|
|
237
|
-
inst.pluginId
|
|
238
|
-
);
|
|
239
|
-
}),
|
|
240
|
-
filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "col-span-2 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No other plugins found" })
|
|
241
|
-
] })
|
|
242
|
-
] });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// src/components/VolumeSlider.tsx
|
|
175
|
+
// src/components/TrackDrawer.tsx
|
|
246
176
|
var import_react2 = require("react");
|
|
247
177
|
|
|
248
|
-
// src/
|
|
249
|
-
var
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// src/components/VolumeSlider.tsx
|
|
268
|
-
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
269
|
-
function formatDb(value) {
|
|
270
|
-
const db = sliderToDb(value);
|
|
271
|
-
if (db <= -60) return "-\u221E dB";
|
|
272
|
-
const sign = db >= 0 ? "+" : "";
|
|
273
|
-
return `${sign}${db.toFixed(1)} dB`;
|
|
274
|
-
}
|
|
275
|
-
function useDebouncedCallback(callback, delay) {
|
|
276
|
-
const timeoutRef = (0, import_react2.useRef)(null);
|
|
277
|
-
const callbackRef = (0, import_react2.useRef)(callback);
|
|
278
|
-
(0, import_react2.useEffect)(() => {
|
|
279
|
-
callbackRef.current = callback;
|
|
280
|
-
}, [callback]);
|
|
281
|
-
const debouncedCallback = (0, import_react2.useCallback)(
|
|
282
|
-
(...args) => {
|
|
283
|
-
if (timeoutRef.current) {
|
|
284
|
-
clearTimeout(timeoutRef.current);
|
|
178
|
+
// src/constants/fx-presets.ts
|
|
179
|
+
var EQ_PRESETS = {
|
|
180
|
+
presets: [
|
|
181
|
+
{
|
|
182
|
+
name: "The Smiley",
|
|
183
|
+
shortLabel: "SM",
|
|
184
|
+
params: {
|
|
185
|
+
"Low-shelf freq": 80,
|
|
186
|
+
"Low-shelf gain": 4,
|
|
187
|
+
"Low-shelf Q": 0.5,
|
|
188
|
+
"Mid freq 1": 500,
|
|
189
|
+
"Mid gain 1": -3,
|
|
190
|
+
"Mid Q 1": 0.7,
|
|
191
|
+
"Mid freq 2": 2e3,
|
|
192
|
+
"Mid gain 2": -2,
|
|
193
|
+
"Mid Q 2": 0.7,
|
|
194
|
+
"High-shelf freq": 12e3,
|
|
195
|
+
"High-shelf gain": 4,
|
|
196
|
+
"High-shelf Q": 0.5
|
|
285
197
|
}
|
|
286
|
-
timeoutRef.current = setTimeout(() => {
|
|
287
|
-
callbackRef.current(...args);
|
|
288
|
-
}, delay);
|
|
289
198
|
},
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}) => {
|
|
307
|
-
const [localValue, setLocalValue] = (0, import_react2.useState)(value);
|
|
308
|
-
const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
|
|
309
|
-
(0, import_react2.useEffect)(() => {
|
|
310
|
-
if (!isDragging) {
|
|
311
|
-
setLocalValue(value);
|
|
312
|
-
}
|
|
313
|
-
}, [value, isDragging]);
|
|
314
|
-
const debouncedOnChange = useDebouncedCallback(onChange, 50);
|
|
315
|
-
const handleChange = (0, import_react2.useCallback)(
|
|
316
|
-
(e) => {
|
|
317
|
-
const newValue = parseFloat(e.target.value);
|
|
318
|
-
setLocalValue(newValue);
|
|
319
|
-
debouncedOnChange(newValue);
|
|
320
|
-
},
|
|
321
|
-
[debouncedOnChange]
|
|
322
|
-
);
|
|
323
|
-
const handleMouseDown = (0, import_react2.useCallback)(() => {
|
|
324
|
-
setIsDragging(true);
|
|
325
|
-
}, []);
|
|
326
|
-
const handleMouseUp = (0, import_react2.useCallback)(() => {
|
|
327
|
-
setIsDragging(false);
|
|
328
|
-
onChange(localValue);
|
|
329
|
-
}, [localValue, onChange]);
|
|
330
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
331
|
-
"div",
|
|
332
|
-
{
|
|
333
|
-
className: `flex items-center ${className}`,
|
|
334
|
-
title: `Volume: ${formatDb(localValue)}`,
|
|
335
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
336
|
-
"input",
|
|
337
|
-
{
|
|
338
|
-
type: "range",
|
|
339
|
-
min: "0",
|
|
340
|
-
max: "1",
|
|
341
|
-
step: "0.01",
|
|
342
|
-
value: localValue,
|
|
343
|
-
onChange: handleChange,
|
|
344
|
-
onMouseDown: handleMouseDown,
|
|
345
|
-
onMouseUp: handleMouseUp,
|
|
346
|
-
onTouchStart: handleMouseDown,
|
|
347
|
-
onTouchEnd: handleMouseUp,
|
|
348
|
-
disabled,
|
|
349
|
-
className: `
|
|
350
|
-
w-full h-1.5 rounded-full appearance-none cursor-pointer
|
|
351
|
-
bg-gray-700
|
|
352
|
-
disabled:opacity-50 disabled:cursor-not-allowed
|
|
353
|
-
[&::-webkit-slider-thumb]:appearance-none
|
|
354
|
-
[&::-webkit-slider-thumb]:w-3
|
|
355
|
-
[&::-webkit-slider-thumb]:h-3
|
|
356
|
-
[&::-webkit-slider-thumb]:rounded-full
|
|
357
|
-
[&::-webkit-slider-thumb]:bg-sas-accent
|
|
358
|
-
[&::-webkit-slider-thumb]:cursor-pointer
|
|
359
|
-
[&::-webkit-slider-thumb]:transition-transform
|
|
360
|
-
[&::-webkit-slider-thumb]:hover:scale-110
|
|
361
|
-
[&::-moz-range-thumb]:w-3
|
|
362
|
-
[&::-moz-range-thumb]:h-3
|
|
363
|
-
[&::-moz-range-thumb]:rounded-full
|
|
364
|
-
[&::-moz-range-thumb]:bg-sas-accent
|
|
365
|
-
[&::-moz-range-thumb]:border-0
|
|
366
|
-
[&::-moz-range-thumb]:cursor-pointer
|
|
367
|
-
`
|
|
368
|
-
}
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
);
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
// src/components/PanSlider.tsx
|
|
375
|
-
var import_react3 = require("react");
|
|
376
|
-
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
377
|
-
function toPanDisplay(value) {
|
|
378
|
-
if (Math.abs(value) < 0.02) {
|
|
379
|
-
return "Center";
|
|
380
|
-
}
|
|
381
|
-
const percent = Math.abs(Math.round(value * 100));
|
|
382
|
-
return value < 0 ? `L${percent}` : `R${percent}`;
|
|
383
|
-
}
|
|
384
|
-
function useDebouncedCallback2(callback, delay) {
|
|
385
|
-
const timeoutRef = (0, import_react3.useRef)(null);
|
|
386
|
-
const callbackRef = (0, import_react3.useRef)(callback);
|
|
387
|
-
(0, import_react3.useEffect)(() => {
|
|
388
|
-
callbackRef.current = callback;
|
|
389
|
-
}, [callback]);
|
|
390
|
-
const debouncedCallback = (0, import_react3.useCallback)(
|
|
391
|
-
(...args) => {
|
|
392
|
-
if (timeoutRef.current) {
|
|
393
|
-
clearTimeout(timeoutRef.current);
|
|
394
|
-
}
|
|
395
|
-
timeoutRef.current = setTimeout(() => {
|
|
396
|
-
callbackRef.current(...args);
|
|
397
|
-
}, delay);
|
|
398
|
-
},
|
|
399
|
-
[delay]
|
|
400
|
-
);
|
|
401
|
-
(0, import_react3.useEffect)(() => {
|
|
402
|
-
return () => {
|
|
403
|
-
if (timeoutRef.current) {
|
|
404
|
-
clearTimeout(timeoutRef.current);
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
}, []);
|
|
408
|
-
return debouncedCallback;
|
|
409
|
-
}
|
|
410
|
-
var PanSlider = ({
|
|
411
|
-
value,
|
|
412
|
-
onChange,
|
|
413
|
-
disabled = false,
|
|
414
|
-
className = ""
|
|
415
|
-
}) => {
|
|
416
|
-
const [localValue, setLocalValue] = (0, import_react3.useState)(value);
|
|
417
|
-
const [isDragging, setIsDragging] = (0, import_react3.useState)(false);
|
|
418
|
-
(0, import_react3.useEffect)(() => {
|
|
419
|
-
if (!isDragging) {
|
|
420
|
-
setLocalValue(value);
|
|
421
|
-
}
|
|
422
|
-
}, [value, isDragging]);
|
|
423
|
-
const debouncedOnChange = useDebouncedCallback2(onChange, 50);
|
|
424
|
-
const handleChange = (0, import_react3.useCallback)(
|
|
425
|
-
(e) => {
|
|
426
|
-
const newValue = parseFloat(e.target.value);
|
|
427
|
-
setLocalValue(newValue);
|
|
428
|
-
debouncedOnChange(newValue);
|
|
429
|
-
},
|
|
430
|
-
[debouncedOnChange]
|
|
431
|
-
);
|
|
432
|
-
const handleMouseDown = (0, import_react3.useCallback)(() => {
|
|
433
|
-
setIsDragging(true);
|
|
434
|
-
}, []);
|
|
435
|
-
const handleMouseUp = (0, import_react3.useCallback)(() => {
|
|
436
|
-
setIsDragging(false);
|
|
437
|
-
onChange(localValue);
|
|
438
|
-
}, [localValue, onChange]);
|
|
439
|
-
const handleDoubleClick = (0, import_react3.useCallback)(() => {
|
|
440
|
-
setLocalValue(0);
|
|
441
|
-
onChange(0);
|
|
442
|
-
}, [onChange]);
|
|
443
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
444
|
-
"div",
|
|
445
|
-
{
|
|
446
|
-
className: `flex items-center ${className}`,
|
|
447
|
-
title: `Pan: ${toPanDisplay(localValue)}`,
|
|
448
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
449
|
-
"input",
|
|
450
|
-
{
|
|
451
|
-
type: "range",
|
|
452
|
-
min: "-1",
|
|
453
|
-
max: "1",
|
|
454
|
-
step: "0.01",
|
|
455
|
-
value: localValue,
|
|
456
|
-
onChange: handleChange,
|
|
457
|
-
onMouseDown: handleMouseDown,
|
|
458
|
-
onMouseUp: handleMouseUp,
|
|
459
|
-
onTouchStart: handleMouseDown,
|
|
460
|
-
onTouchEnd: handleMouseUp,
|
|
461
|
-
onDoubleClick: handleDoubleClick,
|
|
462
|
-
disabled,
|
|
463
|
-
className: `
|
|
464
|
-
w-full h-1.5 rounded-full appearance-none cursor-pointer
|
|
465
|
-
bg-gray-700
|
|
466
|
-
disabled:opacity-50 disabled:cursor-not-allowed
|
|
467
|
-
[&::-webkit-slider-thumb]:appearance-none
|
|
468
|
-
[&::-webkit-slider-thumb]:w-3
|
|
469
|
-
[&::-webkit-slider-thumb]:h-3
|
|
470
|
-
[&::-webkit-slider-thumb]:rounded-full
|
|
471
|
-
[&::-webkit-slider-thumb]:bg-sas-accent
|
|
472
|
-
[&::-webkit-slider-thumb]:cursor-pointer
|
|
473
|
-
[&::-webkit-slider-thumb]:transition-transform
|
|
474
|
-
[&::-webkit-slider-thumb]:hover:scale-110
|
|
475
|
-
[&::-moz-range-thumb]:w-3
|
|
476
|
-
[&::-moz-range-thumb]:h-3
|
|
477
|
-
[&::-moz-range-thumb]:rounded-full
|
|
478
|
-
[&::-moz-range-thumb]:bg-sas-accent
|
|
479
|
-
[&::-moz-range-thumb]:border-0
|
|
480
|
-
[&::-moz-range-thumb]:cursor-pointer
|
|
481
|
-
`
|
|
482
|
-
}
|
|
483
|
-
)
|
|
484
|
-
}
|
|
485
|
-
);
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
// src/constants/fx-presets.ts
|
|
489
|
-
var EQ_PRESETS = {
|
|
490
|
-
presets: [
|
|
491
|
-
{
|
|
492
|
-
name: "The Smiley",
|
|
493
|
-
shortLabel: "SM",
|
|
494
|
-
params: {
|
|
495
|
-
"Low-shelf freq": 80,
|
|
496
|
-
"Low-shelf gain": 4,
|
|
497
|
-
"Low-shelf Q": 0.5,
|
|
498
|
-
"Mid freq 1": 500,
|
|
499
|
-
"Mid gain 1": -3,
|
|
500
|
-
"Mid Q 1": 0.7,
|
|
501
|
-
"Mid freq 2": 2e3,
|
|
502
|
-
"Mid gain 2": -2,
|
|
503
|
-
"Mid Q 2": 0.7,
|
|
504
|
-
"High-shelf freq": 12e3,
|
|
505
|
-
"High-shelf gain": 4,
|
|
506
|
-
"High-shelf Q": 0.5
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
name: "Telephone",
|
|
511
|
-
shortLabel: "TP",
|
|
512
|
-
params: {
|
|
513
|
-
"Low-shelf freq": 400,
|
|
514
|
-
"Low-shelf gain": -20,
|
|
515
|
-
"Low-shelf Q": 1,
|
|
516
|
-
"Mid freq 1": 1e3,
|
|
517
|
-
"Mid gain 1": 5,
|
|
518
|
-
"Mid Q 1": 2,
|
|
519
|
-
"Mid freq 2": 3e3,
|
|
520
|
-
"Mid gain 2": -5,
|
|
521
|
-
"Mid Q 2": 1,
|
|
522
|
-
"High-shelf freq": 5e3,
|
|
523
|
-
"High-shelf gain": -20,
|
|
524
|
-
"High-shelf Q": 1
|
|
199
|
+
{
|
|
200
|
+
name: "Telephone",
|
|
201
|
+
shortLabel: "TP",
|
|
202
|
+
params: {
|
|
203
|
+
"Low-shelf freq": 400,
|
|
204
|
+
"Low-shelf gain": -20,
|
|
205
|
+
"Low-shelf Q": 1,
|
|
206
|
+
"Mid freq 1": 1e3,
|
|
207
|
+
"Mid gain 1": 5,
|
|
208
|
+
"Mid Q 1": 2,
|
|
209
|
+
"Mid freq 2": 3e3,
|
|
210
|
+
"Mid gain 2": -5,
|
|
211
|
+
"Mid Q 2": 1,
|
|
212
|
+
"High-shelf freq": 5e3,
|
|
213
|
+
"High-shelf gain": -20,
|
|
214
|
+
"High-shelf Q": 1
|
|
525
215
|
}
|
|
526
216
|
},
|
|
527
217
|
{
|
|
@@ -764,7 +454,7 @@ var FX_PRESET_CONFIGS = {
|
|
|
764
454
|
};
|
|
765
455
|
|
|
766
456
|
// src/components/FxToggleBar.tsx
|
|
767
|
-
var
|
|
457
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
768
458
|
var FX_COLORS = {
|
|
769
459
|
eq: "bg-blue-500",
|
|
770
460
|
compressor: "bg-orange-500",
|
|
@@ -781,14 +471,14 @@ var FxToggleBar = ({
|
|
|
781
471
|
onDryWetChange,
|
|
782
472
|
disabled = false
|
|
783
473
|
}) => {
|
|
784
|
-
return /* @__PURE__ */ (0,
|
|
474
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-col gap-1", "data-testid": "fx-toggle-bar", children: FX_CATEGORIES.map((category) => {
|
|
785
475
|
const detail = fxState[category];
|
|
786
476
|
const isActive = detail.enabled;
|
|
787
477
|
const label = FX_DISPLAY_LABELS[category];
|
|
788
478
|
const activeColor = FX_COLORS[category];
|
|
789
479
|
const config = FX_PRESET_CONFIGS[category];
|
|
790
|
-
return /* @__PURE__ */ (0,
|
|
791
|
-
/* @__PURE__ */ (0,
|
|
480
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-0.5", children: [
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
792
482
|
"button",
|
|
793
483
|
{
|
|
794
484
|
"data-testid": `fx-toggle-${category}`,
|
|
@@ -799,7 +489,7 @@ var FxToggleBar = ({
|
|
|
799
489
|
children: label
|
|
800
490
|
}
|
|
801
491
|
),
|
|
802
|
-
config.presets.map((preset, idx) => /* @__PURE__ */ (0,
|
|
492
|
+
config.presets.map((preset, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
803
493
|
"button",
|
|
804
494
|
{
|
|
805
495
|
"data-testid": `fx-preset-${category}-${idx}`,
|
|
@@ -811,7 +501,7 @@ var FxToggleBar = ({
|
|
|
811
501
|
},
|
|
812
502
|
idx
|
|
813
503
|
)),
|
|
814
|
-
/* @__PURE__ */ (0,
|
|
504
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
815
505
|
"input",
|
|
816
506
|
{
|
|
817
507
|
type: "range",
|
|
@@ -825,7 +515,7 @@ var FxToggleBar = ({
|
|
|
825
515
|
title: `Dry/Wet: ${Math.round(detail.dryWet * 100)}%`
|
|
826
516
|
}
|
|
827
517
|
),
|
|
828
|
-
/* @__PURE__ */ (0,
|
|
518
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-[8px] text-sas-muted/50 w-6 text-right flex-shrink-0", children: [
|
|
829
519
|
Math.round(detail.dryWet * 100),
|
|
830
520
|
"%"
|
|
831
521
|
] })
|
|
@@ -833,443 +523,3113 @@ var FxToggleBar = ({
|
|
|
833
523
|
}) });
|
|
834
524
|
};
|
|
835
525
|
|
|
836
|
-
// src/components/
|
|
837
|
-
var
|
|
838
|
-
var
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
526
|
+
// src/components/PianoRollEditor.tsx
|
|
527
|
+
var import_react = require("react");
|
|
528
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
529
|
+
var PX_PER_BEAT = 24;
|
|
530
|
+
var ROW_HEIGHT = 12;
|
|
531
|
+
var GUTTER_W = 28;
|
|
532
|
+
var DRAG_DEAD_ZONE = 4;
|
|
533
|
+
var RESIZE_HANDLE_PX = 6;
|
|
534
|
+
var SCROLL_MAX_H = 150;
|
|
535
|
+
var NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
536
|
+
var BLACK_KEYS = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
|
|
537
|
+
var SNAP_LABELS = {
|
|
538
|
+
"2": "1/2",
|
|
539
|
+
"1": "1/4",
|
|
540
|
+
"0.5": "1/8",
|
|
541
|
+
"0.25": "1/16",
|
|
542
|
+
"0.125": "1/32"
|
|
543
|
+
};
|
|
544
|
+
function clamp(v, lo, hi) {
|
|
545
|
+
return Math.max(lo, Math.min(hi, v));
|
|
546
|
+
}
|
|
547
|
+
function snapLabel(s) {
|
|
548
|
+
return SNAP_LABELS[String(s)] ?? `${s}`;
|
|
549
|
+
}
|
|
550
|
+
function pitchToName(pitch) {
|
|
551
|
+
const name = NOTE_NAMES[(pitch % 12 + 12) % 12];
|
|
552
|
+
const octave = Math.floor(pitch / 12) - 1;
|
|
553
|
+
return `${name}${octave}`;
|
|
554
|
+
}
|
|
555
|
+
function cellToPx(pitch, startBeat, hi) {
|
|
556
|
+
return { left: startBeat * PX_PER_BEAT, top: (hi - pitch) * ROW_HEIGHT };
|
|
557
|
+
}
|
|
558
|
+
function pxToCell(localX, localY, hi, snap, bars, beatsPerBar) {
|
|
559
|
+
const totalBeats = bars * beatsPerBar;
|
|
560
|
+
const pitch = clamp(hi - Math.floor(localY / ROW_HEIGHT), 0, 127);
|
|
561
|
+
const rawBeat = localX / PX_PER_BEAT;
|
|
562
|
+
const snapped = Math.round(rawBeat / snap) * snap;
|
|
563
|
+
const startBeat = clamp(snapped, 0, Math.max(0, totalBeats - snap));
|
|
564
|
+
return { pitch, startBeat };
|
|
565
|
+
}
|
|
566
|
+
function resizeNoteDuration(startBeat, localX, snap, bars, beatsPerBar) {
|
|
567
|
+
const totalBeats = bars * beatsPerBar;
|
|
568
|
+
const snappedEnd = Math.round(localX / PX_PER_BEAT / snap) * snap;
|
|
569
|
+
const end = clamp(snappedEnd, startBeat + snap, totalBeats);
|
|
570
|
+
return end - startBeat;
|
|
571
|
+
}
|
|
572
|
+
function centerScrollTop(pitches, hi, rowCount, viewportH) {
|
|
573
|
+
if (pitches.length === 0) return 0;
|
|
574
|
+
const sorted = [...pitches].sort((a, b) => a - b);
|
|
575
|
+
const mid = Math.floor(sorted.length / 2);
|
|
576
|
+
const median = sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
577
|
+
const medianRowCenterPx = (hi - median) * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
578
|
+
const maxScroll = Math.max(0, rowCount * ROW_HEIGHT - viewportH);
|
|
579
|
+
return clamp(medianRowCenterPx - viewportH / 2, 0, maxScroll);
|
|
580
|
+
}
|
|
581
|
+
function transposeNotes(notes, semitones) {
|
|
582
|
+
return notes.map((n) => ({ ...n, pitch: clamp(n.pitch + semitones, 0, 127) }));
|
|
583
|
+
}
|
|
584
|
+
function PianoRollEditor({
|
|
585
|
+
notes,
|
|
586
|
+
onChange,
|
|
587
|
+
bars,
|
|
588
|
+
bpm,
|
|
589
|
+
beatsPerBar = 4,
|
|
590
|
+
snap = 0.25,
|
|
591
|
+
snapOptions = [1, 0.5, 0.25],
|
|
592
|
+
onSnapChange,
|
|
593
|
+
minPitch = 36,
|
|
594
|
+
maxPitch = 84,
|
|
595
|
+
autoFit = true,
|
|
596
|
+
onAuditionNote,
|
|
597
|
+
defaultVelocity = 100,
|
|
598
|
+
disabled = false,
|
|
599
|
+
className,
|
|
600
|
+
testId = "sdk-piano-roll"
|
|
601
|
+
}) {
|
|
602
|
+
const [snapState, setSnapState] = (0, import_react.useState)(snap);
|
|
603
|
+
const gridRef = (0, import_react.useRef)(null);
|
|
604
|
+
const scrollRef = (0, import_react.useRef)(null);
|
|
605
|
+
const dragRef = (0, import_react.useRef)(null);
|
|
606
|
+
const didCenterRef = (0, import_react.useRef)(false);
|
|
607
|
+
const { lo, hi } = (0, import_react.useMemo)(() => {
|
|
608
|
+
if (autoFit && notes.length > 0) {
|
|
609
|
+
const ps = notes.map((n) => n.pitch);
|
|
610
|
+
return {
|
|
611
|
+
lo: Math.max(0, Math.min(minPitch, Math.min(...ps) - 2)),
|
|
612
|
+
hi: Math.min(127, Math.max(maxPitch, Math.max(...ps) + 2))
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return { lo: minPitch, hi: maxPitch };
|
|
616
|
+
}, [autoFit, notes, minPitch, maxPitch]);
|
|
617
|
+
const rowCount = hi - lo + 1;
|
|
618
|
+
const totalBeats = bars * beatsPerBar;
|
|
619
|
+
const gridWidth = totalBeats * PX_PER_BEAT;
|
|
620
|
+
const gridHeight = rowCount * ROW_HEIGHT;
|
|
621
|
+
const stateRef = (0, import_react.useRef)({
|
|
622
|
+
notes,
|
|
623
|
+
onChange,
|
|
624
|
+
snapState,
|
|
625
|
+
hi,
|
|
626
|
+
bars,
|
|
627
|
+
beatsPerBar,
|
|
628
|
+
defaultVelocity,
|
|
629
|
+
bpm,
|
|
630
|
+
onAuditionNote,
|
|
631
|
+
disabled
|
|
632
|
+
});
|
|
633
|
+
stateRef.current = {
|
|
634
|
+
notes,
|
|
635
|
+
onChange,
|
|
636
|
+
snapState,
|
|
637
|
+
hi,
|
|
638
|
+
bars,
|
|
639
|
+
beatsPerBar,
|
|
640
|
+
defaultVelocity,
|
|
641
|
+
bpm,
|
|
642
|
+
onAuditionNote,
|
|
643
|
+
disabled
|
|
644
|
+
};
|
|
645
|
+
const localCoords = (0, import_react.useCallback)((clientX, clientY) => {
|
|
646
|
+
const rect = gridRef.current?.getBoundingClientRect();
|
|
647
|
+
return { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };
|
|
648
|
+
}, []);
|
|
649
|
+
const handlePointerDown = (0, import_react.useCallback)((e) => {
|
|
650
|
+
if (stateRef.current.disabled) return;
|
|
651
|
+
const target = e.target;
|
|
652
|
+
const noteEl = target.closest('[data-testid="sdk-pr-note"]');
|
|
653
|
+
const idxAttr = noteEl?.getAttribute("data-index");
|
|
654
|
+
const onResizeHandle = idxAttr != null && target.closest("[data-resize-handle]") != null;
|
|
655
|
+
dragRef.current = {
|
|
656
|
+
mode: idxAttr == null ? "pending-add" : onResizeHandle ? "pending-resize" : "pending-note",
|
|
657
|
+
index: idxAttr != null ? Number(idxAttr) : -1,
|
|
658
|
+
startX: e.clientX,
|
|
659
|
+
startY: e.clientY
|
|
660
|
+
};
|
|
661
|
+
try {
|
|
662
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}, []);
|
|
666
|
+
const handlePointerMove = (0, import_react.useCallback)((e) => {
|
|
667
|
+
const drag = dragRef.current;
|
|
668
|
+
if (!drag) return;
|
|
669
|
+
const dist = Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY);
|
|
670
|
+
if (dist > DRAG_DEAD_ZONE) {
|
|
671
|
+
if (drag.mode === "pending-note") drag.mode = "drag";
|
|
672
|
+
else if (drag.mode === "pending-resize") drag.mode = "resize";
|
|
673
|
+
}
|
|
674
|
+
const s = stateRef.current;
|
|
675
|
+
const { x, y } = localCoords(e.clientX, e.clientY);
|
|
676
|
+
if (drag.mode === "resize") {
|
|
677
|
+
const note = s.notes[drag.index];
|
|
678
|
+
if (!note) return;
|
|
679
|
+
const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar);
|
|
680
|
+
if (durationBeats === note.durationBeats) return;
|
|
681
|
+
const next2 = s.notes.map((n, i) => i === drag.index ? { ...n, durationBeats } : n);
|
|
682
|
+
s.onChange(next2);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (drag.mode !== "drag") return;
|
|
686
|
+
const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);
|
|
687
|
+
const next = s.notes.map((n, i) => i === drag.index ? { ...n, pitch, startBeat } : n);
|
|
688
|
+
s.onChange(next);
|
|
689
|
+
}, [localCoords]);
|
|
690
|
+
const handlePointerUp = (0, import_react.useCallback)((e) => {
|
|
691
|
+
const drag = dragRef.current;
|
|
692
|
+
dragRef.current = null;
|
|
693
|
+
if (!drag) return;
|
|
694
|
+
const s = stateRef.current;
|
|
695
|
+
if (s.disabled) return;
|
|
696
|
+
if (drag.mode === "pending-note" || drag.mode === "pending-resize") {
|
|
697
|
+
s.onChange(s.notes.filter((_, i) => i !== drag.index));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (drag.mode === "pending-add") {
|
|
701
|
+
const { x, y } = localCoords(e.clientX, e.clientY);
|
|
702
|
+
const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);
|
|
703
|
+
const note = {
|
|
704
|
+
pitch,
|
|
705
|
+
startBeat,
|
|
706
|
+
durationBeats: s.snapState,
|
|
707
|
+
velocity: s.defaultVelocity,
|
|
708
|
+
channel: 0
|
|
709
|
+
};
|
|
710
|
+
s.onChange([...s.notes, note]);
|
|
711
|
+
s.onAuditionNote?.(pitch, s.defaultVelocity, Math.max(1, s.snapState * (60 / s.bpm) * 1e3));
|
|
712
|
+
}
|
|
713
|
+
}, [localCoords]);
|
|
714
|
+
const handlePointerCancel = (0, import_react.useCallback)(() => {
|
|
715
|
+
dragRef.current = null;
|
|
716
|
+
}, []);
|
|
717
|
+
const handleOctave = (0, import_react.useCallback)((delta) => {
|
|
718
|
+
const s = stateRef.current;
|
|
719
|
+
if (s.disabled) return;
|
|
720
|
+
didCenterRef.current = false;
|
|
721
|
+
s.onChange(transposeNotes(s.notes, delta));
|
|
722
|
+
}, []);
|
|
723
|
+
const handleSnapChange = (0, import_react.useCallback)((e) => {
|
|
724
|
+
const v = Number(e.target.value);
|
|
725
|
+
setSnapState(v);
|
|
726
|
+
onSnapChange?.(v);
|
|
727
|
+
}, [onSnapChange]);
|
|
728
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
729
|
+
const el = scrollRef.current;
|
|
730
|
+
if (!el) return;
|
|
731
|
+
if (notes.length === 0) {
|
|
732
|
+
didCenterRef.current = false;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (didCenterRef.current || dragRef.current) return;
|
|
736
|
+
didCenterRef.current = true;
|
|
737
|
+
const viewportH = el.clientHeight || SCROLL_MAX_H;
|
|
738
|
+
el.scrollTop = centerScrollTop(
|
|
739
|
+
notes.map((n) => n.pitch),
|
|
740
|
+
hi,
|
|
741
|
+
rowCount,
|
|
742
|
+
viewportH
|
|
743
|
+
);
|
|
744
|
+
}, [notes, hi, rowCount]);
|
|
745
|
+
const rows = (0, import_react.useMemo)(() => {
|
|
746
|
+
const out = [];
|
|
747
|
+
for (let p = hi; p >= lo; p--) out.push(p);
|
|
748
|
+
return out;
|
|
749
|
+
}, [hi, lo]);
|
|
750
|
+
const gridBg = (0, import_react.useMemo)(() => {
|
|
751
|
+
const beatPx = PX_PER_BEAT;
|
|
752
|
+
const barPx = PX_PER_BEAT * beatsPerBar;
|
|
753
|
+
return [
|
|
754
|
+
`repeating-linear-gradient(to right, transparent 0 ${beatPx - 1}px, rgba(255,255,255,0.06) ${beatPx - 1}px ${beatPx}px)`,
|
|
755
|
+
`repeating-linear-gradient(to right, transparent 0 ${barPx - 1}px, rgba(255,255,255,0.16) ${barPx - 1}px ${barPx}px)`,
|
|
756
|
+
`repeating-linear-gradient(to bottom, transparent 0 ${ROW_HEIGHT - 1}px, rgba(255,255,255,0.04) ${ROW_HEIGHT - 1}px ${ROW_HEIGHT}px)`
|
|
757
|
+
].join(", ");
|
|
758
|
+
}, [beatsPerBar]);
|
|
759
|
+
const octaveDisabled = disabled || notes.length === 0;
|
|
760
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `flex flex-col gap-1 ${className ?? ""}`, "data-testid": testId, children: [
|
|
761
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-1", "data-testid": "sdk-pr-toolbar", children: [
|
|
762
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
763
|
+
"button",
|
|
764
|
+
{
|
|
765
|
+
type: "button",
|
|
766
|
+
"data-testid": "sdk-pr-octave-down",
|
|
767
|
+
disabled: octaveDisabled,
|
|
768
|
+
onClick: () => handleOctave(-12),
|
|
769
|
+
className: "px-1.5 py-0.5 text-[10px] rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-40",
|
|
770
|
+
title: "Octave down (\u221212 semitones)",
|
|
771
|
+
children: "Oct \u2212"
|
|
772
|
+
}
|
|
773
|
+
),
|
|
774
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
775
|
+
"button",
|
|
776
|
+
{
|
|
777
|
+
type: "button",
|
|
778
|
+
"data-testid": "sdk-pr-octave-up",
|
|
779
|
+
disabled: octaveDisabled,
|
|
780
|
+
onClick: () => handleOctave(12),
|
|
781
|
+
className: "px-1.5 py-0.5 text-[10px] rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-40",
|
|
782
|
+
title: "Octave up (+12 semitones)",
|
|
783
|
+
children: "Oct +"
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "flex items-center gap-1 text-[10px] text-sas-muted/70 ml-1", children: [
|
|
787
|
+
"Snap",
|
|
788
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
789
|
+
"select",
|
|
790
|
+
{
|
|
791
|
+
"data-testid": "sdk-pr-snap",
|
|
792
|
+
value: snapState,
|
|
793
|
+
disabled,
|
|
794
|
+
onChange: handleSnapChange,
|
|
795
|
+
className: "sas-input px-1 py-0.5 text-[10px]",
|
|
796
|
+
children: snapOptions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: s, children: snapLabel(s) }, s))
|
|
797
|
+
}
|
|
798
|
+
)
|
|
799
|
+
] }),
|
|
800
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "text-[10px] text-sas-muted/60 ml-auto", "data-testid": "sdk-pr-note-count", children: [
|
|
801
|
+
notes.length,
|
|
802
|
+
" ",
|
|
803
|
+
notes.length === 1 ? "note" : "notes"
|
|
804
|
+
] })
|
|
805
|
+
] }),
|
|
806
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
807
|
+
"div",
|
|
808
|
+
{
|
|
809
|
+
ref: scrollRef,
|
|
810
|
+
className: "overflow-auto border border-sas-border rounded-sm bg-sas-bg",
|
|
811
|
+
style: { maxHeight: SCROLL_MAX_H },
|
|
812
|
+
"data-testid": "sdk-pr-scroll",
|
|
813
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex", style: { width: GUTTER_W + gridWidth }, children: [
|
|
814
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
815
|
+
"div",
|
|
816
|
+
{
|
|
817
|
+
"data-testid": "sdk-pr-gutter",
|
|
818
|
+
className: "sticky left-0 z-10 flex-shrink-0 bg-sas-panel-alt",
|
|
819
|
+
style: { width: GUTTER_W },
|
|
820
|
+
children: rows.map((p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
821
|
+
"div",
|
|
822
|
+
{
|
|
823
|
+
"data-testid": "sdk-pr-key",
|
|
824
|
+
"data-pitch": p,
|
|
825
|
+
className: `flex items-center justify-end pr-1 text-[8px] leading-none border-b border-sas-border/30 ${BLACK_KEYS.has((p % 12 + 12) % 12) ? "bg-sas-bg text-sas-muted/40" : "text-sas-muted/70"}`,
|
|
826
|
+
style: { height: ROW_HEIGHT },
|
|
827
|
+
children: p % 12 === 0 ? pitchToName(p) : ""
|
|
828
|
+
},
|
|
829
|
+
p
|
|
830
|
+
))
|
|
831
|
+
}
|
|
832
|
+
),
|
|
833
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
834
|
+
"div",
|
|
835
|
+
{
|
|
836
|
+
ref: gridRef,
|
|
837
|
+
"data-testid": "sdk-pr-grid",
|
|
838
|
+
className: "relative flex-shrink-0",
|
|
839
|
+
style: {
|
|
840
|
+
width: gridWidth,
|
|
841
|
+
height: gridHeight,
|
|
842
|
+
backgroundImage: gridBg,
|
|
843
|
+
cursor: disabled ? "not-allowed" : "crosshair",
|
|
844
|
+
touchAction: "none"
|
|
845
|
+
},
|
|
846
|
+
onPointerDown: handlePointerDown,
|
|
847
|
+
onPointerMove: handlePointerMove,
|
|
848
|
+
onPointerUp: handlePointerUp,
|
|
849
|
+
onPointerCancel: handlePointerCancel,
|
|
850
|
+
children: [
|
|
851
|
+
notes.map((n, i) => {
|
|
852
|
+
const { left, top } = cellToPx(n.pitch, n.startBeat, hi);
|
|
853
|
+
const width = Math.max(3, n.durationBeats * PX_PER_BEAT);
|
|
854
|
+
const handleW = Math.min(RESIZE_HANDLE_PX, width / 2);
|
|
855
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
856
|
+
"div",
|
|
857
|
+
{
|
|
858
|
+
"data-testid": "sdk-pr-note",
|
|
859
|
+
"data-index": i,
|
|
860
|
+
"data-pitch": n.pitch,
|
|
861
|
+
"data-start-beat": n.startBeat,
|
|
862
|
+
"data-duration-beats": n.durationBeats,
|
|
863
|
+
className: "absolute rounded-[2px] bg-sas-accent/80 border border-sas-accent hover:bg-sas-accent",
|
|
864
|
+
style: { left, top, width, height: ROW_HEIGHT },
|
|
865
|
+
title: `${pitchToName(n.pitch)} \xB7 beat ${n.startBeat} \xB7 ${n.durationBeats}\u266A \xB7 vel ${n.velocity}`,
|
|
866
|
+
children: !disabled && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
867
|
+
"div",
|
|
868
|
+
{
|
|
869
|
+
"data-resize-handle": "",
|
|
870
|
+
"data-testid": "sdk-pr-note-resize",
|
|
871
|
+
className: "absolute top-0 right-0 h-full rounded-r-[2px] hover:bg-sas-bg/40",
|
|
872
|
+
style: { width: handleW, cursor: "ew-resize" }
|
|
873
|
+
}
|
|
874
|
+
)
|
|
875
|
+
},
|
|
876
|
+
i
|
|
877
|
+
);
|
|
878
|
+
}),
|
|
879
|
+
notes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
880
|
+
"div",
|
|
881
|
+
{
|
|
882
|
+
"data-testid": "sdk-pr-empty",
|
|
883
|
+
className: "absolute inset-0 flex items-center justify-center text-[10px] text-sas-muted/50 pointer-events-none",
|
|
884
|
+
children: "No notes \u2014 click to add"
|
|
885
|
+
}
|
|
886
|
+
)
|
|
887
|
+
]
|
|
888
|
+
}
|
|
889
|
+
)
|
|
890
|
+
] })
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
] });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/components/TrackDrawer.tsx
|
|
897
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
898
|
+
var TAB_LABELS = {
|
|
899
|
+
fx: "FX",
|
|
900
|
+
pick: "Pick",
|
|
901
|
+
history: "History",
|
|
902
|
+
import: "Import",
|
|
903
|
+
edit: "Edit"
|
|
904
|
+
};
|
|
905
|
+
function TrackDrawer({
|
|
906
|
+
activeTab,
|
|
907
|
+
onTabChange,
|
|
908
|
+
trackId,
|
|
909
|
+
fxState,
|
|
910
|
+
onFxToggle,
|
|
911
|
+
onFxPresetChange,
|
|
912
|
+
onFxDryWetChange,
|
|
913
|
+
fxDisabled = false,
|
|
914
|
+
instruments = [],
|
|
915
|
+
currentPluginId = null,
|
|
916
|
+
isLoading = false,
|
|
917
|
+
onSelect,
|
|
918
|
+
onRefresh,
|
|
919
|
+
editorStage = false,
|
|
920
|
+
onShowEditor,
|
|
921
|
+
onBackToInstruments,
|
|
922
|
+
selectedInstrumentName,
|
|
923
|
+
soundHistory,
|
|
924
|
+
soundHistoryCursor = -1,
|
|
925
|
+
onRestoreSound,
|
|
926
|
+
onToggleFavorite,
|
|
927
|
+
onImportSound,
|
|
928
|
+
importSoundLabel,
|
|
929
|
+
editNotes,
|
|
930
|
+
onNotesChange,
|
|
931
|
+
editBars,
|
|
932
|
+
editBpm,
|
|
933
|
+
editSnap,
|
|
934
|
+
onAuditionNote
|
|
935
|
+
}) {
|
|
936
|
+
const [search, setSearch] = (0, import_react2.useState)("");
|
|
937
|
+
const fxEnabled = !!onFxToggle;
|
|
938
|
+
const pickEnabled = !!onSelect;
|
|
939
|
+
const historyEnabled = !!onRestoreSound;
|
|
940
|
+
const importEnabled = !!onImportSound;
|
|
941
|
+
const editEnabled = !!onNotesChange;
|
|
942
|
+
const enabledTabs = (0, import_react2.useMemo)(() => {
|
|
943
|
+
const tabs = [];
|
|
944
|
+
if (fxEnabled) tabs.push("fx");
|
|
945
|
+
if (pickEnabled) tabs.push("pick");
|
|
946
|
+
if (historyEnabled) tabs.push("history");
|
|
947
|
+
if (importEnabled) tabs.push("import");
|
|
948
|
+
if (editEnabled) tabs.push("edit");
|
|
949
|
+
return tabs;
|
|
950
|
+
}, [fxEnabled, pickEnabled, historyEnabled, importEnabled, editEnabled]);
|
|
951
|
+
const SURGE_XT_DEFAULT_ID = "Surge XT";
|
|
952
|
+
const filtered = (0, import_react2.useMemo)(() => {
|
|
953
|
+
let all = instruments.filter((i) => i.name !== "Surge XT");
|
|
954
|
+
if (search.trim()) {
|
|
955
|
+
const q = search.toLowerCase();
|
|
956
|
+
all = all.filter(
|
|
957
|
+
(i) => i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q)
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (currentPluginId) {
|
|
961
|
+
const selectedIdx = all.findIndex((i) => i.pluginId === currentPluginId);
|
|
962
|
+
if (selectedIdx > 0) {
|
|
963
|
+
const [selected] = all.splice(selectedIdx, 1);
|
|
964
|
+
all.unshift(selected);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return all;
|
|
968
|
+
}, [instruments, search, currentPluginId]);
|
|
969
|
+
const history = soundHistory ?? [];
|
|
970
|
+
const effectiveTab = enabledTabs.includes(activeTab) ? activeTab : enabledTabs[0] ?? "fx";
|
|
971
|
+
const tabClass = (active) => `px-2 py-0.5 text-xs rounded-sm transition-colors ${active ? "bg-sas-accent/20 text-sas-accent font-medium" : "text-sas-muted hover:text-sas-accent"}`;
|
|
972
|
+
const strip = enabledTabs.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
973
|
+
"div",
|
|
974
|
+
{
|
|
975
|
+
className: "flex items-center gap-1 border-b border-sas-border pb-1",
|
|
976
|
+
"data-testid": "sdk-drawer-tabs",
|
|
977
|
+
children: enabledTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
978
|
+
"button",
|
|
979
|
+
{
|
|
980
|
+
type: "button",
|
|
981
|
+
"data-testid": `sdk-drawer-tab-${tab}`,
|
|
982
|
+
onClick: () => onTabChange?.(tab),
|
|
983
|
+
className: tabClass(effectiveTab === tab),
|
|
984
|
+
children: tab === "history" && history.length > 0 ? `History (${history.length})` : TAB_LABELS[tab]
|
|
985
|
+
},
|
|
986
|
+
tab
|
|
987
|
+
))
|
|
988
|
+
}
|
|
989
|
+
) : null;
|
|
990
|
+
const currentSound = soundHistoryCursor >= 0 && soundHistoryCursor < history.length ? history[soundHistoryCursor].label : null;
|
|
991
|
+
const header = strip || currentSound ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-1", "data-testid": "sdk-drawer-header", children: [
|
|
992
|
+
strip,
|
|
993
|
+
currentSound && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
994
|
+
"span",
|
|
995
|
+
{
|
|
996
|
+
className: "text-[10px] text-sas-muted/60 truncate px-0.5",
|
|
997
|
+
title: currentSound,
|
|
998
|
+
children: currentSound
|
|
999
|
+
}
|
|
1000
|
+
)
|
|
1001
|
+
] }) : null;
|
|
1002
|
+
if (effectiveTab === "edit") {
|
|
1003
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-edit", children: [
|
|
1004
|
+
header,
|
|
1005
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1006
|
+
PianoRollEditor,
|
|
1007
|
+
{
|
|
1008
|
+
notes: editNotes ?? [],
|
|
1009
|
+
onChange: onNotesChange ?? (() => {
|
|
1010
|
+
}),
|
|
1011
|
+
bars: editBars ?? 4,
|
|
1012
|
+
bpm: editBpm ?? 120,
|
|
1013
|
+
snap: editSnap,
|
|
1014
|
+
onAuditionNote
|
|
1015
|
+
}
|
|
1016
|
+
)
|
|
1017
|
+
] });
|
|
844
1018
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1019
|
+
if (effectiveTab === "fx") {
|
|
1020
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-fx", children: [
|
|
1021
|
+
header,
|
|
1022
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1023
|
+
FxToggleBar,
|
|
1024
|
+
{
|
|
1025
|
+
trackId,
|
|
1026
|
+
fxState,
|
|
1027
|
+
onToggle: (_t, category, enabled) => onFxToggle?.(category, enabled),
|
|
1028
|
+
onPresetChange: (_t, category, presetIndex) => onFxPresetChange?.(category, presetIndex),
|
|
1029
|
+
onDryWetChange: (_t, category, value) => onFxDryWetChange?.(category, value),
|
|
1030
|
+
disabled: fxDisabled
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
] });
|
|
1034
|
+
}
|
|
1035
|
+
if (effectiveTab === "import") {
|
|
1036
|
+
const soundNoun = /preset/i.test(importSoundLabel ?? "") ? "preset" : /sample/i.test(importSoundLabel ?? "") ? "sample" : "sound";
|
|
1037
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-import", children: [
|
|
1038
|
+
header,
|
|
1039
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-[11px] text-sas-muted/70 leading-snug", children: [
|
|
1040
|
+
"Copy the sound from a matching track in another scene \u2014 your MIDI stays, only the",
|
|
1041
|
+
" ",
|
|
1042
|
+
soundNoun,
|
|
1043
|
+
" changes."
|
|
1044
|
+
] }),
|
|
1045
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1046
|
+
"button",
|
|
1047
|
+
{
|
|
1048
|
+
type: "button",
|
|
1049
|
+
"data-testid": "sdk-drawer-import-sound",
|
|
1050
|
+
onClick: onImportSound,
|
|
1051
|
+
className: "w-full px-2 py-1.5 text-[11px] rounded-sm border border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent transition-colors",
|
|
1052
|
+
title: "Copy a sound from a track in another scene (ignores contract)",
|
|
1053
|
+
children: [
|
|
1054
|
+
"\u21EA ",
|
|
1055
|
+
importSoundLabel ?? "Import Sound"
|
|
1056
|
+
]
|
|
1057
|
+
}
|
|
1058
|
+
)
|
|
1059
|
+
] });
|
|
1060
|
+
}
|
|
1061
|
+
if (effectiveTab === "history") {
|
|
1062
|
+
const order = history.map((_, i) => i).reverse();
|
|
1063
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
1064
|
+
header,
|
|
1065
|
+
history.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1066
|
+
"div",
|
|
1067
|
+
{
|
|
1068
|
+
className: "text-xs text-sas-muted/60 text-center py-3",
|
|
1069
|
+
"data-testid": "sdk-history-empty",
|
|
1070
|
+
children: "No sounds yet \u2014 shuffle to build history."
|
|
1071
|
+
}
|
|
1072
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1073
|
+
"ul",
|
|
1074
|
+
{
|
|
1075
|
+
className: "flex flex-col gap-1 max-h-[160px] overflow-y-auto",
|
|
1076
|
+
"data-testid": "sdk-history-list",
|
|
1077
|
+
children: order.map((i) => {
|
|
1078
|
+
const entry = history[i];
|
|
1079
|
+
const isCurrent = i === soundHistoryCursor;
|
|
1080
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "flex items-center gap-1", children: [
|
|
1081
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1082
|
+
"button",
|
|
1083
|
+
{
|
|
1084
|
+
type: "button",
|
|
1085
|
+
"data-testid": "sdk-history-entry",
|
|
1086
|
+
disabled: isCurrent,
|
|
1087
|
+
onClick: () => onRestoreSound?.(i),
|
|
1088
|
+
className: `flex-1 min-w-0 flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${isCurrent ? "border-sas-accent bg-sas-accent/20 text-sas-accent cursor-default" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
1089
|
+
title: isCurrent ? "Current sound" : `Restore: ${entry.label}`,
|
|
1090
|
+
children: [
|
|
1091
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "truncate", children: entry.label }),
|
|
1092
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] text-sas-muted/60 flex-shrink-0 ml-2", children: isCurrent ? "\u25CF current" : "restore" })
|
|
1093
|
+
]
|
|
1094
|
+
}
|
|
1095
|
+
),
|
|
1096
|
+
onToggleFavorite && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1097
|
+
"button",
|
|
1098
|
+
{
|
|
1099
|
+
type: "button",
|
|
1100
|
+
"data-testid": "sdk-history-favorite",
|
|
1101
|
+
onClick: () => onToggleFavorite(i),
|
|
1102
|
+
className: `flex-shrink-0 px-1 py-0.5 text-sm leading-none transition-colors ${entry.favorite ? "text-yellow-400" : "text-sas-muted/40 hover:text-yellow-400"}`,
|
|
1103
|
+
title: entry.favorite ? "Unfavorite" : "Favorite (keeps it from being evicted)",
|
|
1104
|
+
children: entry.favorite ? "\u2605" : "\u2606"
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
1107
|
+
] }, i);
|
|
1108
|
+
})
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
] });
|
|
1112
|
+
}
|
|
1113
|
+
if (effectiveTab === "pick" && editorStage) {
|
|
1114
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
1115
|
+
header,
|
|
1116
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1117
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1118
|
+
"button",
|
|
1119
|
+
{
|
|
1120
|
+
onClick: () => onBackToInstruments?.(),
|
|
1121
|
+
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",
|
|
1122
|
+
children: "\u2190 Back"
|
|
1123
|
+
}
|
|
1124
|
+
),
|
|
1125
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
|
|
1126
|
+
] }),
|
|
1127
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1128
|
+
"button",
|
|
1129
|
+
{
|
|
1130
|
+
onClick: () => onShowEditor?.(),
|
|
1131
|
+
className: "w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors",
|
|
1132
|
+
children: "Open Plugin Editor"
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
] });
|
|
1136
|
+
}
|
|
1137
|
+
const isDefaultSelected = currentPluginId === null;
|
|
1138
|
+
const isSelected = (pluginId) => pluginId === currentPluginId;
|
|
1139
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
1140
|
+
header,
|
|
1141
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1142
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1143
|
+
"input",
|
|
1144
|
+
{
|
|
1145
|
+
type: "text",
|
|
1146
|
+
value: search,
|
|
1147
|
+
onChange: (e) => setSearch(e.target.value),
|
|
1148
|
+
placeholder: "Search instruments...",
|
|
1149
|
+
className: "sas-input flex-1 px-2 py-1 text-xs"
|
|
1150
|
+
}
|
|
1151
|
+
),
|
|
1152
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1153
|
+
"button",
|
|
1154
|
+
{
|
|
1155
|
+
onClick: () => onRefresh?.(),
|
|
1156
|
+
disabled: isLoading,
|
|
1157
|
+
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",
|
|
1158
|
+
title: "Re-scan plugins",
|
|
1159
|
+
children: isLoading ? "..." : "Refresh"
|
|
1160
|
+
}
|
|
1161
|
+
)
|
|
1162
|
+
] }),
|
|
1163
|
+
isLoading && instruments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
|
|
1164
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1165
|
+
"button",
|
|
1166
|
+
{
|
|
1167
|
+
onClick: () => onSelect?.(SURGE_XT_DEFAULT_ID),
|
|
1168
|
+
className: `flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${isDefaultSelected ? "border-sas-accent bg-sas-accent/20 text-sas-accent" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
1169
|
+
title: "Surge XT \u2014 Default instrument",
|
|
1170
|
+
children: [
|
|
1171
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "text-xs font-medium truncate w-full", children: [
|
|
1172
|
+
isDefaultSelected && "\u2713 ",
|
|
1173
|
+
"Surge XT"
|
|
1174
|
+
] }),
|
|
1175
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
|
|
1176
|
+
]
|
|
1177
|
+
},
|
|
1178
|
+
"__surge-xt-default__"
|
|
1179
|
+
),
|
|
1180
|
+
filtered.map((inst) => {
|
|
1181
|
+
const selected = isSelected(inst.pluginId);
|
|
1182
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1183
|
+
"button",
|
|
1184
|
+
{
|
|
1185
|
+
onClick: () => onSelect?.(inst.pluginId),
|
|
1186
|
+
className: `flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${selected ? "border-sas-accent bg-sas-accent/20 text-sas-accent" : inst.missing ? "border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
1187
|
+
title: `${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? " \u2014 MISSING" : ""}`,
|
|
1188
|
+
children: [
|
|
1189
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "text-xs font-medium truncate w-full", children: [
|
|
1190
|
+
selected && "\u2713 ",
|
|
1191
|
+
inst.name
|
|
1192
|
+
] }),
|
|
1193
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
|
|
1194
|
+
]
|
|
1195
|
+
},
|
|
1196
|
+
inst.pluginId
|
|
1197
|
+
);
|
|
1198
|
+
}),
|
|
1199
|
+
filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "col-span-2 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No other plugins found" })
|
|
1200
|
+
] })
|
|
1201
|
+
] });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/components/ConfirmDialog.tsx
|
|
1205
|
+
var import_react4 = require("react");
|
|
1206
|
+
|
|
1207
|
+
// src/components/Modal.tsx
|
|
1208
|
+
var import_react3 = require("react");
|
|
1209
|
+
var import_react_dom = require("react-dom");
|
|
1210
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1211
|
+
function Modal({
|
|
1212
|
+
open,
|
|
1213
|
+
onClose,
|
|
1214
|
+
children,
|
|
1215
|
+
testIdPrefix = "modal",
|
|
1216
|
+
closeOnBackdrop = true,
|
|
1217
|
+
closeOnEscape = true,
|
|
1218
|
+
initialFocusRef
|
|
1219
|
+
}) {
|
|
1220
|
+
(0, import_react3.useEffect)(() => {
|
|
1221
|
+
if (!open) return void 0;
|
|
1222
|
+
const onKey = (e) => {
|
|
1223
|
+
if (closeOnEscape && e.key === "Escape") {
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
onClose();
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
window.addEventListener("keydown", onKey);
|
|
1229
|
+
initialFocusRef?.current?.focus();
|
|
1230
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1231
|
+
}, [open, onClose, closeOnEscape, initialFocusRef]);
|
|
1232
|
+
if (!open) return null;
|
|
1233
|
+
return (0, import_react_dom.createPortal)(
|
|
1234
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1235
|
+
"div",
|
|
1236
|
+
{
|
|
1237
|
+
className: "fixed inset-0 z-[1000] flex items-center justify-center bg-black/60",
|
|
1238
|
+
"data-testid": `${testIdPrefix}-overlay`,
|
|
1239
|
+
onClick: closeOnBackdrop ? onClose : void 0,
|
|
1240
|
+
children
|
|
1241
|
+
}
|
|
1242
|
+
),
|
|
1243
|
+
document.body
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/components/ConfirmDialog.tsx
|
|
1248
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1249
|
+
function ConfirmDialog({
|
|
1250
|
+
open,
|
|
1251
|
+
title,
|
|
1252
|
+
message,
|
|
1253
|
+
confirmLabel = "Delete",
|
|
1254
|
+
cancelLabel = "Cancel",
|
|
1255
|
+
destructive = true,
|
|
1256
|
+
onConfirm,
|
|
1257
|
+
onCancel,
|
|
1258
|
+
testIdPrefix = "confirm-dialog"
|
|
1259
|
+
}) {
|
|
1260
|
+
const cancelRef = (0, import_react4.useRef)(null);
|
|
1261
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Modal, { open, onClose: onCancel, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1262
|
+
"div",
|
|
1263
|
+
{
|
|
1264
|
+
className: "w-[360px] max-w-[90vw] flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
1265
|
+
onClick: (e) => e.stopPropagation(),
|
|
1266
|
+
role: "dialog",
|
|
1267
|
+
"aria-modal": "true",
|
|
1268
|
+
"aria-label": title,
|
|
1269
|
+
"data-testid": `${testIdPrefix}-modal`,
|
|
1270
|
+
children: [
|
|
1271
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-4 py-3 border-b border-sas-border", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm font-medium text-sas-text", "data-testid": `${testIdPrefix}-title`, children: title }) }),
|
|
1272
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1273
|
+
"div",
|
|
1274
|
+
{
|
|
1275
|
+
className: "px-4 py-3 text-xs text-sas-muted leading-relaxed break-words",
|
|
1276
|
+
"data-testid": `${testIdPrefix}-message`,
|
|
1277
|
+
children: message
|
|
1278
|
+
}
|
|
1279
|
+
),
|
|
1280
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-end gap-2 px-4 py-3 border-t border-sas-border", children: [
|
|
1281
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1282
|
+
"button",
|
|
1283
|
+
{
|
|
1284
|
+
ref: cancelRef,
|
|
1285
|
+
type: "button",
|
|
1286
|
+
className: "px-3 py-1 rounded-sm text-xs font-medium border border-sas-border bg-sas-panel-alt text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
|
|
1287
|
+
onClick: onCancel,
|
|
1288
|
+
"data-testid": `${testIdPrefix}-cancel`,
|
|
1289
|
+
children: cancelLabel
|
|
1290
|
+
}
|
|
1291
|
+
),
|
|
1292
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1293
|
+
"button",
|
|
1294
|
+
{
|
|
1295
|
+
type: "button",
|
|
1296
|
+
className: `px-3 py-1 rounded-sm text-xs font-medium border transition-colors ${destructive ? "border-sas-danger bg-sas-danger/20 text-sas-danger hover:bg-sas-danger hover:text-sas-bg" : "border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent hover:text-sas-bg"}`,
|
|
1297
|
+
onClick: onConfirm,
|
|
1298
|
+
"data-testid": `${testIdPrefix}-confirm`,
|
|
1299
|
+
children: confirmLabel
|
|
1300
|
+
}
|
|
1301
|
+
)
|
|
1302
|
+
] })
|
|
1303
|
+
]
|
|
1304
|
+
}
|
|
1305
|
+
) });
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/components/LevelMeter.tsx
|
|
1309
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1310
|
+
var COLOR_GREEN = "#2BD576";
|
|
1311
|
+
var COLOR_ORANGE = "#F5A623";
|
|
1312
|
+
var COLOR_RED = "#FF4D5E";
|
|
1313
|
+
var COLOR_TRACK_BG = "#121822";
|
|
1314
|
+
var COLOR_TRACK_BORDER = "#1F2A3A";
|
|
1315
|
+
var COLOR_SEGMENT_GAP = "#0A0E14";
|
|
1316
|
+
var COLOR_PEAK = "#F7FFFB";
|
|
1317
|
+
var METER_GRADIENT = `linear-gradient(90deg, ${COLOR_GREEN} 0%, ${COLOR_GREEN} 45%, ${COLOR_ORANGE} 72%, ${COLOR_RED} 90%, ${COLOR_RED} 100%)`;
|
|
1318
|
+
var SEGMENTS = 22;
|
|
1319
|
+
var SEGMENT_GAP_PX = 2;
|
|
1320
|
+
function dbToPct(db) {
|
|
1321
|
+
return Math.max(0, Math.min(100, (db + 60) / 60 * 100));
|
|
1322
|
+
}
|
|
1323
|
+
var LevelMeter = ({
|
|
1324
|
+
peakDb,
|
|
1325
|
+
active,
|
|
1326
|
+
peakHoldDb,
|
|
1327
|
+
clipped,
|
|
1328
|
+
onClearClip,
|
|
1329
|
+
compact = false,
|
|
1330
|
+
className,
|
|
1331
|
+
"data-testid": testId
|
|
1332
|
+
}) => {
|
|
1333
|
+
const id = testId ?? "sas-level-meter";
|
|
1334
|
+
const widthPct = active ? dbToPct(peakDb) : 0;
|
|
1335
|
+
const showPeak = peakHoldDb != null && active && peakHoldDb > -60;
|
|
1336
|
+
const peakHoldPct = showPeak ? dbToPct(peakHoldDb) : 0;
|
|
1337
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1338
|
+
"div",
|
|
1339
|
+
{
|
|
1340
|
+
className: `sas-level-meter ${className ?? ""}`,
|
|
1341
|
+
"data-testid": id,
|
|
1342
|
+
style: {
|
|
1343
|
+
display: "flex",
|
|
1344
|
+
alignItems: "center",
|
|
1345
|
+
gap: compact ? 0 : 6
|
|
1346
|
+
},
|
|
1347
|
+
children: [
|
|
1348
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1349
|
+
"div",
|
|
1350
|
+
{
|
|
1351
|
+
style: {
|
|
1352
|
+
position: "relative",
|
|
1353
|
+
flex: 1,
|
|
1354
|
+
height: compact ? 5 : 7,
|
|
1355
|
+
background: COLOR_TRACK_BG,
|
|
1356
|
+
border: `1px solid ${COLOR_TRACK_BORDER}`,
|
|
1357
|
+
borderRadius: 2,
|
|
1358
|
+
overflow: "hidden",
|
|
1359
|
+
minWidth: compact ? 0 : 60
|
|
1360
|
+
},
|
|
1361
|
+
children: [
|
|
1362
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { position: "absolute", inset: 0, background: METER_GRADIENT } }),
|
|
1363
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1364
|
+
"div",
|
|
1365
|
+
{
|
|
1366
|
+
style: {
|
|
1367
|
+
position: "absolute",
|
|
1368
|
+
top: 0,
|
|
1369
|
+
bottom: 0,
|
|
1370
|
+
left: `${widthPct}%`,
|
|
1371
|
+
right: 0,
|
|
1372
|
+
background: COLOR_TRACK_BG,
|
|
1373
|
+
transition: "left 30ms linear"
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
),
|
|
1377
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1378
|
+
"div",
|
|
1379
|
+
{
|
|
1380
|
+
"data-testid": `${id}-segments`,
|
|
1381
|
+
style: {
|
|
1382
|
+
position: "absolute",
|
|
1383
|
+
inset: 0,
|
|
1384
|
+
pointerEvents: "none",
|
|
1385
|
+
backgroundImage: `linear-gradient(90deg, transparent 0, transparent calc(100% - ${SEGMENT_GAP_PX}px), ${COLOR_SEGMENT_GAP} calc(100% - ${SEGMENT_GAP_PX}px), ${COLOR_SEGMENT_GAP} 100%)`,
|
|
1386
|
+
backgroundSize: `calc(100% / ${SEGMENTS}) 100%`
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
),
|
|
1390
|
+
showPeak && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1391
|
+
"div",
|
|
1392
|
+
{
|
|
1393
|
+
"data-testid": `${id}-peak`,
|
|
1394
|
+
style: {
|
|
1395
|
+
position: "absolute",
|
|
1396
|
+
top: -1,
|
|
1397
|
+
bottom: -1,
|
|
1398
|
+
left: `${peakHoldPct}%`,
|
|
1399
|
+
width: 2,
|
|
1400
|
+
marginLeft: -1,
|
|
1401
|
+
background: COLOR_PEAK,
|
|
1402
|
+
boxShadow: "0 0 4px rgba(247, 255, 251, 0.7)",
|
|
1403
|
+
transition: "left 80ms linear"
|
|
1404
|
+
},
|
|
1405
|
+
title: "Peak"
|
|
1406
|
+
}
|
|
1407
|
+
)
|
|
1408
|
+
]
|
|
1409
|
+
}
|
|
1410
|
+
),
|
|
1411
|
+
!compact && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1412
|
+
"span",
|
|
1413
|
+
{
|
|
1414
|
+
style: {
|
|
1415
|
+
fontSize: 10,
|
|
1416
|
+
color: "var(--sas-muted, #888)",
|
|
1417
|
+
fontVariantNumeric: "tabular-nums",
|
|
1418
|
+
minWidth: 48,
|
|
1419
|
+
textAlign: "right"
|
|
1420
|
+
},
|
|
1421
|
+
children: active && peakDb > -120 ? `${peakDb.toFixed(0)} dB` : "\u2014"
|
|
1422
|
+
}
|
|
1423
|
+
),
|
|
1424
|
+
clipped && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1425
|
+
"span",
|
|
1426
|
+
{
|
|
1427
|
+
"data-testid": `${id}-clip`,
|
|
1428
|
+
onClick: onClearClip,
|
|
1429
|
+
style: {
|
|
1430
|
+
padding: "1px 5px",
|
|
1431
|
+
fontSize: 9,
|
|
1432
|
+
fontWeight: "bold",
|
|
1433
|
+
background: COLOR_RED,
|
|
1434
|
+
color: "#0A0E14",
|
|
1435
|
+
borderRadius: 2,
|
|
1436
|
+
cursor: onClearClip ? "pointer" : "default",
|
|
1437
|
+
marginLeft: compact ? 3 : 0
|
|
1438
|
+
},
|
|
1439
|
+
title: onClearClip ? "Clipped \u2014 click to clear" : "Clipped",
|
|
1440
|
+
children: "CLIP"
|
|
1441
|
+
}
|
|
1442
|
+
)
|
|
1443
|
+
]
|
|
1444
|
+
}
|
|
1445
|
+
);
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
// src/hooks/useTrackLevels.ts
|
|
1449
|
+
var import_react5 = require("react");
|
|
1450
|
+
var POLL_INTERVAL_MS = 33;
|
|
1451
|
+
var HIDDEN_RECHECK_MS = 250;
|
|
1452
|
+
var METER_FLOOR_DB = -120;
|
|
1453
|
+
var PEAK_HOLD_MS = 1500;
|
|
1454
|
+
var PEAK_DECAY_DB_PER_SEC = 24;
|
|
1455
|
+
function isHidden() {
|
|
1456
|
+
return typeof document !== "undefined" && document.hidden === true;
|
|
1457
|
+
}
|
|
1458
|
+
function useTrackLevels(host, enabled = true) {
|
|
1459
|
+
const mapRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
|
|
1460
|
+
const listenersRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
|
|
1461
|
+
const handleRef = (0, import_react5.useRef)(null);
|
|
1462
|
+
if (handleRef.current === null) {
|
|
1463
|
+
handleRef.current = {
|
|
1464
|
+
getLevel: (trackId) => mapRef.current.get(trackId) ?? null,
|
|
1465
|
+
subscribe: (listener) => {
|
|
1466
|
+
listenersRef.current.add(listener);
|
|
1467
|
+
return () => {
|
|
1468
|
+
listenersRef.current.delete(listener);
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
(0, import_react5.useEffect)(() => {
|
|
1474
|
+
const notify = () => {
|
|
1475
|
+
listenersRef.current.forEach((l) => l());
|
|
1476
|
+
};
|
|
1477
|
+
const clearToIdle = () => {
|
|
1478
|
+
if (mapRef.current.size > 0) {
|
|
1479
|
+
mapRef.current.clear();
|
|
1480
|
+
notify();
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
const canPoll = enabled && !!host && typeof host.getTrackLevels === "function";
|
|
1484
|
+
if (!canPoll) {
|
|
1485
|
+
clearToIdle();
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
let stopped = false;
|
|
1489
|
+
let timer = null;
|
|
1490
|
+
const schedule = (delay) => {
|
|
1491
|
+
if (stopped) return;
|
|
1492
|
+
timer = setTimeout(tick, delay);
|
|
1493
|
+
};
|
|
1494
|
+
const tick = async () => {
|
|
1495
|
+
if (stopped) return;
|
|
1496
|
+
if (isHidden()) {
|
|
1497
|
+
schedule(HIDDEN_RECHECK_MS);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
const levels = await host.getTrackLevels();
|
|
1502
|
+
if (stopped) return;
|
|
1503
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1504
|
+
for (const lvl of levels) {
|
|
1505
|
+
mapRef.current.set(lvl.trackId, lvl);
|
|
1506
|
+
seen.add(lvl.trackId);
|
|
1507
|
+
}
|
|
1508
|
+
for (const key of Array.from(mapRef.current.keys())) {
|
|
1509
|
+
if (!seen.has(key)) mapRef.current.delete(key);
|
|
1510
|
+
}
|
|
1511
|
+
notify();
|
|
1512
|
+
} catch {
|
|
1513
|
+
}
|
|
1514
|
+
schedule(POLL_INTERVAL_MS);
|
|
1515
|
+
};
|
|
1516
|
+
const onVisibility = () => {
|
|
1517
|
+
if (stopped) return;
|
|
1518
|
+
if (!isHidden()) {
|
|
1519
|
+
if (timer) clearTimeout(timer);
|
|
1520
|
+
void tick();
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
if (typeof document !== "undefined") {
|
|
1524
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
1525
|
+
}
|
|
1526
|
+
void tick();
|
|
1527
|
+
return () => {
|
|
1528
|
+
stopped = true;
|
|
1529
|
+
if (timer) clearTimeout(timer);
|
|
1530
|
+
if (typeof document !== "undefined") {
|
|
1531
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
}, [host, enabled]);
|
|
1535
|
+
return handleRef.current;
|
|
1536
|
+
}
|
|
1537
|
+
function sameLevel(a, b) {
|
|
1538
|
+
if (a === b) return true;
|
|
1539
|
+
if (a === null || b === null) return false;
|
|
1540
|
+
return a.peakDb === b.peakDb && a.clipped === b.clipped;
|
|
1541
|
+
}
|
|
1542
|
+
function useTrackLevel(handle, trackId) {
|
|
1543
|
+
const [level, setLevel] = (0, import_react5.useState)(null);
|
|
1544
|
+
(0, import_react5.useEffect)(() => {
|
|
1545
|
+
if (!handle) {
|
|
1546
|
+
setLevel(null);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
const update = () => {
|
|
1550
|
+
const next = handle.getLevel(trackId);
|
|
1551
|
+
setLevel((prev) => sameLevel(prev, next) ? prev : next);
|
|
1552
|
+
};
|
|
1553
|
+
update();
|
|
1554
|
+
return handle.subscribe(update);
|
|
1555
|
+
}, [handle, trackId]);
|
|
1556
|
+
return level;
|
|
1557
|
+
}
|
|
1558
|
+
var IDLE_METER_VIEW = {
|
|
1559
|
+
peakDb: METER_FLOOR_DB,
|
|
1560
|
+
peakHoldDb: METER_FLOOR_DB,
|
|
1561
|
+
clipped: false,
|
|
1562
|
+
active: false
|
|
1563
|
+
};
|
|
1564
|
+
function sameMeter(a, b) {
|
|
1565
|
+
return a.active === b.active && a.clipped === b.clipped && a.peakDb === b.peakDb && Math.round(a.peakHoldDb * 2) === Math.round(b.peakHoldDb * 2);
|
|
1566
|
+
}
|
|
1567
|
+
function useTrackMeter(handle, trackId) {
|
|
1568
|
+
const [view, setView] = (0, import_react5.useState)(IDLE_METER_VIEW);
|
|
1569
|
+
const heldDbRef = (0, import_react5.useRef)(METER_FLOOR_DB);
|
|
1570
|
+
const heldAtRef = (0, import_react5.useRef)(0);
|
|
1571
|
+
const lastTickRef = (0, import_react5.useRef)(0);
|
|
1572
|
+
(0, import_react5.useEffect)(() => {
|
|
1573
|
+
if (!handle) {
|
|
1574
|
+
heldDbRef.current = METER_FLOOR_DB;
|
|
1575
|
+
lastTickRef.current = 0;
|
|
1576
|
+
setView(IDLE_METER_VIEW);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const update = () => {
|
|
1580
|
+
const level = handle.getLevel(trackId);
|
|
1581
|
+
const now = performance.now();
|
|
1582
|
+
const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
|
|
1583
|
+
lastTickRef.current = now;
|
|
1584
|
+
if (level === null) {
|
|
1585
|
+
heldDbRef.current = METER_FLOOR_DB;
|
|
1586
|
+
setView((prev) => sameMeter(prev, IDLE_METER_VIEW) ? prev : IDLE_METER_VIEW);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const p = level.peakDb;
|
|
1590
|
+
if (p >= heldDbRef.current) {
|
|
1591
|
+
heldDbRef.current = p;
|
|
1592
|
+
heldAtRef.current = now;
|
|
1593
|
+
} else if (now - heldAtRef.current > PEAK_HOLD_MS) {
|
|
1594
|
+
heldDbRef.current = Math.max(p, heldDbRef.current - PEAK_DECAY_DB_PER_SEC * dtSec);
|
|
1595
|
+
}
|
|
1596
|
+
const next = {
|
|
1597
|
+
peakDb: p,
|
|
1598
|
+
peakHoldDb: heldDbRef.current,
|
|
1599
|
+
clipped: level.clipped,
|
|
1600
|
+
active: true
|
|
1601
|
+
};
|
|
1602
|
+
setView((prev) => sameMeter(prev, next) ? prev : next);
|
|
1603
|
+
};
|
|
1604
|
+
update();
|
|
1605
|
+
return handle.subscribe(update);
|
|
1606
|
+
}, [handle, trackId]);
|
|
1607
|
+
return view;
|
|
1608
|
+
}
|
|
1609
|
+
function useTransportPlaying(host) {
|
|
1610
|
+
const [playing, setPlaying] = (0, import_react5.useState)(false);
|
|
1611
|
+
(0, import_react5.useEffect)(() => {
|
|
1612
|
+
if (!host) {
|
|
1613
|
+
setPlaying(false);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
let cancelled = false;
|
|
1617
|
+
host.getTransportState().then((state) => {
|
|
1618
|
+
if (!cancelled) setPlaying(!!state.isPlaying);
|
|
1619
|
+
}).catch(() => {
|
|
1620
|
+
});
|
|
1621
|
+
const unsub = host.onTransportEvent?.((evt) => {
|
|
1622
|
+
if (typeof evt.isPlaying === "boolean") {
|
|
1623
|
+
setPlaying(evt.isPlaying);
|
|
1624
|
+
} else if (evt.type === "play") {
|
|
1625
|
+
setPlaying(true);
|
|
1626
|
+
} else if (evt.type === "stop" || evt.type === "pause") {
|
|
1627
|
+
setPlaying(false);
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
return () => {
|
|
1631
|
+
cancelled = true;
|
|
1632
|
+
unsub?.();
|
|
1633
|
+
};
|
|
1634
|
+
}, [host]);
|
|
1635
|
+
return playing;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/components/TrackMeterStrip.tsx
|
|
1639
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1640
|
+
var TrackMeterStrip = ({
|
|
1641
|
+
levels,
|
|
1642
|
+
trackId,
|
|
1643
|
+
roundBottom = true,
|
|
1644
|
+
className
|
|
1645
|
+
}) => {
|
|
1646
|
+
const meter = useTrackMeter(levels, trackId);
|
|
1647
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1648
|
+
"div",
|
|
1649
|
+
{
|
|
1650
|
+
"data-testid": "sdk-track-meter",
|
|
1651
|
+
className: `w-full px-2 py-1 bg-sas-panel-alt border border-t-0 border-sas-border ${roundBottom ? "rounded-b-sm" : ""} ${className ?? ""}`,
|
|
1652
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1653
|
+
LevelMeter,
|
|
1654
|
+
{
|
|
1655
|
+
compact: true,
|
|
1656
|
+
active: meter.active,
|
|
1657
|
+
peakDb: meter.peakDb,
|
|
1658
|
+
peakHoldDb: meter.peakHoldDb,
|
|
1659
|
+
clipped: meter.clipped,
|
|
1660
|
+
"data-testid": `sdk-track-meter-bar-${trackId}`
|
|
1661
|
+
}
|
|
1662
|
+
)
|
|
1663
|
+
}
|
|
1664
|
+
);
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
// src/components/VolumeSlider.tsx
|
|
1668
|
+
var import_react6 = require("react");
|
|
1669
|
+
|
|
1670
|
+
// src/utils/volume-conversion.ts
|
|
1671
|
+
var SLIDER_UNITY = 0.75;
|
|
1672
|
+
var DB_MAX = 6;
|
|
1673
|
+
var DB_MIN = -60;
|
|
1674
|
+
var EXPONENT = Math.log(Math.pow(10, DB_MAX / 20)) / Math.log(1 / SLIDER_UNITY);
|
|
1675
|
+
function sliderToDb(slider) {
|
|
1676
|
+
if (slider <= 0) return DB_MIN;
|
|
1677
|
+
const gain = Math.pow(slider / SLIDER_UNITY, EXPONENT);
|
|
1678
|
+
const db = 20 * Math.log10(gain);
|
|
1679
|
+
return Math.max(DB_MIN, Math.min(DB_MAX, db));
|
|
1680
|
+
}
|
|
1681
|
+
function dbToSlider(db) {
|
|
1682
|
+
if (db <= DB_MIN) return 0;
|
|
1683
|
+
if (db >= DB_MAX) return 1;
|
|
1684
|
+
const gain = Math.pow(10, db / 20);
|
|
1685
|
+
const slider = SLIDER_UNITY * Math.pow(gain, 1 / EXPONENT);
|
|
1686
|
+
return Math.min(1, Math.max(0, slider));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/components/VolumeSlider.tsx
|
|
1690
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1691
|
+
function formatDb(value) {
|
|
1692
|
+
const db = sliderToDb(value);
|
|
1693
|
+
if (db <= -60) return "-\u221E dB";
|
|
1694
|
+
const sign = db >= 0 ? "+" : "";
|
|
1695
|
+
return `${sign}${db.toFixed(1)} dB`;
|
|
1696
|
+
}
|
|
1697
|
+
function useDebouncedCallback(callback, delay) {
|
|
1698
|
+
const timeoutRef = (0, import_react6.useRef)(null);
|
|
1699
|
+
const callbackRef = (0, import_react6.useRef)(callback);
|
|
1700
|
+
(0, import_react6.useEffect)(() => {
|
|
1701
|
+
callbackRef.current = callback;
|
|
1702
|
+
}, [callback]);
|
|
1703
|
+
const debouncedCallback = (0, import_react6.useCallback)(
|
|
1704
|
+
(...args) => {
|
|
1705
|
+
if (timeoutRef.current) {
|
|
1706
|
+
clearTimeout(timeoutRef.current);
|
|
1707
|
+
}
|
|
1708
|
+
timeoutRef.current = setTimeout(() => {
|
|
1709
|
+
callbackRef.current(...args);
|
|
1710
|
+
}, delay);
|
|
1711
|
+
},
|
|
1712
|
+
[delay]
|
|
1713
|
+
);
|
|
1714
|
+
(0, import_react6.useEffect)(() => {
|
|
1715
|
+
return () => {
|
|
1716
|
+
if (timeoutRef.current) {
|
|
1717
|
+
clearTimeout(timeoutRef.current);
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
}, []);
|
|
1721
|
+
return debouncedCallback;
|
|
1722
|
+
}
|
|
1723
|
+
var VolumeSlider = ({
|
|
1724
|
+
value,
|
|
1725
|
+
onChange,
|
|
1726
|
+
disabled = false,
|
|
1727
|
+
className = ""
|
|
1728
|
+
}) => {
|
|
1729
|
+
const [localValue, setLocalValue] = (0, import_react6.useState)(value);
|
|
1730
|
+
const [isDragging, setIsDragging] = (0, import_react6.useState)(false);
|
|
1731
|
+
(0, import_react6.useEffect)(() => {
|
|
1732
|
+
if (!isDragging) {
|
|
1733
|
+
setLocalValue(value);
|
|
1734
|
+
}
|
|
1735
|
+
}, [value, isDragging]);
|
|
1736
|
+
const debouncedOnChange = useDebouncedCallback(onChange, 50);
|
|
1737
|
+
const handleChange = (0, import_react6.useCallback)(
|
|
1738
|
+
(e) => {
|
|
1739
|
+
const newValue = parseFloat(e.target.value);
|
|
1740
|
+
setLocalValue(newValue);
|
|
1741
|
+
debouncedOnChange(newValue);
|
|
1742
|
+
},
|
|
1743
|
+
[debouncedOnChange]
|
|
1744
|
+
);
|
|
1745
|
+
const handleMouseDown = (0, import_react6.useCallback)(() => {
|
|
1746
|
+
setIsDragging(true);
|
|
1747
|
+
}, []);
|
|
1748
|
+
const handleMouseUp = (0, import_react6.useCallback)(() => {
|
|
1749
|
+
setIsDragging(false);
|
|
1750
|
+
onChange(localValue);
|
|
1751
|
+
}, [localValue, onChange]);
|
|
1752
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1753
|
+
"div",
|
|
1754
|
+
{
|
|
1755
|
+
className: `flex items-center ${className}`,
|
|
1756
|
+
title: `Volume: ${formatDb(localValue)}`,
|
|
1757
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1758
|
+
"input",
|
|
1759
|
+
{
|
|
1760
|
+
type: "range",
|
|
1761
|
+
min: "0",
|
|
1762
|
+
max: "1",
|
|
1763
|
+
step: "0.01",
|
|
1764
|
+
value: localValue,
|
|
1765
|
+
onChange: handleChange,
|
|
1766
|
+
onMouseDown: handleMouseDown,
|
|
1767
|
+
onMouseUp: handleMouseUp,
|
|
1768
|
+
onTouchStart: handleMouseDown,
|
|
1769
|
+
onTouchEnd: handleMouseUp,
|
|
1770
|
+
disabled,
|
|
1771
|
+
className: `
|
|
1772
|
+
w-full h-1.5 rounded-full appearance-none cursor-pointer
|
|
1773
|
+
bg-gray-700
|
|
1774
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
1775
|
+
[&::-webkit-slider-thumb]:appearance-none
|
|
1776
|
+
[&::-webkit-slider-thumb]:w-3
|
|
1777
|
+
[&::-webkit-slider-thumb]:h-3
|
|
1778
|
+
[&::-webkit-slider-thumb]:rounded-full
|
|
1779
|
+
[&::-webkit-slider-thumb]:bg-sas-accent
|
|
1780
|
+
[&::-webkit-slider-thumb]:cursor-pointer
|
|
1781
|
+
[&::-webkit-slider-thumb]:transition-transform
|
|
1782
|
+
[&::-webkit-slider-thumb]:hover:scale-110
|
|
1783
|
+
[&::-moz-range-thumb]:w-3
|
|
1784
|
+
[&::-moz-range-thumb]:h-3
|
|
1785
|
+
[&::-moz-range-thumb]:rounded-full
|
|
1786
|
+
[&::-moz-range-thumb]:bg-sas-accent
|
|
1787
|
+
[&::-moz-range-thumb]:border-0
|
|
1788
|
+
[&::-moz-range-thumb]:cursor-pointer
|
|
1789
|
+
`
|
|
1790
|
+
}
|
|
1791
|
+
)
|
|
1792
|
+
}
|
|
1793
|
+
);
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
// src/components/PanSlider.tsx
|
|
1797
|
+
var import_react7 = require("react");
|
|
1798
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1799
|
+
function toPanDisplay(value) {
|
|
1800
|
+
if (Math.abs(value) < 0.02) {
|
|
1801
|
+
return "Center";
|
|
1802
|
+
}
|
|
1803
|
+
const percent = Math.abs(Math.round(value * 100));
|
|
1804
|
+
return value < 0 ? `L${percent}` : `R${percent}`;
|
|
1805
|
+
}
|
|
1806
|
+
function useDebouncedCallback2(callback, delay) {
|
|
1807
|
+
const timeoutRef = (0, import_react7.useRef)(null);
|
|
1808
|
+
const callbackRef = (0, import_react7.useRef)(callback);
|
|
1809
|
+
(0, import_react7.useEffect)(() => {
|
|
1810
|
+
callbackRef.current = callback;
|
|
1811
|
+
}, [callback]);
|
|
1812
|
+
const debouncedCallback = (0, import_react7.useCallback)(
|
|
1813
|
+
(...args) => {
|
|
1814
|
+
if (timeoutRef.current) {
|
|
1815
|
+
clearTimeout(timeoutRef.current);
|
|
1816
|
+
}
|
|
1817
|
+
timeoutRef.current = setTimeout(() => {
|
|
1818
|
+
callbackRef.current(...args);
|
|
1819
|
+
}, delay);
|
|
1820
|
+
},
|
|
1821
|
+
[delay]
|
|
1822
|
+
);
|
|
1823
|
+
(0, import_react7.useEffect)(() => {
|
|
1824
|
+
return () => {
|
|
1825
|
+
if (timeoutRef.current) {
|
|
1826
|
+
clearTimeout(timeoutRef.current);
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}, []);
|
|
1830
|
+
return debouncedCallback;
|
|
1831
|
+
}
|
|
1832
|
+
var PanSlider = ({
|
|
1833
|
+
value,
|
|
1834
|
+
onChange,
|
|
1835
|
+
disabled = false,
|
|
1836
|
+
className = ""
|
|
1837
|
+
}) => {
|
|
1838
|
+
const [localValue, setLocalValue] = (0, import_react7.useState)(value);
|
|
1839
|
+
const [isDragging, setIsDragging] = (0, import_react7.useState)(false);
|
|
1840
|
+
(0, import_react7.useEffect)(() => {
|
|
1841
|
+
if (!isDragging) {
|
|
1842
|
+
setLocalValue(value);
|
|
1843
|
+
}
|
|
1844
|
+
}, [value, isDragging]);
|
|
1845
|
+
const debouncedOnChange = useDebouncedCallback2(onChange, 50);
|
|
1846
|
+
const handleChange = (0, import_react7.useCallback)(
|
|
1847
|
+
(e) => {
|
|
1848
|
+
const newValue = parseFloat(e.target.value);
|
|
1849
|
+
setLocalValue(newValue);
|
|
1850
|
+
debouncedOnChange(newValue);
|
|
1851
|
+
},
|
|
1852
|
+
[debouncedOnChange]
|
|
1853
|
+
);
|
|
1854
|
+
const handleMouseDown = (0, import_react7.useCallback)(() => {
|
|
1855
|
+
setIsDragging(true);
|
|
1856
|
+
}, []);
|
|
1857
|
+
const handleMouseUp = (0, import_react7.useCallback)(() => {
|
|
1858
|
+
setIsDragging(false);
|
|
1859
|
+
onChange(localValue);
|
|
1860
|
+
}, [localValue, onChange]);
|
|
1861
|
+
const handleDoubleClick = (0, import_react7.useCallback)(() => {
|
|
1862
|
+
setLocalValue(0);
|
|
1863
|
+
onChange(0);
|
|
1864
|
+
}, [onChange]);
|
|
1865
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1866
|
+
"div",
|
|
1867
|
+
{
|
|
1868
|
+
className: `flex items-center ${className}`,
|
|
1869
|
+
title: `Pan: ${toPanDisplay(localValue)}`,
|
|
1870
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1871
|
+
"input",
|
|
1872
|
+
{
|
|
1873
|
+
type: "range",
|
|
1874
|
+
min: "-1",
|
|
1875
|
+
max: "1",
|
|
1876
|
+
step: "0.01",
|
|
1877
|
+
value: localValue,
|
|
1878
|
+
onChange: handleChange,
|
|
1879
|
+
onMouseDown: handleMouseDown,
|
|
1880
|
+
onMouseUp: handleMouseUp,
|
|
1881
|
+
onTouchStart: handleMouseDown,
|
|
1882
|
+
onTouchEnd: handleMouseUp,
|
|
1883
|
+
onDoubleClick: handleDoubleClick,
|
|
1884
|
+
disabled,
|
|
1885
|
+
className: `
|
|
1886
|
+
w-full h-1.5 rounded-full appearance-none cursor-pointer
|
|
1887
|
+
bg-gray-700
|
|
1888
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
1889
|
+
[&::-webkit-slider-thumb]:appearance-none
|
|
1890
|
+
[&::-webkit-slider-thumb]:w-3
|
|
1891
|
+
[&::-webkit-slider-thumb]:h-3
|
|
1892
|
+
[&::-webkit-slider-thumb]:rounded-full
|
|
1893
|
+
[&::-webkit-slider-thumb]:bg-sas-accent
|
|
1894
|
+
[&::-webkit-slider-thumb]:cursor-pointer
|
|
1895
|
+
[&::-webkit-slider-thumb]:transition-transform
|
|
1896
|
+
[&::-webkit-slider-thumb]:hover:scale-110
|
|
1897
|
+
[&::-moz-range-thumb]:w-3
|
|
1898
|
+
[&::-moz-range-thumb]:h-3
|
|
1899
|
+
[&::-moz-range-thumb]:rounded-full
|
|
1900
|
+
[&::-moz-range-thumb]:bg-sas-accent
|
|
1901
|
+
[&::-moz-range-thumb]:border-0
|
|
1902
|
+
[&::-moz-range-thumb]:cursor-pointer
|
|
1903
|
+
`
|
|
1904
|
+
}
|
|
1905
|
+
)
|
|
1906
|
+
}
|
|
1907
|
+
);
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
// src/components/SorceryProgressBar.tsx
|
|
1911
|
+
var import_react8 = require("react");
|
|
1912
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1913
|
+
function calculateTimeBasedTarget(elapsedMs, estimatedDurationMs) {
|
|
1914
|
+
const t = elapsedMs / estimatedDurationMs;
|
|
1915
|
+
if (t <= 0) return 0;
|
|
1916
|
+
if (t <= 1) {
|
|
1917
|
+
return 90 * (1 - Math.pow(1 - t, 2.5));
|
|
1918
|
+
}
|
|
1919
|
+
const overshootRatio = (elapsedMs - estimatedDurationMs) / estimatedDurationMs;
|
|
1920
|
+
return 90 + 5 * (1 - Math.exp(-overshootRatio * 3));
|
|
1921
|
+
}
|
|
1922
|
+
function calculateNextProgress(currentProgress) {
|
|
1923
|
+
if (currentProgress < 20) {
|
|
1924
|
+
return currentProgress + Math.random() * 10 + 5;
|
|
1925
|
+
}
|
|
1926
|
+
if (currentProgress < 60) {
|
|
1927
|
+
return currentProgress + Math.random() * 5 + 2;
|
|
1928
|
+
}
|
|
1929
|
+
if (currentProgress < 95) {
|
|
1930
|
+
const remaining = 95 - currentProgress;
|
|
1931
|
+
const increment = remaining * (Math.random() * 0.2 + 0.1);
|
|
1932
|
+
return currentProgress + Math.max(increment, 0.1);
|
|
1933
|
+
}
|
|
1934
|
+
return 95;
|
|
1935
|
+
}
|
|
1936
|
+
function calculateNextTickInterval(progress) {
|
|
1937
|
+
if (progress < 30) {
|
|
1938
|
+
return Math.random() * 200 + 150;
|
|
1939
|
+
}
|
|
1940
|
+
if (progress < 70) {
|
|
1941
|
+
return Math.random() * 300 + 200;
|
|
1942
|
+
}
|
|
1943
|
+
return Math.random() * 600 + 400;
|
|
1944
|
+
}
|
|
1945
|
+
var TIME_BASED_TICK_MIN = 200;
|
|
1946
|
+
var TIME_BASED_TICK_RANGE = 100;
|
|
1947
|
+
function SorceryProgressBar({
|
|
1948
|
+
isLoading,
|
|
1949
|
+
statusText = "CONJURING...",
|
|
1950
|
+
completeText = "COMPLETE",
|
|
1951
|
+
onComplete,
|
|
1952
|
+
heightClass = "h-10",
|
|
1953
|
+
initialProgress = 0,
|
|
1954
|
+
onProgressChange,
|
|
1955
|
+
estimatedDurationMs
|
|
1956
|
+
}) {
|
|
1957
|
+
const [progress, setProgress] = (0, import_react8.useState)(initialProgress);
|
|
1958
|
+
const timerRef = (0, import_react8.useRef)(null);
|
|
1959
|
+
const isLoadingRef = (0, import_react8.useRef)(false);
|
|
1960
|
+
const hasStartedRef = (0, import_react8.useRef)(false);
|
|
1961
|
+
const startTimeRef = (0, import_react8.useRef)(0);
|
|
1962
|
+
const onProgressChangeRef = (0, import_react8.useRef)(onProgressChange);
|
|
1963
|
+
const onCompleteRef = (0, import_react8.useRef)(onComplete);
|
|
1964
|
+
onProgressChangeRef.current = onProgressChange;
|
|
1965
|
+
onCompleteRef.current = onComplete;
|
|
1966
|
+
const initialProgressRef = (0, import_react8.useRef)(initialProgress);
|
|
1967
|
+
initialProgressRef.current = initialProgress;
|
|
1968
|
+
const estimatedDurationMsRef = (0, import_react8.useRef)(estimatedDurationMs);
|
|
1969
|
+
estimatedDurationMsRef.current = estimatedDurationMs;
|
|
1970
|
+
(0, import_react8.useEffect)(() => {
|
|
1971
|
+
const wasLoading = isLoadingRef.current;
|
|
1972
|
+
isLoadingRef.current = isLoading;
|
|
1973
|
+
if (isLoading && !wasLoading) {
|
|
1974
|
+
hasStartedRef.current = true;
|
|
1975
|
+
startTimeRef.current = Date.now();
|
|
1976
|
+
const startProgress = initialProgressRef.current > 0 ? initialProgressRef.current : 0;
|
|
1977
|
+
setProgress(startProgress);
|
|
1978
|
+
const duration = estimatedDurationMsRef.current;
|
|
1979
|
+
if (duration && duration > 0) {
|
|
1980
|
+
const tick = () => {
|
|
1981
|
+
setProgress((prev) => {
|
|
1982
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
1983
|
+
const target = calculateTimeBasedTarget(elapsed, duration);
|
|
1984
|
+
const jitter = (Math.random() - 0.5) * 1;
|
|
1985
|
+
const next = Math.min(Math.max(target + jitter, prev + 0.05), 95);
|
|
1986
|
+
onProgressChangeRef.current?.(next);
|
|
1987
|
+
timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN + Math.random() * TIME_BASED_TICK_RANGE);
|
|
1988
|
+
return next;
|
|
1989
|
+
});
|
|
1990
|
+
};
|
|
1991
|
+
timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN);
|
|
1992
|
+
} else {
|
|
1993
|
+
const tick = () => {
|
|
1994
|
+
setProgress((prev) => {
|
|
1995
|
+
if (prev >= 95) {
|
|
1996
|
+
timerRef.current = setTimeout(tick, 1e3);
|
|
1997
|
+
return 95;
|
|
1998
|
+
}
|
|
1999
|
+
const next = Math.min(calculateNextProgress(prev), 95);
|
|
2000
|
+
onProgressChangeRef.current?.(next);
|
|
2001
|
+
const interval = calculateNextTickInterval(next);
|
|
2002
|
+
timerRef.current = setTimeout(tick, interval);
|
|
2003
|
+
return next;
|
|
2004
|
+
});
|
|
2005
|
+
};
|
|
2006
|
+
const firstInterval = calculateNextTickInterval(startProgress);
|
|
2007
|
+
timerRef.current = setTimeout(tick, firstInterval);
|
|
2008
|
+
}
|
|
2009
|
+
} else if (!isLoading && wasLoading && hasStartedRef.current) {
|
|
2010
|
+
if (timerRef.current) {
|
|
2011
|
+
clearTimeout(timerRef.current);
|
|
2012
|
+
timerRef.current = null;
|
|
2013
|
+
}
|
|
2014
|
+
setProgress(100);
|
|
2015
|
+
onProgressChangeRef.current?.(100);
|
|
2016
|
+
onCompleteRef.current?.();
|
|
2017
|
+
hasStartedRef.current = false;
|
|
2018
|
+
}
|
|
2019
|
+
return () => {
|
|
2020
|
+
if (timerRef.current) {
|
|
2021
|
+
clearTimeout(timerRef.current);
|
|
2022
|
+
timerRef.current = null;
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
}, [isLoading]);
|
|
2026
|
+
if (!isLoading && progress === 0) {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
const displayProgress = Math.floor(progress);
|
|
2030
|
+
const isComplete = !isLoading && progress === 100;
|
|
2031
|
+
const transitionDuration = progress < 50 ? "300ms" : progress < 80 ? "500ms" : "700ms";
|
|
2032
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
2033
|
+
"div",
|
|
2034
|
+
{
|
|
2035
|
+
className: `relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`,
|
|
2036
|
+
children: [
|
|
2037
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
2038
|
+
"div",
|
|
2039
|
+
{
|
|
2040
|
+
className: `
|
|
2041
|
+
h-full
|
|
2042
|
+
bg-gradient-to-r from-sas-accent/70 to-sas-accent
|
|
2043
|
+
shadow-glow-soft
|
|
2044
|
+
sorcery-progress-fill
|
|
2045
|
+
animate-progress-stripes
|
|
2046
|
+
${progress > 70 ? "animate-progress-pulse" : ""}
|
|
2047
|
+
transition-all ease-out
|
|
2048
|
+
`,
|
|
2049
|
+
style: {
|
|
2050
|
+
width: `${progress}%`,
|
|
2051
|
+
transitionDuration
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
),
|
|
2055
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "absolute inset-0 flex items-center justify-center", children: isLoading && progress < 100 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider", children: [
|
|
2056
|
+
statusText,
|
|
2057
|
+
" ",
|
|
2058
|
+
displayProgress,
|
|
2059
|
+
"%"
|
|
2060
|
+
] }) : isComplete ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider", children: completeText }) : null }),
|
|
2061
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
2062
|
+
"div",
|
|
2063
|
+
{
|
|
2064
|
+
className: "absolute inset-0 pointer-events-none opacity-10",
|
|
2065
|
+
style: {
|
|
2066
|
+
backgroundImage: `repeating-linear-gradient(
|
|
2067
|
+
to bottom,
|
|
2068
|
+
transparent,
|
|
2069
|
+
transparent 2px,
|
|
2070
|
+
rgba(0, 0, 0, 0.3) 2px,
|
|
2071
|
+
rgba(0, 0, 0, 0.3) 4px
|
|
2072
|
+
)`
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
)
|
|
2076
|
+
]
|
|
2077
|
+
}
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/components/TrackRow.tsx
|
|
2082
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2083
|
+
function TrackRow({
|
|
2084
|
+
track,
|
|
2085
|
+
prompt,
|
|
2086
|
+
runtimeState,
|
|
2087
|
+
soloedOut = false,
|
|
2088
|
+
fxDetailState,
|
|
2089
|
+
drawerOpen,
|
|
2090
|
+
drawerTab,
|
|
2091
|
+
onTabChange,
|
|
2092
|
+
isGenerating = false,
|
|
2093
|
+
isAuthenticated = false,
|
|
2094
|
+
error,
|
|
2095
|
+
hasMidi = false,
|
|
2096
|
+
generationProgress = 0,
|
|
2097
|
+
estimatedGenerationMs = 15e3,
|
|
2098
|
+
onPromptChange,
|
|
2099
|
+
onGenerate,
|
|
2100
|
+
onShuffle,
|
|
2101
|
+
onCopy,
|
|
2102
|
+
onDelete,
|
|
2103
|
+
contentSlot,
|
|
2104
|
+
onMuteToggle,
|
|
2105
|
+
onSoloToggle,
|
|
2106
|
+
onVolumeChange,
|
|
2107
|
+
onPanChange,
|
|
2108
|
+
onFxToggle,
|
|
2109
|
+
onFxPresetChange,
|
|
2110
|
+
onFxDryWetChange,
|
|
2111
|
+
onToggleFxDrawer,
|
|
2112
|
+
onProgressChange,
|
|
2113
|
+
accentColor = "#A78BFA",
|
|
2114
|
+
instrumentName,
|
|
2115
|
+
instrumentMissing,
|
|
2116
|
+
onToggleDrawer,
|
|
2117
|
+
availableInstruments,
|
|
2118
|
+
currentInstrumentPluginId,
|
|
2119
|
+
onInstrumentSelect,
|
|
2120
|
+
instrumentsLoading,
|
|
2121
|
+
onRefreshInstruments,
|
|
2122
|
+
editorStage,
|
|
2123
|
+
onShowEditor,
|
|
2124
|
+
onBackToInstruments,
|
|
2125
|
+
soundHistory,
|
|
2126
|
+
soundHistoryCursor,
|
|
2127
|
+
onRestoreSound,
|
|
2128
|
+
onToggleFavorite,
|
|
2129
|
+
onImportSound,
|
|
2130
|
+
importSoundLabel,
|
|
2131
|
+
editNotes,
|
|
2132
|
+
onNotesChange,
|
|
2133
|
+
editBars,
|
|
2134
|
+
editBpm,
|
|
2135
|
+
editSnap,
|
|
2136
|
+
onAuditionNote,
|
|
2137
|
+
drag,
|
|
2138
|
+
levels
|
|
2139
|
+
}) {
|
|
2140
|
+
const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;
|
|
2141
|
+
const [confirmDelete, setConfirmDelete] = import_react9.default.useState(false);
|
|
2142
|
+
const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);
|
|
2143
|
+
const hasFxActive = Object.values(fxDetailState).some(
|
|
2144
|
+
(d) => d.enabled
|
|
2145
|
+
);
|
|
2146
|
+
const fxTabOpen = drawerOpen && drawerTab === "fx";
|
|
2147
|
+
const soundTabOpen = drawerOpen && drawerTab !== "fx";
|
|
2148
|
+
const handleKeyDown = (e) => {
|
|
2149
|
+
if (e.key === "Enter" && !e.shiftKey && onGenerate) {
|
|
2150
|
+
e.preventDefault();
|
|
2151
|
+
onGenerate();
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
const borderColorStyle = needsGeneration ? void 0 : accentColor;
|
|
2155
|
+
const borderClass = needsGeneration ? "border-amber-400 animate-pulse" : "border-sas-border";
|
|
2156
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
|
|
2157
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
2158
|
+
"div",
|
|
2159
|
+
{
|
|
2160
|
+
"data-testid": "sdk-track-row",
|
|
2161
|
+
className: `relative flex items-stretch gap-1 p-2 ${levels ? "rounded-t-sm" : "rounded-sm"} border w-full overflow-hidden ${borderClass} bg-sas-panel-alt ${drag?.isDragging ? "opacity-40" : ""} ${drag?.isDragTarget ? "ring-2 ring-sas-accent ring-inset" : ""}`,
|
|
2162
|
+
style: {
|
|
2163
|
+
borderLeftColor: needsGeneration ? "#f59e0b" : borderColorStyle,
|
|
2164
|
+
borderLeftWidth: "3px"
|
|
2165
|
+
},
|
|
2166
|
+
children: [
|
|
2167
|
+
drag && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2168
|
+
"div",
|
|
2169
|
+
{
|
|
2170
|
+
"data-testid": "sdk-drag-handle",
|
|
2171
|
+
...drag.handleProps,
|
|
2172
|
+
className: "flex-shrink-0 self-stretch flex items-center -ml-0.5 pr-0.5 text-sas-muted/40 hover:text-sas-muted cursor-grab active:cursor-grabbing relative z-30",
|
|
2173
|
+
title: "Drag to reorder",
|
|
2174
|
+
"aria-label": "Drag to reorder track",
|
|
2175
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react.GripVertical, { className: "w-3.5 h-3.5", strokeWidth: 2 })
|
|
2176
|
+
}
|
|
2177
|
+
),
|
|
2178
|
+
isGenerating && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "absolute left-0 top-0 bottom-0 right-44 z-20", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2179
|
+
SorceryProgressBar,
|
|
2180
|
+
{
|
|
2181
|
+
isLoading: true,
|
|
2182
|
+
statusText: "CONJURING MIDI...",
|
|
2183
|
+
heightClass: "h-full",
|
|
2184
|
+
initialProgress: generationProgress,
|
|
2185
|
+
onProgressChange,
|
|
2186
|
+
estimatedDurationMs: estimatedGenerationMs
|
|
2187
|
+
}
|
|
2188
|
+
) }),
|
|
2189
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
2190
|
+
"div",
|
|
2191
|
+
{
|
|
2192
|
+
"data-testid": "sdk-track-content",
|
|
2193
|
+
className: `flex flex-col flex-1 min-w-0 relative z-10 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
|
|
2194
|
+
title: soloedOut ? "Silenced \u2014 another track is soloed" : void 0,
|
|
2195
|
+
children: [
|
|
2196
|
+
contentSlot ? contentSlot : onPromptChange ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2197
|
+
"input",
|
|
2198
|
+
{
|
|
2199
|
+
type: "text",
|
|
2200
|
+
"data-testid": "sdk-prompt-input",
|
|
2201
|
+
value: prompt ?? "",
|
|
2202
|
+
onChange: (e) => onPromptChange(e.target.value),
|
|
2203
|
+
onKeyDown: handleKeyDown,
|
|
2204
|
+
placeholder: "Describe your part...",
|
|
2205
|
+
disabled: isGenerating,
|
|
2206
|
+
className: "sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
|
|
2207
|
+
}
|
|
2208
|
+
) : null,
|
|
2209
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-2 mt-1", children: [
|
|
2210
|
+
track.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]", title: track.name, children: track.name }),
|
|
2211
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "vol:" }),
|
|
2212
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2213
|
+
VolumeSlider,
|
|
2214
|
+
{
|
|
2215
|
+
value: currentVolume,
|
|
2216
|
+
onChange: onVolumeChange,
|
|
2217
|
+
disabled: isGenerating,
|
|
2218
|
+
className: "flex-1 min-w-[40px]"
|
|
2219
|
+
}
|
|
2220
|
+
),
|
|
2221
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
|
|
2222
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2223
|
+
PanSlider,
|
|
2224
|
+
{
|
|
2225
|
+
value: currentPan,
|
|
2226
|
+
onChange: onPanChange,
|
|
2227
|
+
disabled: isGenerating,
|
|
2228
|
+
className: "w-10 flex-shrink-0"
|
|
2229
|
+
}
|
|
2230
|
+
)
|
|
2231
|
+
] })
|
|
2232
|
+
]
|
|
2233
|
+
}
|
|
2234
|
+
),
|
|
2235
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2236
|
+
"div",
|
|
2237
|
+
{
|
|
2238
|
+
"data-testid": "sdk-error-indicator",
|
|
2239
|
+
className: "flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help",
|
|
2240
|
+
title: error,
|
|
2241
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative", children: [
|
|
2242
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2243
|
+
import_lucide_react.AlertCircle,
|
|
2244
|
+
{
|
|
2245
|
+
className: "w-5 h-5 text-red-500 animate-pulse",
|
|
2246
|
+
strokeWidth: 2.5
|
|
2247
|
+
}
|
|
2248
|
+
),
|
|
2249
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate", children: error })
|
|
2250
|
+
] })
|
|
2251
|
+
}
|
|
2252
|
+
),
|
|
2253
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
|
|
2254
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-1 items-center", children: [
|
|
2255
|
+
onGenerate && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2256
|
+
"button",
|
|
2257
|
+
{
|
|
2258
|
+
"data-testid": "sdk-generate-button",
|
|
2259
|
+
onClick: onGenerate,
|
|
2260
|
+
disabled: !isAuthenticated || isGenerating || !prompt?.trim(),
|
|
2261
|
+
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!isAuthenticated || isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : needsGeneration ? "bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse" : prompt?.trim() ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
2262
|
+
title: !isAuthenticated ? "Please log in" : isGenerating ? "Generating..." : "Generate MIDI",
|
|
2263
|
+
children: "Create"
|
|
2264
|
+
}
|
|
2265
|
+
),
|
|
2266
|
+
onCopy && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2267
|
+
"button",
|
|
2268
|
+
{
|
|
2269
|
+
"data-testid": "sdk-copy-button",
|
|
2270
|
+
onClick: onCopy,
|
|
2271
|
+
disabled: !hasMidi || isGenerating,
|
|
2272
|
+
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
2273
|
+
title: hasMidi ? "Duplicate track with different preset" : "Generate MIDI first",
|
|
2274
|
+
children: "Copy"
|
|
2275
|
+
}
|
|
2276
|
+
),
|
|
2277
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2278
|
+
"button",
|
|
2279
|
+
{
|
|
2280
|
+
"data-testid": "sdk-mute-button",
|
|
2281
|
+
onClick: onMuteToggle,
|
|
2282
|
+
disabled: isGenerating,
|
|
2283
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2284
|
+
title: isMuted ? "Unmute track" : "Mute track",
|
|
2285
|
+
children: "M"
|
|
2286
|
+
}
|
|
2287
|
+
),
|
|
2288
|
+
onDelete && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2289
|
+
"button",
|
|
2290
|
+
{
|
|
2291
|
+
"data-testid": "sdk-delete-button",
|
|
2292
|
+
onClick: () => setConfirmDelete(true),
|
|
2293
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm",
|
|
2294
|
+
title: "Delete track",
|
|
2295
|
+
children: "x"
|
|
2296
|
+
}
|
|
2297
|
+
)
|
|
2298
|
+
] }),
|
|
2299
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-1 items-center", children: [
|
|
2300
|
+
onShuffle && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2301
|
+
"button",
|
|
2302
|
+
{
|
|
2303
|
+
"data-testid": "sdk-shuffle-button",
|
|
2304
|
+
onClick: onShuffle,
|
|
2305
|
+
disabled: !hasMidi || isGenerating || !!currentInstrumentPluginId,
|
|
2306
|
+
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating || !!currentInstrumentPluginId ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
2307
|
+
title: currentInstrumentPluginId ? "Shuffle only works with default Surge XT" : hasMidi ? "Re-roll sound (keep MIDI)" : "Generate MIDI first",
|
|
2308
|
+
children: "Shuffle"
|
|
2309
|
+
}
|
|
2310
|
+
),
|
|
2311
|
+
onToggleFxDrawer && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2312
|
+
"button",
|
|
2313
|
+
{
|
|
2314
|
+
"data-testid": "sdk-fx-button",
|
|
2315
|
+
onClick: onToggleFxDrawer,
|
|
2316
|
+
disabled: isGenerating,
|
|
2317
|
+
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : fxTabOpen ? "bg-sas-accent border-sas-accent text-sas-bg" : hasFxActive ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
2318
|
+
title: fxTabOpen ? "Hide FX controls" : "Show FX controls",
|
|
2319
|
+
children: "FX"
|
|
2320
|
+
}
|
|
2321
|
+
),
|
|
2322
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2323
|
+
"button",
|
|
2324
|
+
{
|
|
2325
|
+
"data-testid": "sdk-solo-button",
|
|
2326
|
+
onClick: onSoloToggle,
|
|
2327
|
+
disabled: isGenerating,
|
|
2328
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isSoloed ? "bg-yellow-500 text-black" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2329
|
+
title: isSoloed ? "Unsolo track" : "Solo track",
|
|
2330
|
+
children: "S"
|
|
2331
|
+
}
|
|
2332
|
+
),
|
|
2333
|
+
onToggleDrawer && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2334
|
+
"button",
|
|
2335
|
+
{
|
|
2336
|
+
"data-testid": "sdk-plugin-button",
|
|
2337
|
+
onClick: onToggleDrawer,
|
|
2338
|
+
disabled: isGenerating,
|
|
2339
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : soundTabOpen ? "bg-sas-accent border-sas-accent text-sas-bg" : instrumentMissing ? "bg-amber-500/20 text-amber-400 hover:bg-amber-500/40" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2340
|
+
title: `Sound \u2014 presets & history${instrumentMissing ? " (instrument missing)" : ""}`,
|
|
2341
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react.ChevronDown, { className: "w-3 h-3", strokeWidth: 2.5 })
|
|
2342
|
+
}
|
|
2343
|
+
)
|
|
2344
|
+
] })
|
|
2345
|
+
] })
|
|
2346
|
+
]
|
|
2347
|
+
}
|
|
2348
|
+
),
|
|
2349
|
+
levels && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(TrackMeterStrip, { levels, trackId: track.id, roundBottom: !drawerOpen }),
|
|
2350
|
+
drawerOpen && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2351
|
+
"div",
|
|
2352
|
+
{
|
|
2353
|
+
"data-testid": "sdk-track-drawer",
|
|
2354
|
+
className: "border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[260px] overflow-y-auto",
|
|
2355
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2356
|
+
TrackDrawer,
|
|
2357
|
+
{
|
|
2358
|
+
activeTab: drawerTab,
|
|
2359
|
+
onTabChange,
|
|
2360
|
+
trackId: track.id,
|
|
2361
|
+
fxState: fxDetailState,
|
|
2362
|
+
onFxToggle,
|
|
2363
|
+
onFxPresetChange,
|
|
2364
|
+
onFxDryWetChange,
|
|
2365
|
+
fxDisabled: isGenerating,
|
|
2366
|
+
instruments: availableInstruments,
|
|
2367
|
+
currentPluginId: currentInstrumentPluginId ?? null,
|
|
2368
|
+
isLoading: instrumentsLoading ?? false,
|
|
2369
|
+
onSelect: onInstrumentSelect,
|
|
2370
|
+
onRefresh: onRefreshInstruments,
|
|
2371
|
+
editorStage,
|
|
2372
|
+
onShowEditor,
|
|
2373
|
+
onBackToInstruments,
|
|
2374
|
+
selectedInstrumentName: instrumentName,
|
|
2375
|
+
soundHistory,
|
|
2376
|
+
soundHistoryCursor,
|
|
2377
|
+
onRestoreSound,
|
|
2378
|
+
onToggleFavorite,
|
|
2379
|
+
onImportSound,
|
|
2380
|
+
importSoundLabel,
|
|
2381
|
+
editNotes,
|
|
2382
|
+
onNotesChange,
|
|
2383
|
+
editBars,
|
|
2384
|
+
editBpm,
|
|
2385
|
+
editSnap,
|
|
2386
|
+
onAuditionNote
|
|
2387
|
+
}
|
|
2388
|
+
)
|
|
2389
|
+
}
|
|
2390
|
+
),
|
|
2391
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2392
|
+
ConfirmDialog,
|
|
2393
|
+
{
|
|
2394
|
+
open: confirmDelete,
|
|
2395
|
+
title: "Delete track?",
|
|
2396
|
+
message: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
|
|
2397
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-sas-text", children: track.name?.trim() || "This track" }),
|
|
2398
|
+
" will be permanently removed from this scene. This cannot be undone."
|
|
2399
|
+
] }),
|
|
2400
|
+
confirmLabel: "Delete",
|
|
2401
|
+
onConfirm: () => {
|
|
2402
|
+
setConfirmDelete(false);
|
|
2403
|
+
onDelete?.();
|
|
2404
|
+
},
|
|
2405
|
+
onCancel: () => setConfirmDelete(false),
|
|
2406
|
+
testIdPrefix: "track-delete-confirm"
|
|
2407
|
+
}
|
|
2408
|
+
)
|
|
2409
|
+
] });
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// src/components/CrossfadeTrackRow.tsx
|
|
2413
|
+
var import_react10 = __toESM(require("react"));
|
|
2414
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
2415
|
+
function LayerCaption({ tag, layer }) {
|
|
2416
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
|
|
2417
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
|
|
2418
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
|
|
2419
|
+
layer.soundLabel && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
|
|
2420
|
+
"\xB7 ",
|
|
2421
|
+
layer.soundLabel
|
|
2422
|
+
] })
|
|
2423
|
+
] });
|
|
2424
|
+
}
|
|
2425
|
+
function CrossfadeTrackRow({
|
|
2426
|
+
origin,
|
|
2427
|
+
target,
|
|
2428
|
+
sliderPos = 0.5,
|
|
2429
|
+
onMuteToggle,
|
|
2430
|
+
onSoloToggle,
|
|
2431
|
+
onVolumeChange,
|
|
2432
|
+
onPanChange,
|
|
2433
|
+
onDelete,
|
|
2434
|
+
onSliderChange,
|
|
2435
|
+
levels,
|
|
2436
|
+
accentColor = "#9333EA"
|
|
2437
|
+
}) {
|
|
2438
|
+
const [confirmDelete, setConfirmDelete] = import_react10.default.useState(false);
|
|
2439
|
+
const renderLayer = (layer, slot, tag) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2440
|
+
TrackRow,
|
|
2441
|
+
{
|
|
2442
|
+
track: { id: layer.trackId, name: "", role: layer.role },
|
|
2443
|
+
runtimeState: layer.runtimeState,
|
|
2444
|
+
fxDetailState: EMPTY_FX_DETAIL_STATE,
|
|
2445
|
+
drawerOpen: false,
|
|
2446
|
+
drawerTab: "fx",
|
|
2447
|
+
levels,
|
|
2448
|
+
accentColor,
|
|
2449
|
+
contentSlot: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(LayerCaption, { tag, layer }),
|
|
2450
|
+
onMuteToggle,
|
|
2451
|
+
onSoloToggle,
|
|
2452
|
+
onVolumeChange: (v) => onVolumeChange(slot, v),
|
|
2453
|
+
onPanChange: (p) => onPanChange(slot, p)
|
|
2454
|
+
}
|
|
2455
|
+
);
|
|
2456
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
2457
|
+
"div",
|
|
2458
|
+
{
|
|
2459
|
+
"data-testid": "crossfade-track-row",
|
|
2460
|
+
className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
|
|
2461
|
+
style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
|
|
2462
|
+
children: [
|
|
2463
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
|
|
2464
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-[10px] font-bold uppercase tracking-wide", style: { color: accentColor }, children: "\u21C4 Crossfade" }),
|
|
2465
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2466
|
+
"button",
|
|
2467
|
+
{
|
|
2468
|
+
"data-testid": "crossfade-delete-button",
|
|
2469
|
+
onClick: () => setConfirmDelete(true),
|
|
2470
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
|
|
2471
|
+
title: "Delete crossfade pair",
|
|
2472
|
+
"aria-label": "Delete crossfade pair",
|
|
2473
|
+
children: "x"
|
|
2474
|
+
}
|
|
2475
|
+
)
|
|
2476
|
+
] }),
|
|
2477
|
+
renderLayer(origin, "origin", "Origin"),
|
|
2478
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "crossfade-slider-row", children: [
|
|
2479
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2480
|
+
"span",
|
|
2481
|
+
{
|
|
2482
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
|
|
2483
|
+
title: origin.sourceName ?? origin.name,
|
|
2484
|
+
children: origin.sourceName ?? origin.name
|
|
2485
|
+
}
|
|
2486
|
+
),
|
|
2487
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2488
|
+
"input",
|
|
2489
|
+
{
|
|
2490
|
+
type: "range",
|
|
2491
|
+
"data-testid": "crossfade-slider",
|
|
2492
|
+
min: 0,
|
|
2493
|
+
max: 1,
|
|
2494
|
+
step: 0.01,
|
|
2495
|
+
value: sliderPos,
|
|
2496
|
+
disabled: !onSliderChange,
|
|
2497
|
+
onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
|
|
2498
|
+
style: { accentColor },
|
|
2499
|
+
className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
|
|
2500
|
+
"aria-label": "Crossfade position"
|
|
2501
|
+
}
|
|
2502
|
+
),
|
|
2503
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2504
|
+
"span",
|
|
2505
|
+
{
|
|
2506
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
|
|
2507
|
+
title: target.sourceName ?? target.name,
|
|
2508
|
+
children: target.sourceName ?? target.name
|
|
2509
|
+
}
|
|
2510
|
+
)
|
|
2511
|
+
] }),
|
|
2512
|
+
renderLayer(target, "target", "Target"),
|
|
2513
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2514
|
+
ConfirmDialog,
|
|
2515
|
+
{
|
|
2516
|
+
open: confirmDelete,
|
|
2517
|
+
title: "Delete crossfade?",
|
|
2518
|
+
message: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_jsx_runtime12.Fragment, { children: "This crossfade pair (both layers) will be permanently removed from this scene. This cannot be undone." }),
|
|
2519
|
+
confirmLabel: "Delete",
|
|
2520
|
+
onConfirm: () => {
|
|
2521
|
+
setConfirmDelete(false);
|
|
2522
|
+
onDelete();
|
|
2523
|
+
},
|
|
2524
|
+
onCancel: () => setConfirmDelete(false),
|
|
2525
|
+
testIdPrefix: "crossfade-delete-confirm"
|
|
2526
|
+
}
|
|
2527
|
+
)
|
|
2528
|
+
]
|
|
2529
|
+
}
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// src/crossfade-meta.ts
|
|
2534
|
+
var EQUAL_POWER_GAIN = 0.707;
|
|
2535
|
+
function asCrossfadeMeta(val) {
|
|
2536
|
+
if (!val || typeof val !== "object") return null;
|
|
2537
|
+
const m = val;
|
|
2538
|
+
if (typeof m.groupId !== "string" || m.slot !== "origin" && m.slot !== "target") return null;
|
|
2539
|
+
if (typeof m.partnerDbId !== "string") return null;
|
|
2540
|
+
return {
|
|
2541
|
+
groupId: m.groupId,
|
|
2542
|
+
slot: m.slot,
|
|
2543
|
+
partnerDbId: m.partnerDbId,
|
|
2544
|
+
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2545
|
+
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2546
|
+
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
2547
|
+
soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
|
|
2548
|
+
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
|
|
2549
|
+
};
|
|
847
2550
|
}
|
|
848
|
-
function
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
2551
|
+
function parseCrossfadePairs(sceneData) {
|
|
2552
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2553
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
2554
|
+
const match = /^track:(.+):crossfade$/.exec(key);
|
|
2555
|
+
if (!match) continue;
|
|
2556
|
+
const meta = asCrossfadeMeta(val);
|
|
2557
|
+
if (!meta) continue;
|
|
2558
|
+
const dbId = match[1];
|
|
2559
|
+
const g = groups.get(meta.groupId) ?? {};
|
|
2560
|
+
if (meta.slot === "origin") g.origin = { dbId, meta };
|
|
2561
|
+
else g.target = { dbId, meta };
|
|
2562
|
+
groups.set(meta.groupId, g);
|
|
854
2563
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
2564
|
+
const pairs = [];
|
|
2565
|
+
for (const [groupId, g] of groups) {
|
|
2566
|
+
if (!g.origin || !g.target) continue;
|
|
2567
|
+
pairs.push({
|
|
2568
|
+
groupId,
|
|
2569
|
+
sliderPos: g.origin.meta.sliderPos,
|
|
2570
|
+
originDbId: g.origin.dbId,
|
|
2571
|
+
targetDbId: g.target.dbId,
|
|
2572
|
+
originSourceName: g.origin.meta.sourceName,
|
|
2573
|
+
originSoundLabel: g.origin.meta.soundLabel,
|
|
2574
|
+
targetSourceName: g.target.meta.sourceName,
|
|
2575
|
+
targetSoundLabel: g.target.meta.soundLabel
|
|
2576
|
+
});
|
|
859
2577
|
}
|
|
860
|
-
return
|
|
2578
|
+
return pairs;
|
|
861
2579
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
2580
|
+
|
|
2581
|
+
// src/crossfade-inpaint.ts
|
|
2582
|
+
var PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
2583
|
+
function round3(n) {
|
|
2584
|
+
return Math.round(n * 1e3) / 1e3;
|
|
2585
|
+
}
|
|
2586
|
+
function pitchName(p) {
|
|
2587
|
+
return `${PITCH_NAMES[(p % 12 + 12) % 12]}${Math.floor(p / 12) - 1}`;
|
|
2588
|
+
}
|
|
2589
|
+
function compactNote(n) {
|
|
2590
|
+
return { pitch: n.pitch, startBeat: round3(n.startBeat), durationBeats: round3(n.durationBeats), velocity: n.velocity };
|
|
2591
|
+
}
|
|
2592
|
+
function summarize(notes, percussive) {
|
|
2593
|
+
if (notes.length === 0) return "empty (no notes)";
|
|
2594
|
+
const span = round3(Math.max(...notes.map((n) => n.startBeat + n.durationBeats)));
|
|
2595
|
+
if (percussive) return `${notes.length} hits, spans ~${span} beats`;
|
|
2596
|
+
const pitches = notes.map((n) => n.pitch);
|
|
2597
|
+
return `${notes.length} notes, ${pitchName(Math.min(...pitches))}\u2013${pitchName(Math.max(...pitches))}, spans ~${span} beats`;
|
|
2598
|
+
}
|
|
2599
|
+
function gloss(notes, percussive) {
|
|
2600
|
+
const sorted = [...notes].sort((a, b) => a.startBeat - b.startBeat);
|
|
2601
|
+
const maxEnd = Math.max(...sorted.map((n) => n.startBeat + n.durationBeats));
|
|
2602
|
+
const bars = Math.max(1, Math.ceil(maxEnd / 4));
|
|
2603
|
+
const lines = [];
|
|
2604
|
+
for (let b = 0; b < bars; b++) {
|
|
2605
|
+
const inBar = sorted.filter((n) => n.startBeat >= b * 4 && n.startBeat < (b + 1) * 4);
|
|
2606
|
+
if (inBar.length === 0) continue;
|
|
2607
|
+
const body = percussive ? inBar.map((n) => `${round3(n.startBeat)}(v${n.velocity})`).join(" ") : inBar.map((n) => `${pitchName(n.pitch)}@${round3(n.startBeat)}`).join(" ");
|
|
2608
|
+
lines.push(` Bar ${b + 1}: ${body}`);
|
|
865
2609
|
}
|
|
866
|
-
|
|
867
|
-
|
|
2610
|
+
return lines.join("\n");
|
|
2611
|
+
}
|
|
2612
|
+
function patternBlock(label, name, key, notes, percussive) {
|
|
2613
|
+
const keyLabel = key ? ` in ${key}` : "";
|
|
2614
|
+
const header = `${label} \u2014 "${name}"${keyLabel} (${summarize(notes, percussive)}):`;
|
|
2615
|
+
if (notes.length === 0) return `${header}
|
|
2616
|
+
(no notes \u2014 treat this end as open)`;
|
|
2617
|
+
return `${header}
|
|
2618
|
+
${gloss(notes, percussive)}
|
|
2619
|
+
exact JSON: ${JSON.stringify(notes.map(compactNote))}`;
|
|
2620
|
+
}
|
|
2621
|
+
function buildCrossfadeInpaintPrompt(input) {
|
|
2622
|
+
const { role, bars, originName, targetName, originKey, targetKey, originNotes, targetNotes } = input;
|
|
2623
|
+
const percussive = input.percussive ?? false;
|
|
2624
|
+
const part = role || (percussive ? "drum" : "melodic");
|
|
2625
|
+
const modulation = originKey && targetKey ? originKey === targetKey ? `stays in ${targetKey}` : `modulates from ${originKey} toward ${targetKey}` : "resolves toward the destination key";
|
|
2626
|
+
const lines = [
|
|
2627
|
+
`TASK \u2014 TRANSITION BRIDGE (musical inpainting).`,
|
|
2628
|
+
`Compose a ${part} part that MORPHS from the ORIGIN pattern into the TARGET pattern across the ${bars} bars`,
|
|
2629
|
+
`of this transition. The Key / BPM / chord progression are given above \u2014 it ${modulation}; honour that`,
|
|
2630
|
+
`frame, don't restate it. Each pattern below is shown as a pitch/rhythm gloss for musicality plus its exact`,
|
|
2631
|
+
`JSON; output your bridge in the same JSON note schema (per the system prompt).`,
|
|
2632
|
+
``,
|
|
2633
|
+
patternBlock("ORIGIN pattern (where the bridge BEGINS)", originName, originKey, originNotes, percussive),
|
|
2634
|
+
``,
|
|
2635
|
+
patternBlock("TARGET pattern (where the bridge must ARRIVE)", targetName, targetKey, targetNotes, percussive),
|
|
2636
|
+
``,
|
|
2637
|
+
`Requirements:`,
|
|
2638
|
+
`- The FIRST bar feels continuous with the ORIGIN \u2014 borrow its register, rhythm, and contour so the seam`,
|
|
2639
|
+
` from the previous scene is seamless.`,
|
|
2640
|
+
`- Across the middle bars, gradually transform toward the TARGET (shift register / rhythm / motifs step by step).`,
|
|
2641
|
+
`- The LAST bar lands on the TARGET's material and resolves onto the destination chord, so the seam into the`,
|
|
2642
|
+
` next scene is seamless.`,
|
|
2643
|
+
`- Stay within the transition chord progression above; favour chord tones at the bar boundaries.`,
|
|
2644
|
+
`- This is inpainting between two FIXED endpoints \u2014 a listener should not be able to point to where the`,
|
|
2645
|
+
` origin ends or the target begins.`
|
|
2646
|
+
];
|
|
2647
|
+
if (originNotes.length === 0 || targetNotes.length === 0) {
|
|
2648
|
+
lines.push(
|
|
2649
|
+
``,
|
|
2650
|
+
originNotes.length === 0 && targetNotes.length === 0 ? `(Both endpoints are empty \u2014 compose a short ${part} bridge from the chords alone.)` : originNotes.length === 0 ? `(The ORIGIN is empty \u2014 begin sparse and grow INTO the TARGET.)` : `(The TARGET is empty \u2014 begin from the ORIGIN and dissolve toward the destination chord.)`
|
|
2651
|
+
);
|
|
868
2652
|
}
|
|
869
|
-
return
|
|
2653
|
+
return lines.join("\n");
|
|
870
2654
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
2655
|
+
|
|
2656
|
+
// src/components/ImportTrackModal.tsx
|
|
2657
|
+
var import_react11 = require("react");
|
|
2658
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
2659
|
+
function ImportTrackModal({
|
|
2660
|
+
host,
|
|
2661
|
+
open,
|
|
2662
|
+
onClose,
|
|
2663
|
+
onImported,
|
|
2664
|
+
title = "Import track from scene (must match contract)",
|
|
2665
|
+
testIdPrefix = "import-track",
|
|
2666
|
+
mode = "track",
|
|
2667
|
+
onPick,
|
|
2668
|
+
onPortTrack
|
|
882
2669
|
}) {
|
|
883
|
-
const [
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
timerRef.current = setTimeout(tick, 1e3);
|
|
923
|
-
return 95;
|
|
924
|
-
}
|
|
925
|
-
const next = Math.min(calculateNextProgress(prev), 95);
|
|
926
|
-
onProgressChangeRef.current?.(next);
|
|
927
|
-
const interval = calculateNextTickInterval(next);
|
|
928
|
-
timerRef.current = setTimeout(tick, interval);
|
|
929
|
-
return next;
|
|
930
|
-
});
|
|
931
|
-
};
|
|
932
|
-
const firstInterval = calculateNextTickInterval(startProgress);
|
|
933
|
-
timerRef.current = setTimeout(tick, firstInterval);
|
|
2670
|
+
const [load, setLoad] = (0, import_react11.useState)({ status: "loading" });
|
|
2671
|
+
const [selectedSceneId, setSelectedSceneId] = (0, import_react11.useState)(null);
|
|
2672
|
+
const [importingTrackId, setImportingTrackId] = (0, import_react11.useState)(null);
|
|
2673
|
+
const refresh = (0, import_react11.useCallback)(async () => {
|
|
2674
|
+
if (!host.listImportableTracks) {
|
|
2675
|
+
setLoad({ status: "error", message: "This host does not support importing tracks." });
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
setLoad({ status: "loading" });
|
|
2679
|
+
try {
|
|
2680
|
+
const wantsPort = mode === "track" && !!onPortTrack;
|
|
2681
|
+
const scenes2 = await host.listImportableTracks(wantsPort ? { includeSameScene: true } : void 0);
|
|
2682
|
+
setLoad({ status: "ready", scenes: scenes2 });
|
|
2683
|
+
const sameScene = scenes2.find((s) => s.sameScene);
|
|
2684
|
+
if (sameScene) setSelectedSceneId(sameScene.sceneId);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
|
|
2687
|
+
}
|
|
2688
|
+
}, [host, mode, onPortTrack]);
|
|
2689
|
+
(0, import_react11.useEffect)(() => {
|
|
2690
|
+
if (open) {
|
|
2691
|
+
setSelectedSceneId(null);
|
|
2692
|
+
setImportingTrackId(null);
|
|
2693
|
+
void refresh();
|
|
2694
|
+
}
|
|
2695
|
+
}, [open, refresh]);
|
|
2696
|
+
const handleImport = (0, import_react11.useCallback)(
|
|
2697
|
+
async (track, sourceSceneId, sceneName, isSameScene) => {
|
|
2698
|
+
if (isSameScene && onPortTrack) {
|
|
2699
|
+
if (!track.importable) return;
|
|
2700
|
+
setImportingTrackId(track.trackId);
|
|
2701
|
+
try {
|
|
2702
|
+
await onPortTrack({ sourceTrackDbId: track.dbId, trackName: track.name, role: track.role });
|
|
2703
|
+
onClose();
|
|
2704
|
+
} catch (err) {
|
|
2705
|
+
host.showToast?.("error", err instanceof Error ? err.message : "Import failed");
|
|
2706
|
+
setImportingTrackId(null);
|
|
2707
|
+
}
|
|
2708
|
+
return;
|
|
934
2709
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
2710
|
+
if (mode === "sound") {
|
|
2711
|
+
setImportingTrackId(track.trackId);
|
|
2712
|
+
try {
|
|
2713
|
+
await onPick?.({ sourceTrackDbId: track.dbId, trackName: track.name, sceneName });
|
|
2714
|
+
onClose();
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
host.showToast?.("error", err instanceof Error ? err.message : "Import failed");
|
|
2717
|
+
setImportingTrackId(null);
|
|
2718
|
+
}
|
|
2719
|
+
return;
|
|
939
2720
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
2721
|
+
if (!track.importable || !host.importTrack) return;
|
|
2722
|
+
setImportingTrackId(track.trackId);
|
|
2723
|
+
try {
|
|
2724
|
+
const handle = await host.importTrack({ sourceSceneId, sourceTrackId: track.trackId });
|
|
2725
|
+
onImported(handle);
|
|
2726
|
+
onClose();
|
|
2727
|
+
} catch (err) {
|
|
2728
|
+
host.showToast?.("error", err instanceof Error ? err.message : "Import failed");
|
|
2729
|
+
setImportingTrackId(null);
|
|
949
2730
|
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
const transitionDuration = progress < 50 ? "300ms" : progress < 80 ? "500ms" : "700ms";
|
|
958
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
2731
|
+
},
|
|
2732
|
+
[host, onImported, onClose, mode, onPick, onPortTrack]
|
|
2733
|
+
);
|
|
2734
|
+
if (!open) return null;
|
|
2735
|
+
const scenes = load.status === "ready" ? load.scenes : [];
|
|
2736
|
+
const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
|
|
2737
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
959
2738
|
"div",
|
|
960
2739
|
{
|
|
961
|
-
className:
|
|
2740
|
+
className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
2741
|
+
onClick: (e) => e.stopPropagation(),
|
|
2742
|
+
"data-testid": `${testIdPrefix}-modal`,
|
|
962
2743
|
children: [
|
|
963
|
-
/* @__PURE__ */ (0,
|
|
964
|
-
"div",
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
2744
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
|
|
2745
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
2746
|
+
selectedScene && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2747
|
+
"button",
|
|
2748
|
+
{
|
|
2749
|
+
className: "text-sas-muted hover:text-sas-accent text-xs",
|
|
2750
|
+
onClick: () => setSelectedSceneId(null),
|
|
2751
|
+
"data-testid": `${testIdPrefix}-back`,
|
|
2752
|
+
children: "\u2190"
|
|
2753
|
+
}
|
|
2754
|
+
),
|
|
2755
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
|
|
2756
|
+
] }),
|
|
2757
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2758
|
+
"button",
|
|
2759
|
+
{
|
|
2760
|
+
className: "text-sas-muted hover:text-sas-accent text-sm",
|
|
2761
|
+
onClick: onClose,
|
|
2762
|
+
"data-testid": `${testIdPrefix}-close`,
|
|
2763
|
+
children: "\u2715"
|
|
978
2764
|
}
|
|
979
|
-
|
|
980
|
-
),
|
|
981
|
-
/* @__PURE__ */ (0,
|
|
982
|
-
|
|
2765
|
+
)
|
|
2766
|
+
] }),
|
|
2767
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
|
|
2768
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
|
|
2769
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
2770
|
+
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
|
|
2771
|
+
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2772
|
+
"button",
|
|
2773
|
+
{
|
|
2774
|
+
className: "w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
|
|
2775
|
+
onClick: () => setSelectedSceneId(scene.sceneId),
|
|
2776
|
+
"data-testid": `${testIdPrefix}-scene`,
|
|
2777
|
+
children: [
|
|
2778
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "truncate", children: scene.sceneName }),
|
|
2779
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "text-sas-muted", children: [
|
|
2780
|
+
scene.tracks.length,
|
|
2781
|
+
" \u2192"
|
|
2782
|
+
] })
|
|
2783
|
+
]
|
|
2784
|
+
}
|
|
2785
|
+
) }, scene.sceneId)) }),
|
|
2786
|
+
selectedScene && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
|
|
2787
|
+
const busy = importingTrackId === track.trackId;
|
|
2788
|
+
const gated = mode === "track" && !track.importable;
|
|
2789
|
+
const disabled = gated || busy;
|
|
2790
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2791
|
+
"button",
|
|
2792
|
+
{
|
|
2793
|
+
className: `w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${disabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent"}`,
|
|
2794
|
+
disabled,
|
|
2795
|
+
title: gated ? track.disabledReason : void 0,
|
|
2796
|
+
onClick: () => void handleImport(track, selectedScene.sceneId, selectedScene.sceneName, !!selectedScene.sameScene),
|
|
2797
|
+
"data-testid": `${testIdPrefix}-track`,
|
|
2798
|
+
"data-importable": mode === "sound" || track.importable ? "true" : "false",
|
|
2799
|
+
children: [
|
|
2800
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "truncate", children: [
|
|
2801
|
+
track.name,
|
|
2802
|
+
track.role ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "text-sas-muted", children: [
|
|
2803
|
+
" \xB7 ",
|
|
2804
|
+
track.role
|
|
2805
|
+
] }) : null
|
|
2806
|
+
] }),
|
|
2807
|
+
busy ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
|
|
2808
|
+
]
|
|
2809
|
+
}
|
|
2810
|
+
) }, track.dbId);
|
|
2811
|
+
}) })
|
|
2812
|
+
] })
|
|
2813
|
+
]
|
|
2814
|
+
}
|
|
2815
|
+
) });
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// src/components/CrossfadeModal.tsx
|
|
2819
|
+
var import_react12 = require("react");
|
|
2820
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2821
|
+
function CrossfadeModal({
|
|
2822
|
+
host,
|
|
2823
|
+
open,
|
|
2824
|
+
fromSceneId,
|
|
2825
|
+
toSceneId,
|
|
2826
|
+
fromSceneName,
|
|
2827
|
+
toSceneName,
|
|
2828
|
+
onClose,
|
|
2829
|
+
onCreate,
|
|
2830
|
+
testIdPrefix = "crossfade-modal"
|
|
2831
|
+
}) {
|
|
2832
|
+
const [load, setLoad] = (0, import_react12.useState)({ status: "loading" });
|
|
2833
|
+
const [originDbId, setOriginDbId] = (0, import_react12.useState)("");
|
|
2834
|
+
const [targetDbId, setTargetDbId] = (0, import_react12.useState)("");
|
|
2835
|
+
const [isCreating, setIsCreating] = (0, import_react12.useState)(false);
|
|
2836
|
+
const [error, setError] = (0, import_react12.useState)(null);
|
|
2837
|
+
const cancelRef = (0, import_react12.useRef)(null);
|
|
2838
|
+
const refresh = (0, import_react12.useCallback)(async () => {
|
|
2839
|
+
if (!host.listSceneFamilyTracks) {
|
|
2840
|
+
setLoad({ status: "error", message: "This host does not support crossfade tracks." });
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
setLoad({ status: "loading" });
|
|
2844
|
+
try {
|
|
2845
|
+
const [origin, target] = await Promise.all([
|
|
2846
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
2847
|
+
host.listSceneFamilyTracks(toSceneId)
|
|
2848
|
+
]);
|
|
2849
|
+
setLoad({ status: "ready", origin, target });
|
|
2850
|
+
setOriginDbId(origin[0]?.dbId ?? "");
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2853
|
+
}
|
|
2854
|
+
}, [host, fromSceneId, toSceneId]);
|
|
2855
|
+
(0, import_react12.useEffect)(() => {
|
|
2856
|
+
if (open) {
|
|
2857
|
+
setError(null);
|
|
2858
|
+
setIsCreating(false);
|
|
2859
|
+
setOriginDbId("");
|
|
2860
|
+
setTargetDbId("");
|
|
2861
|
+
void refresh();
|
|
2862
|
+
}
|
|
2863
|
+
}, [open, refresh]);
|
|
2864
|
+
const originTrack = (0, import_react12.useMemo)(
|
|
2865
|
+
() => load.status === "ready" ? load.origin.find((t) => t.dbId === originDbId) ?? null : null,
|
|
2866
|
+
[load, originDbId]
|
|
2867
|
+
);
|
|
2868
|
+
const originRole = originTrack?.role;
|
|
2869
|
+
const targetCandidates = (0, import_react12.useMemo)(() => {
|
|
2870
|
+
if (load.status !== "ready") return [];
|
|
2871
|
+
if (!originRole) return load.target;
|
|
2872
|
+
return load.target.filter((t) => t.role === originRole);
|
|
2873
|
+
}, [load, originRole]);
|
|
2874
|
+
(0, import_react12.useEffect)(() => {
|
|
2875
|
+
if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
|
|
2876
|
+
setTargetDbId(targetCandidates[0]?.dbId ?? "");
|
|
2877
|
+
}
|
|
2878
|
+
}, [targetCandidates, targetDbId]);
|
|
2879
|
+
const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
|
|
2880
|
+
const canCreate = !isCreating && !!originTrack && !!targetTrack;
|
|
2881
|
+
const handleClose = (0, import_react12.useCallback)(() => {
|
|
2882
|
+
if (!isCreating) onClose();
|
|
2883
|
+
}, [isCreating, onClose]);
|
|
2884
|
+
const handleCreate = (0, import_react12.useCallback)(async () => {
|
|
2885
|
+
if (!originTrack || !targetTrack) return;
|
|
2886
|
+
setIsCreating(true);
|
|
2887
|
+
setError(null);
|
|
2888
|
+
try {
|
|
2889
|
+
await onCreate(
|
|
2890
|
+
{ dbId: originTrack.dbId, name: originTrack.name, role: originTrack.role },
|
|
2891
|
+
{ dbId: targetTrack.dbId, name: targetTrack.name, role: targetTrack.role }
|
|
2892
|
+
);
|
|
2893
|
+
onClose();
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
setError(err instanceof Error ? err.message : "Failed to create crossfade.");
|
|
2896
|
+
setIsCreating(false);
|
|
2897
|
+
}
|
|
2898
|
+
}, [originTrack, targetTrack, onCreate, onClose]);
|
|
2899
|
+
if (!open) return null;
|
|
2900
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
2901
|
+
"div",
|
|
2902
|
+
{
|
|
2903
|
+
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2904
|
+
onClick: (e) => e.stopPropagation(),
|
|
2905
|
+
"data-testid": `${testIdPrefix}-box`,
|
|
2906
|
+
children: [
|
|
2907
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
|
|
2908
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2909
|
+
"Bridge a track from",
|
|
983
2910
|
" ",
|
|
984
|
-
|
|
985
|
-
"
|
|
986
|
-
|
|
987
|
-
|
|
2911
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-sas-text", children: fromSceneName ?? "the origin scene" }),
|
|
2912
|
+
" into one from",
|
|
2913
|
+
" ",
|
|
2914
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-sas-text", children: toSceneName ?? "the target scene" }),
|
|
2915
|
+
". Both layers share one generated part; each keeps its own preset."
|
|
2916
|
+
] }),
|
|
2917
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
2918
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
2919
|
+
load.status === "ready" && (load.origin.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
988
2920
|
"div",
|
|
989
2921
|
{
|
|
990
|
-
className: "
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
2922
|
+
className: "text-xs text-sas-muted py-4 text-center",
|
|
2923
|
+
"data-testid": `${testIdPrefix}-empty-origin`,
|
|
2924
|
+
children: "No matching tracks in the origin scene. Add one there first."
|
|
2925
|
+
}
|
|
2926
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
2927
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("label", { className: "block", children: [
|
|
2928
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "Origin (top)" }),
|
|
2929
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2930
|
+
"select",
|
|
2931
|
+
{
|
|
2932
|
+
"data-testid": `${testIdPrefix}-origin-select`,
|
|
2933
|
+
value: originDbId,
|
|
2934
|
+
onChange: (e) => setOriginDbId(e.target.value),
|
|
2935
|
+
disabled: isCreating,
|
|
2936
|
+
className: "sas-input w-full mt-0.5 text-xs",
|
|
2937
|
+
children: load.origin.map((t) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("option", { value: t.dbId, children: [
|
|
2938
|
+
t.name,
|
|
2939
|
+
t.role ? ` \xB7 ${t.role}` : ""
|
|
2940
|
+
] }, t.dbId))
|
|
2941
|
+
}
|
|
2942
|
+
)
|
|
2943
|
+
] }),
|
|
2944
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("label", { className: "block", children: [
|
|
2945
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2946
|
+
"Target (bottom)",
|
|
2947
|
+
originRole ? ` \xB7 ${originRole}` : ""
|
|
2948
|
+
] }),
|
|
2949
|
+
targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
|
|
2950
|
+
"No ",
|
|
2951
|
+
originRole ?? "matching",
|
|
2952
|
+
" track in the target scene to crossfade into."
|
|
2953
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2954
|
+
"select",
|
|
2955
|
+
{
|
|
2956
|
+
"data-testid": `${testIdPrefix}-target-select`,
|
|
2957
|
+
value: targetDbId,
|
|
2958
|
+
onChange: (e) => setTargetDbId(e.target.value),
|
|
2959
|
+
disabled: isCreating,
|
|
2960
|
+
className: "sas-input w-full mt-0.5 text-xs",
|
|
2961
|
+
children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("option", { value: t.dbId, children: [
|
|
2962
|
+
t.name,
|
|
2963
|
+
t.role ? ` \xB7 ${t.role}` : ""
|
|
2964
|
+
] }, t.dbId))
|
|
2965
|
+
}
|
|
2966
|
+
)
|
|
2967
|
+
] })
|
|
2968
|
+
] })),
|
|
2969
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
2970
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
2971
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2972
|
+
"button",
|
|
2973
|
+
{
|
|
2974
|
+
ref: cancelRef,
|
|
2975
|
+
"data-testid": `${testIdPrefix}-cancel`,
|
|
2976
|
+
onClick: onClose,
|
|
2977
|
+
disabled: isCreating,
|
|
2978
|
+
className: "px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50",
|
|
2979
|
+
children: "Cancel"
|
|
2980
|
+
}
|
|
2981
|
+
),
|
|
2982
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2983
|
+
"button",
|
|
2984
|
+
{
|
|
2985
|
+
"data-testid": `${testIdPrefix}-confirm`,
|
|
2986
|
+
onClick: handleCreate,
|
|
2987
|
+
disabled: !canCreate,
|
|
2988
|
+
className: `px-3 py-1 text-xs rounded-sm border transition-colors ${canCreate ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
2989
|
+
children: isCreating ? "Generating bridge\u2026" : "Create crossfade"
|
|
999
2990
|
}
|
|
2991
|
+
)
|
|
2992
|
+
] })
|
|
2993
|
+
]
|
|
2994
|
+
}
|
|
2995
|
+
) });
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// src/components/DownloadPackButton.tsx
|
|
2999
|
+
var import_react13 = require("react");
|
|
3000
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
3001
|
+
function formatSize(bytes) {
|
|
3002
|
+
if (!bytes || bytes <= 0) return "";
|
|
3003
|
+
const gb = bytes / 1024 ** 3;
|
|
3004
|
+
if (gb >= 1) return `${gb.toFixed(1)} GB`;
|
|
3005
|
+
const mb = bytes / 1024 ** 2;
|
|
3006
|
+
return `${Math.round(mb)} MB`;
|
|
3007
|
+
}
|
|
3008
|
+
var DownloadPackButton = ({
|
|
3009
|
+
host,
|
|
3010
|
+
packId,
|
|
3011
|
+
displayName,
|
|
3012
|
+
sizeBytes,
|
|
3013
|
+
variant = "compact",
|
|
3014
|
+
onDownloadComplete
|
|
3015
|
+
}) => {
|
|
3016
|
+
const [status, setStatus] = (0, import_react13.useState)("idle");
|
|
3017
|
+
const [progress, setProgress] = (0, import_react13.useState)(0);
|
|
3018
|
+
const [errorMessage, setErrorMessage] = (0, import_react13.useState)(null);
|
|
3019
|
+
(0, import_react13.useEffect)(() => {
|
|
3020
|
+
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
3021
|
+
setStatus(p.status);
|
|
3022
|
+
setProgress(p.progress);
|
|
3023
|
+
if (p.status === "error") {
|
|
3024
|
+
setErrorMessage(p.message || "Download failed");
|
|
3025
|
+
} else if (p.status === "complete") {
|
|
3026
|
+
setErrorMessage(null);
|
|
3027
|
+
setTimeout(() => onDownloadComplete?.(), 250);
|
|
3028
|
+
} else {
|
|
3029
|
+
setErrorMessage(null);
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
return unsub;
|
|
3033
|
+
}, [host, packId, onDownloadComplete]);
|
|
3034
|
+
const handleClick = (0, import_react13.useCallback)(async () => {
|
|
3035
|
+
if (status !== "idle" && status !== "error") return;
|
|
3036
|
+
try {
|
|
3037
|
+
setStatus("downloading");
|
|
3038
|
+
setProgress(0);
|
|
3039
|
+
setErrorMessage(null);
|
|
3040
|
+
const result = await host.startSamplePackDownload(packId);
|
|
3041
|
+
if (!result.success) {
|
|
3042
|
+
setStatus("error");
|
|
3043
|
+
setErrorMessage(result.error || "Download failed");
|
|
3044
|
+
}
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
console.error("[DownloadPackButton] start failed:", err);
|
|
3047
|
+
setStatus("error");
|
|
3048
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
3049
|
+
}
|
|
3050
|
+
}, [host, packId, status]);
|
|
3051
|
+
const isWorking = status === "downloading" || status === "verifying" || status === "extracting" || status === "installing";
|
|
3052
|
+
const isDisabled = isWorking || status === "complete";
|
|
3053
|
+
const buttonLabel = (() => {
|
|
3054
|
+
switch (status) {
|
|
3055
|
+
case "downloading":
|
|
3056
|
+
return `${progress}%`;
|
|
3057
|
+
case "verifying":
|
|
3058
|
+
return "Verifying...";
|
|
3059
|
+
case "extracting":
|
|
3060
|
+
return "Extracting...";
|
|
3061
|
+
case "installing":
|
|
3062
|
+
return "Installing...";
|
|
3063
|
+
case "complete":
|
|
3064
|
+
return "Done!";
|
|
3065
|
+
case "error":
|
|
3066
|
+
return "Retry";
|
|
3067
|
+
default:
|
|
3068
|
+
return variant === "large" ? `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ""}` : "Download";
|
|
3069
|
+
}
|
|
3070
|
+
})();
|
|
3071
|
+
const tooltip = (() => {
|
|
3072
|
+
if (status === "error") return errorMessage || "Download failed. Click to retry.";
|
|
3073
|
+
if (isWorking) return `${buttonLabel} \u2014 ${displayName}`;
|
|
3074
|
+
if (status === "complete") return "Installation complete";
|
|
3075
|
+
return `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ""}`;
|
|
3076
|
+
})();
|
|
3077
|
+
const baseClasses = variant === "large" ? "px-4 py-2 text-sm font-medium rounded border transition-colors" : "px-2 py-0.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors";
|
|
3078
|
+
let className;
|
|
3079
|
+
if (status === "error") {
|
|
3080
|
+
className = `${baseClasses} text-red-400 border-red-400/50 hover:text-red-300 hover:border-red-300`;
|
|
3081
|
+
} else if (status === "complete") {
|
|
3082
|
+
className = `${baseClasses} text-green-400 border-green-400/50`;
|
|
3083
|
+
} else if (isDisabled) {
|
|
3084
|
+
className = `${baseClasses} text-sas-accent border-sas-accent/50 cursor-wait`;
|
|
3085
|
+
} else {
|
|
3086
|
+
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3087
|
+
}
|
|
3088
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
|
|
3089
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3090
|
+
"button",
|
|
3091
|
+
{
|
|
3092
|
+
"data-testid": `download-pack-button-${packId}`,
|
|
3093
|
+
onClick: handleClick,
|
|
3094
|
+
disabled: isDisabled,
|
|
3095
|
+
className,
|
|
3096
|
+
title: tooltip,
|
|
3097
|
+
children: buttonLabel
|
|
3098
|
+
}
|
|
3099
|
+
),
|
|
3100
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3101
|
+
] });
|
|
3102
|
+
};
|
|
3103
|
+
|
|
3104
|
+
// src/components/SamplePackCTACard.tsx
|
|
3105
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
3106
|
+
var SamplePackCTACard = ({
|
|
3107
|
+
host,
|
|
3108
|
+
pack,
|
|
3109
|
+
status,
|
|
3110
|
+
onDownloadComplete
|
|
3111
|
+
}) => {
|
|
3112
|
+
if (status === "checking") {
|
|
3113
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3114
|
+
"div",
|
|
3115
|
+
{
|
|
3116
|
+
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
3117
|
+
className: "flex items-center justify-center py-16 text-sas-muted text-sm",
|
|
3118
|
+
children: "Checking sample library..."
|
|
3119
|
+
}
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
3123
|
+
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
3124
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
3125
|
+
"div",
|
|
3126
|
+
{
|
|
3127
|
+
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3128
|
+
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3129
|
+
children: [
|
|
3130
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
3131
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
3132
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
3133
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3134
|
+
DownloadPackButton,
|
|
3135
|
+
{
|
|
3136
|
+
host,
|
|
3137
|
+
packId: pack.packId,
|
|
3138
|
+
displayName: pack.displayName,
|
|
3139
|
+
sizeBytes: pack.sizeBytes,
|
|
3140
|
+
variant: "large",
|
|
3141
|
+
onDownloadComplete
|
|
1000
3142
|
}
|
|
1001
3143
|
)
|
|
1002
3144
|
]
|
|
1003
3145
|
}
|
|
1004
3146
|
);
|
|
3147
|
+
};
|
|
3148
|
+
|
|
3149
|
+
// src/components/WaveformView.tsx
|
|
3150
|
+
var import_react14 = require("react");
|
|
3151
|
+
|
|
3152
|
+
// src/components/waveform.ts
|
|
3153
|
+
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
3154
|
+
const { length, numberOfChannels, sampleRate } = audioBuffer;
|
|
3155
|
+
const channels = [];
|
|
3156
|
+
for (let c = 0; c < numberOfChannels; c++) {
|
|
3157
|
+
channels.push(audioBuffer.getChannelData(c));
|
|
3158
|
+
}
|
|
3159
|
+
const totalForBinning = typeof targetSamples === "number" && targetSamples > length ? targetSamples : length;
|
|
3160
|
+
const samplesPerBin = Math.max(1, Math.floor(totalForBinning / bins));
|
|
3161
|
+
const out = new Float32Array(bins * 2);
|
|
3162
|
+
for (let i = 0; i < bins; i++) {
|
|
3163
|
+
const startIdx = i * samplesPerBin;
|
|
3164
|
+
const endIdx = Math.min(length, startIdx + samplesPerBin);
|
|
3165
|
+
if (startIdx >= length) {
|
|
3166
|
+
out[i * 2] = 0;
|
|
3167
|
+
out[i * 2 + 1] = 0;
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
let mn = Infinity;
|
|
3171
|
+
let mx = -Infinity;
|
|
3172
|
+
for (let j = startIdx; j < endIdx; j++) {
|
|
3173
|
+
let v = 0;
|
|
3174
|
+
for (let c = 0; c < numberOfChannels; c++) {
|
|
3175
|
+
v += channels[c][j];
|
|
3176
|
+
}
|
|
3177
|
+
v /= numberOfChannels;
|
|
3178
|
+
if (v < mn) mn = v;
|
|
3179
|
+
if (v > mx) mx = v;
|
|
3180
|
+
}
|
|
3181
|
+
if (!Number.isFinite(mn)) mn = 0;
|
|
3182
|
+
if (!Number.isFinite(mx)) mx = 0;
|
|
3183
|
+
out[i * 2] = mn;
|
|
3184
|
+
out[i * 2 + 1] = mx;
|
|
3185
|
+
}
|
|
3186
|
+
return { sampleRate, totalSamples: totalForBinning, peaks: out };
|
|
3187
|
+
}
|
|
3188
|
+
function drawWaveform(canvas, peaks, options = {}) {
|
|
3189
|
+
const dpr = window.devicePixelRatio || 1;
|
|
3190
|
+
const cssWidth = canvas.clientWidth;
|
|
3191
|
+
const cssHeight = canvas.clientHeight;
|
|
3192
|
+
if (cssWidth === 0 || cssHeight === 0) return;
|
|
3193
|
+
canvas.width = Math.floor(cssWidth * dpr);
|
|
3194
|
+
canvas.height = Math.floor(cssHeight * dpr);
|
|
3195
|
+
const ctx = canvas.getContext("2d");
|
|
3196
|
+
if (!ctx) return;
|
|
3197
|
+
ctx.scale(dpr, dpr);
|
|
3198
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
3199
|
+
ctx.fillStyle = options.fillStyle ?? "rgba(255, 255, 255, 0.4)";
|
|
3200
|
+
const bins = peaks.peaks.length / 2;
|
|
3201
|
+
const mid = cssHeight / 2;
|
|
3202
|
+
for (let x = 0; x < cssWidth; x++) {
|
|
3203
|
+
const binIdx = Math.floor(x / cssWidth * bins);
|
|
3204
|
+
const mn = peaks.peaks[binIdx * 2];
|
|
3205
|
+
const mx = peaks.peaks[binIdx * 2 + 1];
|
|
3206
|
+
const yTop = mid - mx * mid;
|
|
3207
|
+
const yBot = mid - mn * mid;
|
|
3208
|
+
ctx.fillRect(x, yTop, 1, Math.max(1, yBot - yTop));
|
|
3209
|
+
}
|
|
1005
3210
|
}
|
|
1006
3211
|
|
|
1007
|
-
// src/components/
|
|
1008
|
-
var
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
3212
|
+
// src/components/WaveformView.tsx
|
|
3213
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3214
|
+
var WaveformView = ({
|
|
3215
|
+
host,
|
|
3216
|
+
filePath,
|
|
3217
|
+
bins = 256,
|
|
3218
|
+
className,
|
|
3219
|
+
fillStyle,
|
|
3220
|
+
targetSamples
|
|
3221
|
+
}) => {
|
|
3222
|
+
const canvasRef = (0, import_react14.useRef)(null);
|
|
3223
|
+
const [peaks, setPeaks] = (0, import_react14.useState)(null);
|
|
3224
|
+
(0, import_react14.useEffect)(() => {
|
|
3225
|
+
let cancelled = false;
|
|
3226
|
+
let audioContext = null;
|
|
3227
|
+
(async () => {
|
|
3228
|
+
try {
|
|
3229
|
+
const bytes = await host.getAudioFileBytes(filePath);
|
|
3230
|
+
if (cancelled) return;
|
|
3231
|
+
const ContextCtor = window.AudioContext ?? window.webkitAudioContext;
|
|
3232
|
+
audioContext = new ContextCtor();
|
|
3233
|
+
const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));
|
|
3234
|
+
if (cancelled) return;
|
|
3235
|
+
const computed = computePeaks(audioBuffer, bins, targetSamples);
|
|
3236
|
+
setPeaks(computed);
|
|
3237
|
+
} catch (err) {
|
|
3238
|
+
console.warn("[WaveformView] failed to decode", filePath, err);
|
|
3239
|
+
} finally {
|
|
3240
|
+
if (audioContext) {
|
|
3241
|
+
audioContext.close().catch(() => {
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
})();
|
|
3246
|
+
return () => {
|
|
3247
|
+
cancelled = true;
|
|
3248
|
+
};
|
|
3249
|
+
}, [host, filePath, bins, targetSamples]);
|
|
3250
|
+
(0, import_react14.useEffect)(() => {
|
|
3251
|
+
if (!peaks) return;
|
|
3252
|
+
const canvas = canvasRef.current;
|
|
3253
|
+
if (!canvas) return;
|
|
3254
|
+
drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : void 0);
|
|
3255
|
+
const observer = new ResizeObserver(() => {
|
|
3256
|
+
drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : void 0);
|
|
3257
|
+
});
|
|
3258
|
+
observer.observe(canvas);
|
|
3259
|
+
return () => observer.disconnect();
|
|
3260
|
+
}, [peaks, fillStyle]);
|
|
3261
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3262
|
+
"canvas",
|
|
3263
|
+
{
|
|
3264
|
+
ref: canvasRef,
|
|
3265
|
+
"data-testid": "waveform-view",
|
|
3266
|
+
className: className ?? "w-full h-10"
|
|
3267
|
+
}
|
|
3268
|
+
);
|
|
3269
|
+
};
|
|
3270
|
+
|
|
3271
|
+
// src/components/ScrollingWaveform.tsx
|
|
3272
|
+
var import_react15 = require("react");
|
|
3273
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3274
|
+
var ScrollingWaveform = ({
|
|
3275
|
+
getPeakDb,
|
|
3276
|
+
active,
|
|
3277
|
+
columns = 256,
|
|
3278
|
+
className,
|
|
3279
|
+
fillStyle
|
|
3280
|
+
}) => {
|
|
3281
|
+
const canvasRef = (0, import_react15.useRef)(null);
|
|
3282
|
+
const ringRef = (0, import_react15.useRef)(new Float32Array(columns));
|
|
3283
|
+
const writeIdxRef = (0, import_react15.useRef)(0);
|
|
3284
|
+
const rafRef = (0, import_react15.useRef)(null);
|
|
3285
|
+
(0, import_react15.useEffect)(() => {
|
|
3286
|
+
if (ringRef.current.length !== columns) {
|
|
3287
|
+
const next = new Float32Array(columns);
|
|
3288
|
+
const prev = ringRef.current;
|
|
3289
|
+
const copyLen = Math.min(prev.length, columns);
|
|
3290
|
+
for (let i = 0; i < copyLen; i++) {
|
|
3291
|
+
next[i] = prev[i];
|
|
3292
|
+
}
|
|
3293
|
+
ringRef.current = next;
|
|
3294
|
+
writeIdxRef.current = writeIdxRef.current % columns;
|
|
3295
|
+
}
|
|
3296
|
+
}, [columns]);
|
|
3297
|
+
(0, import_react15.useEffect)(() => {
|
|
3298
|
+
if (!active) {
|
|
3299
|
+
if (rafRef.current !== null) {
|
|
3300
|
+
cancelAnimationFrame(rafRef.current);
|
|
3301
|
+
rafRef.current = null;
|
|
3302
|
+
}
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
const tick = () => {
|
|
3306
|
+
const peakDb = getPeakDb();
|
|
3307
|
+
const amp = peakDb <= -120 ? 0 : Math.max(0, Math.min(1, (peakDb + 60) / 60));
|
|
3308
|
+
const ring = ringRef.current;
|
|
3309
|
+
ring[writeIdxRef.current] = amp;
|
|
3310
|
+
writeIdxRef.current = (writeIdxRef.current + 1) % ring.length;
|
|
3311
|
+
const canvas = canvasRef.current;
|
|
3312
|
+
if (canvas) {
|
|
3313
|
+
const dpr = window.devicePixelRatio || 1;
|
|
3314
|
+
const cssW = canvas.clientWidth;
|
|
3315
|
+
const cssH = canvas.clientHeight;
|
|
3316
|
+
if (cssW > 0 && cssH > 0) {
|
|
3317
|
+
if (canvas.width !== Math.floor(cssW * dpr) || canvas.height !== Math.floor(cssH * dpr)) {
|
|
3318
|
+
canvas.width = Math.floor(cssW * dpr);
|
|
3319
|
+
canvas.height = Math.floor(cssH * dpr);
|
|
3320
|
+
}
|
|
3321
|
+
const ctx = canvas.getContext("2d");
|
|
3322
|
+
if (ctx) {
|
|
3323
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3324
|
+
ctx.clearRect(0, 0, cssW, cssH);
|
|
3325
|
+
ctx.fillStyle = fillStyle ?? "#6af2c5";
|
|
3326
|
+
const mid = cssH / 2;
|
|
3327
|
+
const cols = ring.length;
|
|
3328
|
+
const colW = cssW / cols;
|
|
3329
|
+
const start = writeIdxRef.current;
|
|
3330
|
+
for (let x = 0; x < cols; x++) {
|
|
3331
|
+
const ringIdx = (start + x) % cols;
|
|
3332
|
+
const a = ring[ringIdx];
|
|
3333
|
+
const half = a * mid;
|
|
3334
|
+
ctx.fillRect(x * colW, mid - half, Math.max(1, colW), Math.max(1, half * 2));
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
3340
|
+
};
|
|
3341
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
3342
|
+
return () => {
|
|
3343
|
+
if (rafRef.current !== null) {
|
|
3344
|
+
cancelAnimationFrame(rafRef.current);
|
|
3345
|
+
rafRef.current = null;
|
|
3346
|
+
}
|
|
3347
|
+
};
|
|
3348
|
+
}, [active, getPeakDb, fillStyle]);
|
|
3349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3350
|
+
"canvas",
|
|
3351
|
+
{
|
|
3352
|
+
ref: canvasRef,
|
|
3353
|
+
"data-testid": "scrolling-waveform",
|
|
3354
|
+
className: className ?? "w-full h-12"
|
|
3355
|
+
}
|
|
3356
|
+
);
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
// src/components/OffsetScrubber.tsx
|
|
3360
|
+
var import_react16 = require("react");
|
|
3361
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3362
|
+
var SLIDER_HEIGHT_PX = 28;
|
|
3363
|
+
var TICK_HEIGHT_PX = 14;
|
|
3364
|
+
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
3365
|
+
var THUMB_WIDTH_PX = 4;
|
|
3366
|
+
function OffsetScrubber({
|
|
3367
|
+
cuePoints,
|
|
3368
|
+
offsetSamples,
|
|
3369
|
+
projectBpm,
|
|
3370
|
+
meter = 4,
|
|
3371
|
+
onChange,
|
|
3372
|
+
disabled = false
|
|
1049
3373
|
}) {
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
|
|
3374
|
+
const trackRef = (0, import_react16.useRef)(null);
|
|
3375
|
+
const [draftOffset, setDraftOffset] = (0, import_react16.useState)(offsetSamples);
|
|
3376
|
+
const [isDragging, setIsDragging] = (0, import_react16.useState)(false);
|
|
3377
|
+
(0, import_react16.useEffect)(() => {
|
|
3378
|
+
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3379
|
+
}, [offsetSamples, isDragging]);
|
|
3380
|
+
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3381
|
+
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3382
|
+
const beatsForRange = (0, import_react16.useMemo)(() => {
|
|
3383
|
+
return Math.round(60 / projectBpm * sampleRate);
|
|
3384
|
+
}, [projectBpm, sampleRate]);
|
|
3385
|
+
const rangeSamples = beatsForRange * meter;
|
|
3386
|
+
const sampleToFraction = (0, import_react16.useCallback)(
|
|
3387
|
+
(sample) => {
|
|
3388
|
+
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3389
|
+
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
3390
|
+
},
|
|
3391
|
+
[rangeSamples]
|
|
1054
3392
|
);
|
|
1055
|
-
const
|
|
1056
|
-
|
|
3393
|
+
const fractionToSample = (0, import_react16.useCallback)(
|
|
3394
|
+
(fraction) => {
|
|
3395
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3396
|
+
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
3397
|
+
},
|
|
3398
|
+
[rangeSamples]
|
|
3399
|
+
);
|
|
3400
|
+
const snapTargets = (0, import_react16.useMemo)(() => {
|
|
3401
|
+
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
3402
|
+
const downbeat = cuePoints.beats[0];
|
|
3403
|
+
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3404
|
+
const negatives = positives.slice(1).map((p) => -p);
|
|
3405
|
+
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3406
|
+
}, [cuePoints]);
|
|
3407
|
+
const snapToBeat = (0, import_react16.useCallback)(
|
|
3408
|
+
(sample) => {
|
|
3409
|
+
if (snapTargets.length === 0) return sample;
|
|
3410
|
+
let best = snapTargets[0];
|
|
3411
|
+
let bestDist = Math.abs(sample - best);
|
|
3412
|
+
for (const t of snapTargets) {
|
|
3413
|
+
const d = Math.abs(sample - t);
|
|
3414
|
+
if (d < bestDist) {
|
|
3415
|
+
best = t;
|
|
3416
|
+
bestDist = d;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
return best;
|
|
3420
|
+
},
|
|
3421
|
+
[snapTargets]
|
|
3422
|
+
);
|
|
3423
|
+
const handlePointerDown = (0, import_react16.useCallback)(
|
|
3424
|
+
(e) => {
|
|
3425
|
+
if (disabled || !cuePoints) return;
|
|
1057
3426
|
e.preventDefault();
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
3427
|
+
const track = trackRef.current;
|
|
3428
|
+
if (!track) return;
|
|
3429
|
+
track.setPointerCapture(e.pointerId);
|
|
3430
|
+
setIsDragging(true);
|
|
3431
|
+
const updateFromEvent = (clientX, shiftHeld) => {
|
|
3432
|
+
const rect = track.getBoundingClientRect();
|
|
3433
|
+
const fraction = (clientX - rect.left) / rect.width;
|
|
3434
|
+
const raw = fractionToSample(fraction);
|
|
3435
|
+
return shiftHeld ? raw : snapToBeat(raw);
|
|
3436
|
+
};
|
|
3437
|
+
setDraftOffset(updateFromEvent(e.clientX, e.shiftKey));
|
|
3438
|
+
const onMove = (ev) => {
|
|
3439
|
+
setDraftOffset(updateFromEvent(ev.clientX, ev.shiftKey));
|
|
3440
|
+
};
|
|
3441
|
+
const onUp = (ev) => {
|
|
3442
|
+
const final = updateFromEvent(ev.clientX, ev.shiftKey);
|
|
3443
|
+
track.releasePointerCapture(e.pointerId);
|
|
3444
|
+
track.removeEventListener("pointermove", onMove);
|
|
3445
|
+
track.removeEventListener("pointerup", onUp);
|
|
3446
|
+
track.removeEventListener("pointercancel", onUp);
|
|
3447
|
+
setIsDragging(false);
|
|
3448
|
+
setDraftOffset(final);
|
|
3449
|
+
onChange(final);
|
|
3450
|
+
};
|
|
3451
|
+
track.addEventListener("pointermove", onMove);
|
|
3452
|
+
track.addEventListener("pointerup", onUp);
|
|
3453
|
+
track.addEventListener("pointercancel", onUp);
|
|
3454
|
+
},
|
|
3455
|
+
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
3456
|
+
);
|
|
3457
|
+
const handleResetToZero = (0, import_react16.useCallback)(() => {
|
|
3458
|
+
if (disabled) return;
|
|
3459
|
+
setDraftOffset(0);
|
|
3460
|
+
onChange(0);
|
|
3461
|
+
}, [disabled, onChange]);
|
|
3462
|
+
const thumbFraction = sampleToFraction(draftOffset);
|
|
3463
|
+
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
3464
|
+
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
3465
|
+
const ticks = (0, import_react16.useMemo)(() => {
|
|
3466
|
+
if (!cuePoints) return [];
|
|
3467
|
+
const downbeat = cuePoints.beats[0] ?? 0;
|
|
3468
|
+
return cuePoints.beats.map((b, i) => {
|
|
3469
|
+
const offsetCandidate = b - downbeat;
|
|
3470
|
+
const fraction = sampleToFraction(offsetCandidate);
|
|
3471
|
+
const isDownbeat = i === 0;
|
|
3472
|
+
return { i, fraction, isDownbeat };
|
|
3473
|
+
});
|
|
3474
|
+
}, [cuePoints, sampleToFraction]);
|
|
3475
|
+
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
3476
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
3477
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
3478
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
1065
3479
|
"div",
|
|
1066
3480
|
{
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
3481
|
+
ref: trackRef,
|
|
3482
|
+
"data-testid": "offset-scrubber-track",
|
|
3483
|
+
onPointerDown: handlePointerDown,
|
|
3484
|
+
className: `relative flex-1 min-w-0 rounded-sm select-none ${isDisabled ? "bg-sas-panel cursor-not-allowed opacity-40" : "bg-sas-bg cursor-pointer"}`,
|
|
3485
|
+
style: { height: SLIDER_HEIGHT_PX },
|
|
3486
|
+
title: isDisabled ? "Generate audio first to enable offset alignment" : "Drag to align beat 1. Hold Shift for free, no-snap movement.",
|
|
3487
|
+
role: "slider",
|
|
3488
|
+
"aria-label": "Audio offset alignment",
|
|
3489
|
+
"aria-valuemin": -rangeSamples,
|
|
3490
|
+
"aria-valuemax": rangeSamples,
|
|
3491
|
+
"aria-valuenow": draftOffset,
|
|
3492
|
+
"aria-disabled": isDisabled,
|
|
1073
3493
|
children: [
|
|
1074
|
-
|
|
1075
|
-
|
|
3494
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3495
|
+
"div",
|
|
1076
3496
|
{
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
initialProgress: generationProgress,
|
|
1081
|
-
onProgressChange,
|
|
1082
|
-
estimatedDurationMs: estimatedGenerationMs
|
|
3497
|
+
"aria-hidden": "true",
|
|
3498
|
+
className: "absolute top-0 bottom-0 w-px bg-sas-accent/40",
|
|
3499
|
+
style: { left: "50%" }
|
|
1083
3500
|
}
|
|
1084
|
-
)
|
|
1085
|
-
/* @__PURE__ */ (0,
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
className: "sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
|
|
3501
|
+
),
|
|
3502
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3503
|
+
"div",
|
|
3504
|
+
{
|
|
3505
|
+
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
3506
|
+
"aria-hidden": "true",
|
|
3507
|
+
className: t.isDownbeat ? "absolute bg-sas-accent" : "absolute bg-sas-muted/50",
|
|
3508
|
+
style: {
|
|
3509
|
+
left: `${(t.fraction * 100).toFixed(2)}%`,
|
|
3510
|
+
top: (SLIDER_HEIGHT_PX - (t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX)) / 2,
|
|
3511
|
+
width: 1,
|
|
3512
|
+
height: t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX
|
|
1097
3513
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1103
|
-
VolumeSlider,
|
|
1104
|
-
{
|
|
1105
|
-
value: currentVolume,
|
|
1106
|
-
onChange: onVolumeChange,
|
|
1107
|
-
disabled: isGenerating,
|
|
1108
|
-
className: "flex-1 min-w-[40px]"
|
|
1109
|
-
}
|
|
1110
|
-
),
|
|
1111
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
|
|
1112
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1113
|
-
PanSlider,
|
|
1114
|
-
{
|
|
1115
|
-
value: currentPan,
|
|
1116
|
-
onChange: onPanChange,
|
|
1117
|
-
disabled: isGenerating,
|
|
1118
|
-
className: "w-10 flex-shrink-0"
|
|
1119
|
-
}
|
|
1120
|
-
)
|
|
1121
|
-
] })
|
|
1122
|
-
] }),
|
|
1123
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
3514
|
+
},
|
|
3515
|
+
t.i
|
|
3516
|
+
)),
|
|
3517
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
1124
3518
|
"div",
|
|
1125
3519
|
{
|
|
1126
|
-
"data-testid": "
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
),
|
|
1137
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate", children: error })
|
|
1138
|
-
] })
|
|
3520
|
+
"data-testid": "offset-scrubber-thumb",
|
|
3521
|
+
"aria-hidden": "true",
|
|
3522
|
+
className: `absolute top-0 bottom-0 rounded-sm ${isDragging ? "bg-sas-accent" : "bg-sas-accent/80"}`,
|
|
3523
|
+
style: {
|
|
3524
|
+
left: thumbLeftPct,
|
|
3525
|
+
width: THUMB_WIDTH_PX,
|
|
3526
|
+
transform: "translateX(-50%)",
|
|
3527
|
+
pointerEvents: "none"
|
|
3528
|
+
}
|
|
1139
3529
|
}
|
|
1140
|
-
)
|
|
1141
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
|
|
1142
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-1 items-center", children: [
|
|
1143
|
-
onGenerate && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1144
|
-
"button",
|
|
1145
|
-
{
|
|
1146
|
-
"data-testid": "sdk-generate-button",
|
|
1147
|
-
onClick: onGenerate,
|
|
1148
|
-
disabled: !isAuthenticated || isGenerating || !prompt?.trim(),
|
|
1149
|
-
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!isAuthenticated || isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : needsGeneration ? "bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse" : prompt?.trim() ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
1150
|
-
title: !isAuthenticated ? "Please log in" : isGenerating ? "Generating..." : "Generate MIDI",
|
|
1151
|
-
children: "Create"
|
|
1152
|
-
}
|
|
1153
|
-
),
|
|
1154
|
-
onCopy && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1155
|
-
"button",
|
|
1156
|
-
{
|
|
1157
|
-
"data-testid": "sdk-copy-button",
|
|
1158
|
-
onClick: onCopy,
|
|
1159
|
-
disabled: !hasMidi || isGenerating,
|
|
1160
|
-
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
1161
|
-
title: hasMidi ? "Duplicate track with different preset" : "Generate MIDI first",
|
|
1162
|
-
children: "Copy"
|
|
1163
|
-
}
|
|
1164
|
-
),
|
|
1165
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1166
|
-
"button",
|
|
1167
|
-
{
|
|
1168
|
-
"data-testid": "sdk-mute-button",
|
|
1169
|
-
onClick: onMuteToggle,
|
|
1170
|
-
disabled: isGenerating,
|
|
1171
|
-
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
1172
|
-
title: isMuted ? "Unmute track" : "Mute track",
|
|
1173
|
-
children: "M"
|
|
1174
|
-
}
|
|
1175
|
-
),
|
|
1176
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1177
|
-
"button",
|
|
1178
|
-
{
|
|
1179
|
-
"data-testid": "sdk-delete-button",
|
|
1180
|
-
onClick: onDelete,
|
|
1181
|
-
className: "text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm",
|
|
1182
|
-
title: "Delete track",
|
|
1183
|
-
children: "x"
|
|
1184
|
-
}
|
|
1185
|
-
)
|
|
1186
|
-
] }),
|
|
1187
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-1 items-center", children: [
|
|
1188
|
-
onShuffle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1189
|
-
"button",
|
|
1190
|
-
{
|
|
1191
|
-
"data-testid": "sdk-shuffle-button",
|
|
1192
|
-
onClick: onShuffle,
|
|
1193
|
-
disabled: !hasMidi || isGenerating || !!currentInstrumentPluginId,
|
|
1194
|
-
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating || !!currentInstrumentPluginId ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
1195
|
-
title: currentInstrumentPluginId ? "Shuffle only works with default Surge XT" : hasMidi ? "Re-roll sound (keep MIDI)" : "Generate MIDI first",
|
|
1196
|
-
children: "Shuffle"
|
|
1197
|
-
}
|
|
1198
|
-
),
|
|
1199
|
-
onToggleFxDrawer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1200
|
-
"button",
|
|
1201
|
-
{
|
|
1202
|
-
"data-testid": "sdk-fx-button",
|
|
1203
|
-
onClick: onToggleFxDrawer,
|
|
1204
|
-
disabled: isGenerating,
|
|
1205
|
-
className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : fxDrawerOpen ? "bg-sas-accent border-sas-accent text-sas-bg" : hasFxActive ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
1206
|
-
title: fxDrawerOpen ? "Hide FX controls" : "Show FX controls",
|
|
1207
|
-
children: "FX"
|
|
1208
|
-
}
|
|
1209
|
-
),
|
|
1210
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1211
|
-
"button",
|
|
1212
|
-
{
|
|
1213
|
-
"data-testid": "sdk-solo-button",
|
|
1214
|
-
onClick: onSoloToggle,
|
|
1215
|
-
disabled: isGenerating,
|
|
1216
|
-
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isSoloed ? "bg-yellow-500 text-black" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
1217
|
-
title: isSoloed ? "Unsolo track" : "Solo track",
|
|
1218
|
-
children: "S"
|
|
1219
|
-
}
|
|
1220
|
-
),
|
|
1221
|
-
onToggleInstrumentDrawer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1222
|
-
"button",
|
|
1223
|
-
{
|
|
1224
|
-
"data-testid": "sdk-plugin-button",
|
|
1225
|
-
onClick: onToggleInstrumentDrawer,
|
|
1226
|
-
disabled: isGenerating,
|
|
1227
|
-
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : instrumentDrawerOpen ? "bg-sas-accent border-sas-accent text-sas-bg" : instrumentMissing ? "bg-amber-500/20 text-amber-400 hover:bg-amber-500/40" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
1228
|
-
title: `Plugin: ${instrumentName ?? "Surge XT"}${instrumentMissing ? " (missing)" : ""}`,
|
|
1229
|
-
children: "P"
|
|
1230
|
-
}
|
|
1231
|
-
)
|
|
1232
|
-
] })
|
|
1233
|
-
] })
|
|
3530
|
+
)
|
|
1234
3531
|
]
|
|
1235
3532
|
}
|
|
1236
3533
|
),
|
|
1237
|
-
|
|
1238
|
-
|
|
3534
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3535
|
+
"span",
|
|
1239
3536
|
{
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
onPresetChange: (_trackId, category, presetIndex) => onFxPresetChange?.(category, presetIndex),
|
|
1244
|
-
onDryWetChange: (_trackId, category, value) => onFxDryWetChange?.(category, value),
|
|
1245
|
-
disabled: isGenerating
|
|
3537
|
+
"data-testid": "offset-scrubber-readout",
|
|
3538
|
+
className: "text-[10px] text-sas-muted/70 tabular-nums flex-shrink-0 min-w-[64px] text-right",
|
|
3539
|
+
children: formatOffset(draftOffset, sampleRate)
|
|
1246
3540
|
}
|
|
1247
|
-
)
|
|
1248
|
-
|
|
1249
|
-
|
|
3541
|
+
),
|
|
3542
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3543
|
+
"button",
|
|
3544
|
+
{
|
|
3545
|
+
type: "button",
|
|
3546
|
+
"data-testid": "offset-scrubber-reset",
|
|
3547
|
+
onClick: handleResetToZero,
|
|
3548
|
+
disabled: isDisabled || draftOffset === 0,
|
|
3549
|
+
className: `text-[10px] px-1 py-0.5 rounded-sm border transition-colors flex-shrink-0 ${isDisabled || draftOffset === 0 ? "border-sas-border text-sas-muted/30 cursor-not-allowed" : "border-sas-border text-sas-muted/70 hover:border-sas-accent hover:text-sas-accent"}`,
|
|
3550
|
+
title: "Reset offset to 0 (bar 1)",
|
|
3551
|
+
children: "\u2316"
|
|
3552
|
+
}
|
|
3553
|
+
),
|
|
3554
|
+
bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3555
|
+
"span",
|
|
1250
3556
|
{
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
onRefresh: onRefreshInstruments,
|
|
1256
|
-
stage: instrumentDrawerStage,
|
|
1257
|
-
onShowEditor,
|
|
1258
|
-
onBackToInstruments,
|
|
1259
|
-
selectedInstrumentName: instrumentName
|
|
3557
|
+
"data-testid": "offset-bpm-mismatch",
|
|
3558
|
+
className: "text-[9px] px-1 py-0.5 rounded-sm bg-amber-500/15 text-amber-400 border border-amber-500/30 flex-shrink-0",
|
|
3559
|
+
title: `Detected ${detectedBpm.toFixed(1)} BPM \u2014 beats may not align with project ${projectBpm} BPM grid`,
|
|
3560
|
+
children: "BPM \u2260"
|
|
1260
3561
|
}
|
|
1261
|
-
)
|
|
3562
|
+
)
|
|
1262
3563
|
] });
|
|
1263
3564
|
}
|
|
3565
|
+
function formatOffset(samples, sampleRate) {
|
|
3566
|
+
const sign = samples > 0 ? "+" : samples < 0 ? "-" : "";
|
|
3567
|
+
const abs = Math.abs(samples);
|
|
3568
|
+
const ms = Math.round(abs / sampleRate * 1e3);
|
|
3569
|
+
return `${sign}${abs} spl (${sign}${ms} ms)`;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// src/components/wavPeakAnalyzer.ts
|
|
3573
|
+
var CLIP_THRESHOLD_LINEAR = 0.891;
|
|
3574
|
+
async function analyzeWavPeak(host, filePath) {
|
|
3575
|
+
const bytes = await host.getAudioFileBytes(filePath);
|
|
3576
|
+
const ContextCtor = window.AudioContext ?? window.webkitAudioContext;
|
|
3577
|
+
const audioContext = new ContextCtor();
|
|
3578
|
+
try {
|
|
3579
|
+
const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));
|
|
3580
|
+
let peak = 0;
|
|
3581
|
+
for (let c = 0; c < audioBuffer.numberOfChannels; c++) {
|
|
3582
|
+
const data = audioBuffer.getChannelData(c);
|
|
3583
|
+
for (let i = 0; i < data.length; i++) {
|
|
3584
|
+
const a = Math.abs(data[i]);
|
|
3585
|
+
if (a > peak) peak = a;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
const peakDb = peak > 1e-6 ? 20 * Math.log10(peak) : -120;
|
|
3589
|
+
return {
|
|
3590
|
+
peakLinear: peak,
|
|
3591
|
+
peakDb,
|
|
3592
|
+
clipped: peak >= CLIP_THRESHOLD_LINEAR - 5e-3
|
|
3593
|
+
};
|
|
3594
|
+
} finally {
|
|
3595
|
+
await audioContext.close().catch(() => {
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// src/components/synthesizeCuePoints.ts
|
|
3601
|
+
function synthesizeCuePoints({
|
|
3602
|
+
bpm,
|
|
3603
|
+
sampleRate,
|
|
3604
|
+
bars,
|
|
3605
|
+
meter = 4
|
|
3606
|
+
}) {
|
|
3607
|
+
const safeBpm = bpm > 0 ? bpm : 120;
|
|
3608
|
+
const safeSampleRate = sampleRate > 0 ? sampleRate : 48e3;
|
|
3609
|
+
const samplesPerBeat = Math.round(60 / safeBpm * safeSampleRate);
|
|
3610
|
+
const totalBeats = Math.max(1, Math.round(bars * meter));
|
|
3611
|
+
const beats = [];
|
|
3612
|
+
for (let i = 0; i < totalBeats; i++) {
|
|
3613
|
+
beats.push(i * samplesPerBeat);
|
|
3614
|
+
}
|
|
3615
|
+
return {
|
|
3616
|
+
schema: 1,
|
|
3617
|
+
sample_rate: safeSampleRate,
|
|
3618
|
+
detected_bpm: safeBpm,
|
|
3619
|
+
downbeat_sample: 0,
|
|
3620
|
+
beats,
|
|
3621
|
+
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
1264
3624
|
|
|
1265
3625
|
// src/hooks/useSceneState.ts
|
|
1266
|
-
var
|
|
3626
|
+
var import_react17 = require("react");
|
|
1267
3627
|
function useSceneState(activeSceneId, initialValue) {
|
|
1268
|
-
const [stateMap, setStateMap] = (0,
|
|
1269
|
-
const activeSceneIdRef = (0,
|
|
3628
|
+
const [stateMap, setStateMap] = (0, import_react17.useState)(() => /* @__PURE__ */ new Map());
|
|
3629
|
+
const activeSceneIdRef = (0, import_react17.useRef)(activeSceneId);
|
|
1270
3630
|
activeSceneIdRef.current = activeSceneId;
|
|
1271
3631
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
1272
|
-
const setForCurrentScene = (0,
|
|
3632
|
+
const setForCurrentScene = (0, import_react17.useCallback)((value) => {
|
|
1273
3633
|
const sid = activeSceneIdRef.current;
|
|
1274
3634
|
if (sid === null) return;
|
|
1275
3635
|
setStateMap((prev) => {
|
|
@@ -1280,7 +3640,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
1280
3640
|
return newMap;
|
|
1281
3641
|
});
|
|
1282
3642
|
}, [initialValue]);
|
|
1283
|
-
const setForScene = (0,
|
|
3643
|
+
const setForScene = (0, import_react17.useCallback)((sceneId, value) => {
|
|
1284
3644
|
setStateMap((prev) => {
|
|
1285
3645
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
1286
3646
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -1292,33 +3652,434 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
1292
3652
|
return [currentValue, setForCurrentScene, setForScene];
|
|
1293
3653
|
}
|
|
1294
3654
|
|
|
3655
|
+
// src/hooks/useAnySolo.ts
|
|
3656
|
+
var import_react18 = require("react");
|
|
3657
|
+
function useAnySolo(host) {
|
|
3658
|
+
const [anySolo, setAnySolo] = (0, import_react18.useState)(false);
|
|
3659
|
+
(0, import_react18.useEffect)(() => {
|
|
3660
|
+
let active = true;
|
|
3661
|
+
const refresh = () => {
|
|
3662
|
+
host.isAnySoloActive().then((v) => {
|
|
3663
|
+
if (active) setAnySolo(v);
|
|
3664
|
+
}).catch(() => {
|
|
3665
|
+
});
|
|
3666
|
+
};
|
|
3667
|
+
refresh();
|
|
3668
|
+
const unsub = host.onTrackStateChange(() => refresh());
|
|
3669
|
+
return () => {
|
|
3670
|
+
active = false;
|
|
3671
|
+
unsub();
|
|
3672
|
+
};
|
|
3673
|
+
}, [host]);
|
|
3674
|
+
return anySolo;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/hooks/useSoundHistory.ts
|
|
3678
|
+
var import_react19 = require("react");
|
|
3679
|
+
var EMPTY = { entries: [], cursor: -1 };
|
|
3680
|
+
function sameDescriptor(a, b) {
|
|
3681
|
+
if (a === b) return true;
|
|
3682
|
+
try {
|
|
3683
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
3684
|
+
} catch {
|
|
3685
|
+
return false;
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
function useSoundHistory(applySound, opts = {}) {
|
|
3689
|
+
const max = Math.max(2, opts.max ?? 24);
|
|
3690
|
+
const applyRef = (0, import_react19.useRef)(applySound);
|
|
3691
|
+
applyRef.current = applySound;
|
|
3692
|
+
const onChangeRef = (0, import_react19.useRef)(opts.onChange);
|
|
3693
|
+
onChangeRef.current = opts.onChange;
|
|
3694
|
+
const dataRef = (0, import_react19.useRef)({});
|
|
3695
|
+
const [, setVersion] = (0, import_react19.useState)(0);
|
|
3696
|
+
const bump = (0, import_react19.useCallback)(() => setVersion((v) => v + 1), []);
|
|
3697
|
+
const commit = (0, import_react19.useCallback)(
|
|
3698
|
+
(trackId, next, notify) => {
|
|
3699
|
+
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
3700
|
+
bump();
|
|
3701
|
+
if (notify) onChangeRef.current?.(trackId, next);
|
|
3702
|
+
},
|
|
3703
|
+
[bump]
|
|
3704
|
+
);
|
|
3705
|
+
const record = (0, import_react19.useCallback)(
|
|
3706
|
+
(trackId, descriptor, label) => {
|
|
3707
|
+
const h = dataRef.current[trackId];
|
|
3708
|
+
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
3709
|
+
if (current && sameDescriptor(current.descriptor, descriptor)) return;
|
|
3710
|
+
const entries = [...h ? h.entries : [], { descriptor, label }];
|
|
3711
|
+
while (entries.length > max) {
|
|
3712
|
+
const victim = entries.findIndex((e) => !e.favorite);
|
|
3713
|
+
if (victim === -1) break;
|
|
3714
|
+
entries.splice(victim, 1);
|
|
3715
|
+
}
|
|
3716
|
+
commit(trackId, { entries, cursor: entries.length - 1 }, true);
|
|
3717
|
+
},
|
|
3718
|
+
[max, commit]
|
|
3719
|
+
);
|
|
3720
|
+
const restoreTo = (0, import_react19.useCallback)(
|
|
3721
|
+
async (trackId, index) => {
|
|
3722
|
+
const h = dataRef.current[trackId];
|
|
3723
|
+
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
3724
|
+
await applyRef.current(trackId, h.entries[index].descriptor);
|
|
3725
|
+
commit(trackId, { entries: h.entries, cursor: index }, true);
|
|
3726
|
+
return true;
|
|
3727
|
+
},
|
|
3728
|
+
[commit]
|
|
3729
|
+
);
|
|
3730
|
+
const undo = (0, import_react19.useCallback)(
|
|
3731
|
+
(trackId) => {
|
|
3732
|
+
const h = dataRef.current[trackId];
|
|
3733
|
+
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
3734
|
+
return restoreTo(trackId, h.cursor - 1);
|
|
3735
|
+
},
|
|
3736
|
+
[restoreTo]
|
|
3737
|
+
);
|
|
3738
|
+
const toggleFavorite = (0, import_react19.useCallback)(
|
|
3739
|
+
(trackId, index) => {
|
|
3740
|
+
const h = dataRef.current[trackId];
|
|
3741
|
+
if (!h || index < 0 || index >= h.entries.length) return;
|
|
3742
|
+
const entries = h.entries.map((e, i) => i === index ? { ...e, favorite: !e.favorite } : e);
|
|
3743
|
+
commit(trackId, { entries, cursor: h.cursor }, true);
|
|
3744
|
+
},
|
|
3745
|
+
[commit]
|
|
3746
|
+
);
|
|
3747
|
+
const restore = (0, import_react19.useCallback)(
|
|
3748
|
+
(trackId, state) => {
|
|
3749
|
+
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
3750
|
+
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
3751
|
+
const cursor = entries.length === 0 ? -1 : Math.min(Math.max(raw, 0), entries.length - 1);
|
|
3752
|
+
commit(trackId, { entries, cursor }, false);
|
|
3753
|
+
},
|
|
3754
|
+
[commit]
|
|
3755
|
+
);
|
|
3756
|
+
const list = (0, import_react19.useCallback)(
|
|
3757
|
+
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
3758
|
+
[]
|
|
3759
|
+
);
|
|
3760
|
+
const canUndo = (0, import_react19.useCallback)((trackId) => {
|
|
3761
|
+
const h = dataRef.current[trackId];
|
|
3762
|
+
return !!h && h.cursor > 0;
|
|
3763
|
+
}, []);
|
|
3764
|
+
const clear = (0, import_react19.useCallback)(
|
|
3765
|
+
(trackId) => {
|
|
3766
|
+
if (dataRef.current[trackId]) {
|
|
3767
|
+
const next = { ...dataRef.current };
|
|
3768
|
+
delete next[trackId];
|
|
3769
|
+
dataRef.current = next;
|
|
3770
|
+
bump();
|
|
3771
|
+
}
|
|
3772
|
+
onChangeRef.current?.(trackId, EMPTY);
|
|
3773
|
+
},
|
|
3774
|
+
[bump]
|
|
3775
|
+
);
|
|
3776
|
+
const reset = (0, import_react19.useCallback)(() => {
|
|
3777
|
+
dataRef.current = {};
|
|
3778
|
+
bump();
|
|
3779
|
+
}, [bump]);
|
|
3780
|
+
return (0, import_react19.useMemo)(
|
|
3781
|
+
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
3782
|
+
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
// src/hooks/useTrackReorder.ts
|
|
3787
|
+
var import_react20 = require("react");
|
|
3788
|
+
function moveItem(arr, from, to) {
|
|
3789
|
+
const next = arr.slice();
|
|
3790
|
+
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3791
|
+
return next;
|
|
3792
|
+
}
|
|
3793
|
+
const [moved] = next.splice(from, 1);
|
|
3794
|
+
next.splice(to, 0, moved);
|
|
3795
|
+
return next;
|
|
3796
|
+
}
|
|
3797
|
+
function useTrackReorder({
|
|
3798
|
+
host,
|
|
3799
|
+
items,
|
|
3800
|
+
setItems,
|
|
3801
|
+
getId,
|
|
3802
|
+
onError
|
|
3803
|
+
}) {
|
|
3804
|
+
const [draggingIndex, setDraggingIndex] = (0, import_react20.useState)(null);
|
|
3805
|
+
const [dragOverIndex, setDragOverIndex] = (0, import_react20.useState)(null);
|
|
3806
|
+
const fromRef = (0, import_react20.useRef)(null);
|
|
3807
|
+
const itemsRef = (0, import_react20.useRef)(items);
|
|
3808
|
+
itemsRef.current = items;
|
|
3809
|
+
const dragPropsFor = (0, import_react20.useCallback)(
|
|
3810
|
+
(index) => ({
|
|
3811
|
+
handleProps: {
|
|
3812
|
+
draggable: true,
|
|
3813
|
+
onDragStart: (e) => {
|
|
3814
|
+
fromRef.current = index;
|
|
3815
|
+
setDraggingIndex(index);
|
|
3816
|
+
if (e.dataTransfer) {
|
|
3817
|
+
e.dataTransfer.effectAllowed = "move";
|
|
3818
|
+
try {
|
|
3819
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
3820
|
+
} catch {
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
},
|
|
3824
|
+
onDragEnd: () => {
|
|
3825
|
+
fromRef.current = null;
|
|
3826
|
+
setDraggingIndex(null);
|
|
3827
|
+
setDragOverIndex(null);
|
|
3828
|
+
}
|
|
3829
|
+
},
|
|
3830
|
+
rowProps: {
|
|
3831
|
+
onDragEnter: (e) => {
|
|
3832
|
+
if (fromRef.current === null) return;
|
|
3833
|
+
e.preventDefault();
|
|
3834
|
+
setDragOverIndex(index);
|
|
3835
|
+
},
|
|
3836
|
+
onDragOver: (e) => {
|
|
3837
|
+
if (fromRef.current === null) return;
|
|
3838
|
+
e.preventDefault();
|
|
3839
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3840
|
+
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3841
|
+
},
|
|
3842
|
+
onDragLeave: () => {
|
|
3843
|
+
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3844
|
+
},
|
|
3845
|
+
onDrop: (e) => {
|
|
3846
|
+
e.preventDefault();
|
|
3847
|
+
const from = fromRef.current;
|
|
3848
|
+
fromRef.current = null;
|
|
3849
|
+
setDraggingIndex(null);
|
|
3850
|
+
setDragOverIndex(null);
|
|
3851
|
+
if (from === null || from === index) return;
|
|
3852
|
+
const prev = itemsRef.current;
|
|
3853
|
+
const next = moveItem(prev, from, index);
|
|
3854
|
+
setItems(next);
|
|
3855
|
+
const ids = next.map(getId);
|
|
3856
|
+
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3857
|
+
setItems(prev);
|
|
3858
|
+
onError?.(err);
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
},
|
|
3862
|
+
isDragging: draggingIndex === index,
|
|
3863
|
+
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3864
|
+
}),
|
|
3865
|
+
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3866
|
+
);
|
|
3867
|
+
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3868
|
+
}
|
|
3869
|
+
|
|
1295
3870
|
// src/constants/sdk-version.ts
|
|
1296
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
3871
|
+
var PLUGIN_SDK_VERSION = "2.24.0";
|
|
3872
|
+
|
|
3873
|
+
// src/utils/format-concurrent-tracks.ts
|
|
3874
|
+
function formatConcurrentTracks(ctx) {
|
|
3875
|
+
const tracks = ctx.concurrentTracks;
|
|
3876
|
+
if (!tracks || tracks.length === 0) return "";
|
|
3877
|
+
const lines = [`Concurrent tracks in scene (already generated):`];
|
|
3878
|
+
for (const track of tracks) {
|
|
3879
|
+
const promptStr = track.prompt ? ` prompt="${escapeQuotes(track.prompt)}"` : "";
|
|
3880
|
+
lines.push(` - role=${track.role ?? "unknown"}${promptStr}`);
|
|
3881
|
+
if (track.notesByChord.length === 0) {
|
|
3882
|
+
lines.push(` (no notes)`);
|
|
3883
|
+
} else {
|
|
3884
|
+
for (const segment of track.notesByChord) {
|
|
3885
|
+
if (segment.notes.length === 0) continue;
|
|
3886
|
+
lines.push(` ${formatChordSegment(segment)}`);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
if (track.truncated && typeof track.originalNoteCount === "number") {
|
|
3890
|
+
const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
|
|
3891
|
+
if (dropped > 0) {
|
|
3892
|
+
lines.push(` \u2026 (${dropped} more notes truncated)`);
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
if (ctx.truncatedTrackCount && ctx.truncatedTrackCount > 0) {
|
|
3897
|
+
lines.push(
|
|
3898
|
+
` \u2026 (${ctx.truncatedTrackCount} additional track${ctx.truncatedTrackCount === 1 ? "" : "s"} omitted to fit token budget)`
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
return lines.join("\n");
|
|
3902
|
+
}
|
|
3903
|
+
function formatChordSegment(segment) {
|
|
3904
|
+
const [start, end] = segment.chordRangeQn;
|
|
3905
|
+
const notesJson = JSON.stringify(segment.notes.map(compactNote2));
|
|
3906
|
+
return `${segment.chord} (beats ${start}-${end}): ${notesJson}`;
|
|
3907
|
+
}
|
|
3908
|
+
function compactNote2(n) {
|
|
3909
|
+
return {
|
|
3910
|
+
pitch: n.pitch,
|
|
3911
|
+
startBeat: n.startBeat,
|
|
3912
|
+
durationBeats: n.durationBeats,
|
|
3913
|
+
velocity: n.velocity
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
function escapeQuotes(s) {
|
|
3917
|
+
return s.replace(/"/g, '\\"');
|
|
3918
|
+
}
|
|
3919
|
+
function sumKeptNotes(segments) {
|
|
3920
|
+
let total = 0;
|
|
3921
|
+
for (const s of segments) total += s.notes.length;
|
|
3922
|
+
return total;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// src/utils/semantic-match.ts
|
|
3926
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
3927
|
+
"a",
|
|
3928
|
+
"an",
|
|
3929
|
+
"the",
|
|
3930
|
+
"and",
|
|
3931
|
+
"or",
|
|
3932
|
+
"but",
|
|
3933
|
+
"with",
|
|
3934
|
+
"for",
|
|
3935
|
+
"to",
|
|
3936
|
+
"of",
|
|
3937
|
+
"in",
|
|
3938
|
+
"on",
|
|
3939
|
+
"at",
|
|
3940
|
+
"by",
|
|
3941
|
+
"is",
|
|
3942
|
+
"it",
|
|
3943
|
+
"this",
|
|
3944
|
+
"that",
|
|
3945
|
+
"i",
|
|
3946
|
+
"my",
|
|
3947
|
+
"me",
|
|
3948
|
+
"make",
|
|
3949
|
+
"please",
|
|
3950
|
+
"give",
|
|
3951
|
+
"want",
|
|
3952
|
+
"need",
|
|
3953
|
+
"some",
|
|
3954
|
+
"like",
|
|
3955
|
+
"get",
|
|
3956
|
+
"something"
|
|
3957
|
+
]);
|
|
3958
|
+
function tokenizePrompt(text) {
|
|
3959
|
+
if (!text) return [];
|
|
3960
|
+
const withoutNegatives = text.split(",").map((clause) => clause.trim()).filter((clause) => clause.length > 0 && !/^no\s/i.test(clause)).join(" ");
|
|
3961
|
+
return withoutNegatives.toLowerCase().split(/[^a-z0-9]+/u).filter((tok) => {
|
|
3962
|
+
if (!tok) return false;
|
|
3963
|
+
if (STOP_WORDS.has(tok)) return false;
|
|
3964
|
+
if (/^\d{1,2}$/.test(tok)) return false;
|
|
3965
|
+
return true;
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
function scorePromptMatch(query, candidatePrompts) {
|
|
3969
|
+
const n = candidatePrompts.length;
|
|
3970
|
+
if (n === 0) return [];
|
|
3971
|
+
const queryTokens = Array.from(new Set(tokenizePrompt(query)));
|
|
3972
|
+
if (queryTokens.length === 0) return candidatePrompts.map(() => 0);
|
|
3973
|
+
const candidateTokenSets = candidatePrompts.map((p) => new Set(tokenizePrompt(p)));
|
|
3974
|
+
const idf = /* @__PURE__ */ new Map();
|
|
3975
|
+
for (const token of queryTokens) {
|
|
3976
|
+
let df = 0;
|
|
3977
|
+
for (const set of candidateTokenSets) {
|
|
3978
|
+
if (set.has(token)) df += 1;
|
|
3979
|
+
}
|
|
3980
|
+
if (df > 0) idf.set(token, Math.log(1 + n / df));
|
|
3981
|
+
}
|
|
3982
|
+
let denominator = 0;
|
|
3983
|
+
for (const weight of idf.values()) denominator += weight;
|
|
3984
|
+
if (denominator === 0) return candidatePrompts.map(() => 0);
|
|
3985
|
+
return candidateTokenSets.map((set) => {
|
|
3986
|
+
let numerator = 0;
|
|
3987
|
+
for (const [token, weight] of idf) {
|
|
3988
|
+
if (set.has(token)) numerator += weight;
|
|
3989
|
+
}
|
|
3990
|
+
return numerator / denominator;
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
function pickTopKWeighted(scored, options = {}) {
|
|
3994
|
+
const { k = 5, temperature = 0.3, excludeKeys, rng = Math.random } = options;
|
|
3995
|
+
let pool = scored;
|
|
3996
|
+
if (excludeKeys && excludeKeys.size > 0) {
|
|
3997
|
+
pool = pool.filter((c) => c.key === void 0 || !excludeKeys.has(c.key));
|
|
3998
|
+
}
|
|
3999
|
+
if (pool.length === 0) return null;
|
|
4000
|
+
const sorted = [...pool].sort((a, b) => b.score - a.score);
|
|
4001
|
+
const top = sorted.slice(0, Math.max(1, k));
|
|
4002
|
+
const maxScore = top[0].score;
|
|
4003
|
+
const safeTemp = Math.max(1e-6, temperature);
|
|
4004
|
+
const weights = top.map((c) => Math.exp((c.score - maxScore) / safeTemp));
|
|
4005
|
+
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
|
4006
|
+
let threshold = rng() * totalWeight;
|
|
4007
|
+
for (let i = 0; i < top.length; i += 1) {
|
|
4008
|
+
threshold -= weights[i];
|
|
4009
|
+
if (threshold <= 0) return top[i].item;
|
|
4010
|
+
}
|
|
4011
|
+
return top[top.length - 1].item;
|
|
4012
|
+
}
|
|
1297
4013
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1298
4014
|
0 && (module.exports = {
|
|
4015
|
+
ConfirmDialog,
|
|
4016
|
+
CrossfadeModal,
|
|
4017
|
+
CrossfadeTrackRow,
|
|
1299
4018
|
DB_MAX,
|
|
1300
4019
|
DB_MIN,
|
|
1301
4020
|
DEFAULT_FX_CATEGORY_DETAIL,
|
|
1302
4021
|
DEFAULT_FX_DRY_WET,
|
|
4022
|
+
DRAG_DEAD_ZONE,
|
|
4023
|
+
DownloadPackButton,
|
|
1303
4024
|
EMPTY_FX_DETAIL_STATE,
|
|
1304
4025
|
EMPTY_FX_STATE,
|
|
4026
|
+
EQUAL_POWER_GAIN,
|
|
1305
4027
|
FX_CATEGORIES,
|
|
1306
4028
|
FX_CHAIN_ORDER,
|
|
1307
4029
|
FX_DISPLAY_LABELS,
|
|
1308
4030
|
FX_ENGINE_PLUGIN_NAMES,
|
|
1309
4031
|
FX_PRESET_CONFIGS,
|
|
1310
4032
|
FxToggleBar,
|
|
4033
|
+
GUTTER_W,
|
|
4034
|
+
ImportTrackModal,
|
|
1311
4035
|
InstrumentDrawer,
|
|
4036
|
+
LevelMeter,
|
|
4037
|
+
Modal,
|
|
4038
|
+
OffsetScrubber,
|
|
1312
4039
|
PLUGIN_SDK_VERSION,
|
|
4040
|
+
PX_PER_BEAT,
|
|
1313
4041
|
PanSlider,
|
|
4042
|
+
PianoRollEditor,
|
|
1314
4043
|
PluginError,
|
|
4044
|
+
RESIZE_HANDLE_PX,
|
|
4045
|
+
ROW_HEIGHT,
|
|
1315
4046
|
SLIDER_UNITY,
|
|
4047
|
+
SamplePackCTACard,
|
|
4048
|
+
ScrollingWaveform,
|
|
1316
4049
|
SorceryProgressBar,
|
|
4050
|
+
TrackDrawer,
|
|
4051
|
+
TrackMeterStrip,
|
|
1317
4052
|
TrackRow,
|
|
1318
4053
|
VolumeSlider,
|
|
4054
|
+
WaveformView,
|
|
4055
|
+
analyzeWavPeak,
|
|
4056
|
+
asCrossfadeMeta,
|
|
4057
|
+
buildCrossfadeInpaintPrompt,
|
|
1319
4058
|
calculateTimeBasedTarget,
|
|
4059
|
+
cellToPx,
|
|
4060
|
+
centerScrollTop,
|
|
4061
|
+
computePeaks,
|
|
1320
4062
|
dbToSlider,
|
|
4063
|
+
drawWaveform,
|
|
4064
|
+
formatConcurrentTracks,
|
|
4065
|
+
moveItem,
|
|
4066
|
+
parseCrossfadePairs,
|
|
4067
|
+
pickTopKWeighted,
|
|
4068
|
+
pitchToName,
|
|
4069
|
+
pxToCell,
|
|
4070
|
+
resizeNoteDuration,
|
|
4071
|
+
scorePromptMatch,
|
|
1321
4072
|
sliderToDb,
|
|
1322
|
-
|
|
4073
|
+
synthesizeCuePoints,
|
|
4074
|
+
tokenizePrompt,
|
|
4075
|
+
transposeNotes,
|
|
4076
|
+
useAnySolo,
|
|
4077
|
+
useSceneState,
|
|
4078
|
+
useSoundHistory,
|
|
4079
|
+
useTrackLevel,
|
|
4080
|
+
useTrackLevels,
|
|
4081
|
+
useTrackMeter,
|
|
4082
|
+
useTrackReorder,
|
|
4083
|
+
useTransportPlaying
|
|
1323
4084
|
});
|
|
1324
4085
|
//# sourceMappingURL=index.js.map
|