@signalsandsorcery/plugin-sdk 2.34.1 → 2.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +679 -74
- package/dist/index.d.ts +679 -74
- package/dist/index.js +2342 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2332 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4753,6 +4753,9 @@ function synthesizeCuePoints({
|
|
|
4753
4753
|
};
|
|
4754
4754
|
}
|
|
4755
4755
|
|
|
4756
|
+
// src/panel-core/useGeneratorPanelCore.tsx
|
|
4757
|
+
import { useState as useState19, useEffect as useEffect16, useCallback as useCallback14, useRef as useRef17, useMemo as useMemo8 } from "react";
|
|
4758
|
+
|
|
4756
4759
|
// src/hooks/useSceneState.ts
|
|
4757
4760
|
import { useState as useState15, useCallback as useCallback11, useRef as useRef14 } from "react";
|
|
4758
4761
|
function useSceneState(activeSceneId, initialValue) {
|
|
@@ -4914,8 +4917,2326 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4914
4917
|
);
|
|
4915
4918
|
}
|
|
4916
4919
|
|
|
4920
|
+
// src/panel-core/track-state.ts
|
|
4921
|
+
function newTrackState(handle, overrides = {}) {
|
|
4922
|
+
return {
|
|
4923
|
+
handle,
|
|
4924
|
+
prompt: "",
|
|
4925
|
+
role: "",
|
|
4926
|
+
runtimeState: { id: handle.id, muted: false, solo: false, volume: 0.75, pan: 0 },
|
|
4927
|
+
fxDetailState: { ...EMPTY_FX_DETAIL_STATE },
|
|
4928
|
+
drawerOpen: false,
|
|
4929
|
+
drawerTab: "fx",
|
|
4930
|
+
editorStage: false,
|
|
4931
|
+
isGenerating: false,
|
|
4932
|
+
error: null,
|
|
4933
|
+
hasMidi: false,
|
|
4934
|
+
generationProgress: 0,
|
|
4935
|
+
editNotes: [],
|
|
4936
|
+
editBars: 4,
|
|
4937
|
+
editBpm: 120,
|
|
4938
|
+
instrumentPluginId: handle.instrumentPluginId ?? null,
|
|
4939
|
+
instrumentName: handle.instrumentName ?? null,
|
|
4940
|
+
instrumentMissing: false,
|
|
4941
|
+
shuffleHistory: /* @__PURE__ */ new Set(),
|
|
4942
|
+
...overrides
|
|
4943
|
+
};
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
// src/panel-core/panel-helpers.ts
|
|
4947
|
+
function trackDataKey(dbId, suffix) {
|
|
4948
|
+
return `track:${dbId}:${suffix}`;
|
|
4949
|
+
}
|
|
4950
|
+
function pluginFxToToggleFx(sdkState) {
|
|
4951
|
+
const result = { ...EMPTY_FX_DETAIL_STATE };
|
|
4952
|
+
for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
|
|
4953
|
+
const sdkCat = sdkState[category];
|
|
4954
|
+
if (sdkCat) {
|
|
4955
|
+
result[category] = {
|
|
4956
|
+
enabled: sdkCat.enabled,
|
|
4957
|
+
presetIndex: sdkCat.presetIndex,
|
|
4958
|
+
dryWet: sdkCat.dryWet
|
|
4959
|
+
};
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
return result;
|
|
4963
|
+
}
|
|
4964
|
+
function parseLLMNoteResponse(content) {
|
|
4965
|
+
try {
|
|
4966
|
+
let jsonStr = content.trim();
|
|
4967
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
4968
|
+
if (fenceMatch) {
|
|
4969
|
+
jsonStr = fenceMatch[1].trim();
|
|
4970
|
+
}
|
|
4971
|
+
const parsed = JSON.parse(jsonStr);
|
|
4972
|
+
if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
|
|
4973
|
+
return null;
|
|
4974
|
+
}
|
|
4975
|
+
const obj = parsed;
|
|
4976
|
+
if (!Array.isArray(obj.notes)) {
|
|
4977
|
+
return null;
|
|
4978
|
+
}
|
|
4979
|
+
const validNotes = [];
|
|
4980
|
+
for (const raw of obj.notes) {
|
|
4981
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
4982
|
+
const note = raw;
|
|
4983
|
+
const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
|
|
4984
|
+
const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
|
|
4985
|
+
const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
|
|
4986
|
+
const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
|
|
4987
|
+
if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
|
|
4988
|
+
validNotes.push({
|
|
4989
|
+
pitch: Math.round(pitch),
|
|
4990
|
+
startBeat,
|
|
4991
|
+
durationBeats,
|
|
4992
|
+
velocity: Math.round(velocity)
|
|
4993
|
+
});
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
const role = typeof obj.role === "string" ? obj.role : void 0;
|
|
4997
|
+
return { notes: validNotes, role };
|
|
4998
|
+
} catch {
|
|
4999
|
+
return null;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
// src/panel-core/group-meta.ts
|
|
5004
|
+
function parseTrackGroups(sceneData, spec) {
|
|
5005
|
+
const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
|
|
5006
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5007
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
5008
|
+
const match = pattern.exec(key);
|
|
5009
|
+
if (!match) continue;
|
|
5010
|
+
const meta = spec.asMeta(val);
|
|
5011
|
+
if (!meta) continue;
|
|
5012
|
+
const groupId = spec.groupIdOf(meta);
|
|
5013
|
+
const list = groups.get(groupId) ?? [];
|
|
5014
|
+
list.push({ dbId: match[1], meta });
|
|
5015
|
+
groups.set(groupId, list);
|
|
5016
|
+
}
|
|
5017
|
+
const out = [];
|
|
5018
|
+
for (const [groupId, members] of groups) {
|
|
5019
|
+
if (spec.sortMembers) members.sort(spec.sortMembers);
|
|
5020
|
+
out.push({ groupId, members });
|
|
5021
|
+
}
|
|
5022
|
+
return out;
|
|
5023
|
+
}
|
|
5024
|
+
function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
|
|
5025
|
+
const byDbId = /* @__PURE__ */ new Map();
|
|
5026
|
+
for (const t of tracks) byDbId.set(getDbId(t), t);
|
|
5027
|
+
const resolved = [];
|
|
5028
|
+
const memberDbIds = /* @__PURE__ */ new Set();
|
|
5029
|
+
const staleMemberDbIds = [];
|
|
5030
|
+
for (const parsed of parsedGroups) {
|
|
5031
|
+
const live = { groupId: parsed.groupId, members: [] };
|
|
5032
|
+
for (const member of parsed.members) {
|
|
5033
|
+
const track = byDbId.get(member.dbId);
|
|
5034
|
+
if (track) live.members.push({ dbId: member.dbId, meta: member.meta, track });
|
|
5035
|
+
else staleMemberDbIds.push(member.dbId);
|
|
5036
|
+
}
|
|
5037
|
+
if (live.members.length === 0) continue;
|
|
5038
|
+
const complete = opts.isComplete ? opts.isComplete(live, parsed) : live.members.length === parsed.members.length;
|
|
5039
|
+
if (!complete) continue;
|
|
5040
|
+
resolved.push(live);
|
|
5041
|
+
for (const m of live.members) memberDbIds.add(m.dbId);
|
|
5042
|
+
}
|
|
5043
|
+
return { resolved, memberDbIds, staleMemberDbIds };
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
// src/panel-core/useTransitionOps.ts
|
|
5047
|
+
import { useCallback as useCallback13, useEffect as useEffect15, useRef as useRef16, useState as useState18 } from "react";
|
|
5048
|
+
function useTransitionOps({
|
|
5049
|
+
host,
|
|
5050
|
+
adapter,
|
|
5051
|
+
activeSceneId,
|
|
5052
|
+
isConnected,
|
|
5053
|
+
isAuthenticated,
|
|
5054
|
+
sceneContext,
|
|
5055
|
+
tracks,
|
|
5056
|
+
setTracks,
|
|
5057
|
+
loadTracks,
|
|
5058
|
+
setCrossfadePairsMeta,
|
|
5059
|
+
setFadesMeta,
|
|
5060
|
+
resolvedCrossfadePairs,
|
|
5061
|
+
resolvedFades
|
|
5062
|
+
}) {
|
|
5063
|
+
const { identity } = adapter;
|
|
5064
|
+
const appliedFadeAutomationRef = useRef16(/* @__PURE__ */ new Set());
|
|
5065
|
+
const applyCrossfadeAutomation = useCallback13(
|
|
5066
|
+
async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
|
|
5067
|
+
if (host.setTrackVolumeAutomation) {
|
|
5068
|
+
const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
|
|
5069
|
+
await host.setTrackVolumeAutomation(originTrackId, curves.origin).catch(() => {
|
|
5070
|
+
});
|
|
5071
|
+
await host.setTrackVolumeAutomation(targetTrackId, curves.target).catch(() => {
|
|
5072
|
+
});
|
|
5073
|
+
} else {
|
|
5074
|
+
await host.setTrackVolume(originTrackId, EQUAL_POWER_GAIN).catch(() => {
|
|
5075
|
+
});
|
|
5076
|
+
await host.setTrackVolume(targetTrackId, EQUAL_POWER_GAIN).catch(() => {
|
|
5077
|
+
});
|
|
5078
|
+
}
|
|
5079
|
+
},
|
|
5080
|
+
[host]
|
|
5081
|
+
);
|
|
5082
|
+
const applyFadeAutomation = useCallback13(
|
|
5083
|
+
async (trackId, direction, bars, bpm, sliderPos, gesture) => {
|
|
5084
|
+
if (!host.setTrackVolumeAutomation) return;
|
|
5085
|
+
const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
|
|
5086
|
+
await host.setTrackVolumeAutomation(trackId, points).catch(() => {
|
|
5087
|
+
});
|
|
5088
|
+
},
|
|
5089
|
+
[host]
|
|
5090
|
+
);
|
|
5091
|
+
const [isCreatingCrossfade, setIsCreatingCrossfade] = useState18(false);
|
|
5092
|
+
const handleCreateCrossfade = useCallback13(
|
|
5093
|
+
async (origin, target) => {
|
|
5094
|
+
const scene = activeSceneId;
|
|
5095
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
5096
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
5097
|
+
if (!scene) throw new Error("No active scene.");
|
|
5098
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
5099
|
+
if (!isAuthenticated) throw new Error("Please sign in to generate the bridge.");
|
|
5100
|
+
if (tracks.length + 2 > identity.maxTracks) {
|
|
5101
|
+
throw new Error("Not enough track slots for a crossfade.");
|
|
5102
|
+
}
|
|
5103
|
+
setIsCreatingCrossfade(true);
|
|
5104
|
+
const created = [];
|
|
5105
|
+
try {
|
|
5106
|
+
const role = target.role ?? origin.role ?? "";
|
|
5107
|
+
const mc = await host.getMusicalContext();
|
|
5108
|
+
const [originMidi, targetMidi, originKey, targetKey] = await Promise.all([
|
|
5109
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(origin.dbId) : Promise.resolve({ clips: [] }),
|
|
5110
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(target.dbId) : Promise.resolve({ clips: [] }),
|
|
5111
|
+
host.getSceneKey ? host.getSceneKey(fromSceneId) : Promise.resolve(null),
|
|
5112
|
+
host.getSceneKey ? host.getSceneKey(toSceneId) : Promise.resolve(null)
|
|
5113
|
+
]);
|
|
5114
|
+
const userPrompt = buildCrossfadeInpaintPrompt({
|
|
5115
|
+
role,
|
|
5116
|
+
bars: mc.bars,
|
|
5117
|
+
originName: origin.name,
|
|
5118
|
+
targetName: target.name,
|
|
5119
|
+
originKey: originKey ? `${originKey.key} ${originKey.mode}` : null,
|
|
5120
|
+
targetKey: targetKey ? `${targetKey.key} ${targetKey.mode}` : null,
|
|
5121
|
+
originNotes: originMidi.clips[0]?.notes ?? [],
|
|
5122
|
+
targetNotes: targetMidi.clips[0]?.notes ?? []
|
|
5123
|
+
});
|
|
5124
|
+
const llm = await host.generateWithLLM({
|
|
5125
|
+
system: adapter.buildSystemPrompt(host.getValidRoles()),
|
|
5126
|
+
user: userPrompt,
|
|
5127
|
+
responseFormat: "json"
|
|
5128
|
+
});
|
|
5129
|
+
const parsed = adapter.parseNotesResponse(llm.content);
|
|
5130
|
+
if (!parsed || parsed.notes.length === 0) {
|
|
5131
|
+
throw new Error("The bridge generator returned no notes.");
|
|
5132
|
+
}
|
|
5133
|
+
const notes = await host.postProcessMidi(parsed.notes, {
|
|
5134
|
+
quantize: true,
|
|
5135
|
+
removeOverlaps: true
|
|
5136
|
+
});
|
|
5137
|
+
const clip = {
|
|
5138
|
+
startTime: 0,
|
|
5139
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5140
|
+
tempo: mc.bpm,
|
|
5141
|
+
notes
|
|
5142
|
+
};
|
|
5143
|
+
const top = await host.createTrack({
|
|
5144
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-xf-o`,
|
|
5145
|
+
...adapter.createTrackOptions()
|
|
5146
|
+
});
|
|
5147
|
+
created.push(top);
|
|
5148
|
+
const bottom = await host.createTrack({
|
|
5149
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-xf-t`,
|
|
5150
|
+
...adapter.createTrackOptions()
|
|
5151
|
+
});
|
|
5152
|
+
created.push(bottom);
|
|
5153
|
+
if (role) {
|
|
5154
|
+
await host.setTrackRole(top.id, role).catch(() => {
|
|
5155
|
+
});
|
|
5156
|
+
await host.setTrackRole(bottom.id, role).catch(() => {
|
|
5157
|
+
});
|
|
5158
|
+
}
|
|
5159
|
+
await host.writeMidiClip(top.id, clip);
|
|
5160
|
+
await host.writeMidiClip(bottom.id, clip);
|
|
5161
|
+
const copySound = async (newTrackId, sourceDbId) => {
|
|
5162
|
+
if (!host.getTrackSound) return "default";
|
|
5163
|
+
const snap = await host.getTrackSound(sourceDbId);
|
|
5164
|
+
if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) return "default";
|
|
5165
|
+
return adapter.sound.copySnapshot(newTrackId, snap);
|
|
5166
|
+
};
|
|
5167
|
+
const originLabel = await copySound(top.id, origin.dbId);
|
|
5168
|
+
const targetLabel = await copySound(bottom.id, target.dbId);
|
|
5169
|
+
await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
|
|
5170
|
+
const groupId = top.dbId;
|
|
5171
|
+
const originMeta = {
|
|
5172
|
+
groupId,
|
|
5173
|
+
slot: "origin",
|
|
5174
|
+
partnerDbId: bottom.dbId,
|
|
5175
|
+
sourceTrackDbId: origin.dbId,
|
|
5176
|
+
sourceSceneId: fromSceneId,
|
|
5177
|
+
sourceName: origin.name,
|
|
5178
|
+
soundLabel: originLabel,
|
|
5179
|
+
sliderPos: 0.5
|
|
5180
|
+
};
|
|
5181
|
+
const targetMeta = {
|
|
5182
|
+
groupId,
|
|
5183
|
+
slot: "target",
|
|
5184
|
+
partnerDbId: top.dbId,
|
|
5185
|
+
sourceTrackDbId: target.dbId,
|
|
5186
|
+
sourceSceneId: toSceneId,
|
|
5187
|
+
sourceName: target.name,
|
|
5188
|
+
soundLabel: targetLabel,
|
|
5189
|
+
sliderPos: 0.5
|
|
5190
|
+
};
|
|
5191
|
+
await host.setSceneData(scene, `track:${top.dbId}:crossfade`, originMeta);
|
|
5192
|
+
await host.setSceneData(scene, `track:${bottom.dbId}:crossfade`, targetMeta);
|
|
5193
|
+
await loadTracks(true);
|
|
5194
|
+
host.showToast("success", "Crossfade created", `${origin.name} \u2192 ${target.name}`);
|
|
5195
|
+
} catch (err) {
|
|
5196
|
+
for (const h of [...created].reverse()) {
|
|
5197
|
+
try {
|
|
5198
|
+
await host.deleteTrack(h.id);
|
|
5199
|
+
} catch {
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
5203
|
+
} finally {
|
|
5204
|
+
setIsCreatingCrossfade(false);
|
|
5205
|
+
}
|
|
5206
|
+
},
|
|
5207
|
+
[
|
|
5208
|
+
host,
|
|
5209
|
+
adapter,
|
|
5210
|
+
identity,
|
|
5211
|
+
activeSceneId,
|
|
5212
|
+
isConnected,
|
|
5213
|
+
isAuthenticated,
|
|
5214
|
+
tracks.length,
|
|
5215
|
+
sceneContext,
|
|
5216
|
+
applyCrossfadeAutomation,
|
|
5217
|
+
loadTracks
|
|
5218
|
+
]
|
|
5219
|
+
);
|
|
5220
|
+
const [isCreatingFade, setIsCreatingFade] = useState18(false);
|
|
5221
|
+
const handleCreateFade = useCallback13(
|
|
5222
|
+
async (selection, direction, gesture) => {
|
|
5223
|
+
const scene = activeSceneId;
|
|
5224
|
+
const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
|
|
5225
|
+
const toSceneId = sceneContext?.transitionToSceneId ?? "";
|
|
5226
|
+
if (!scene) throw new Error("No active scene.");
|
|
5227
|
+
if (!isConnected) throw new Error("Systems not connected.");
|
|
5228
|
+
if (!isAuthenticated) throw new Error("Please sign in to generate the fade.");
|
|
5229
|
+
if (tracks.length + 1 > identity.maxTracks) {
|
|
5230
|
+
throw new Error("Not enough track slots for a fade.");
|
|
5231
|
+
}
|
|
5232
|
+
setIsCreatingFade(true);
|
|
5233
|
+
const created = [];
|
|
5234
|
+
try {
|
|
5235
|
+
const role = selection.role ?? "";
|
|
5236
|
+
const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
|
|
5237
|
+
const mc = await host.getMusicalContext();
|
|
5238
|
+
const [srcMidi, srcKey] = await Promise.all([
|
|
5239
|
+
host.readImportableTrackMidi ? host.readImportableTrackMidi(selection.dbId) : Promise.resolve({ clips: [] }),
|
|
5240
|
+
host.getSceneKey ? host.getSceneKey(sourceSceneId) : Promise.resolve(null)
|
|
5241
|
+
]);
|
|
5242
|
+
const srcNotes = srcMidi.clips[0]?.notes ?? [];
|
|
5243
|
+
const keyStr = srcKey ? `${srcKey.key} ${srcKey.mode}` : null;
|
|
5244
|
+
const userPrompt = buildCrossfadeInpaintPrompt({
|
|
5245
|
+
role,
|
|
5246
|
+
bars: mc.bars,
|
|
5247
|
+
originName: direction === "out" ? selection.name : "silence",
|
|
5248
|
+
targetName: direction === "in" ? selection.name : "silence",
|
|
5249
|
+
originKey: direction === "out" ? keyStr : null,
|
|
5250
|
+
targetKey: direction === "in" ? keyStr : null,
|
|
5251
|
+
originNotes: direction === "out" ? srcNotes : [],
|
|
5252
|
+
targetNotes: direction === "in" ? srcNotes : []
|
|
5253
|
+
});
|
|
5254
|
+
const llm = await host.generateWithLLM({
|
|
5255
|
+
system: adapter.buildSystemPrompt(host.getValidRoles()),
|
|
5256
|
+
user: userPrompt,
|
|
5257
|
+
responseFormat: "json"
|
|
5258
|
+
});
|
|
5259
|
+
const parsed = adapter.parseNotesResponse(llm.content);
|
|
5260
|
+
if (!parsed || parsed.notes.length === 0) {
|
|
5261
|
+
throw new Error("The fade generator returned no notes.");
|
|
5262
|
+
}
|
|
5263
|
+
const notes = await host.postProcessMidi(parsed.notes, {
|
|
5264
|
+
quantize: true,
|
|
5265
|
+
removeOverlaps: true
|
|
5266
|
+
});
|
|
5267
|
+
const clip = {
|
|
5268
|
+
startTime: 0,
|
|
5269
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5270
|
+
tempo: mc.bpm,
|
|
5271
|
+
notes
|
|
5272
|
+
};
|
|
5273
|
+
const track = await host.createTrack({
|
|
5274
|
+
name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}`,
|
|
5275
|
+
...adapter.createTrackOptions()
|
|
5276
|
+
});
|
|
5277
|
+
created.push(track);
|
|
5278
|
+
if (role) await host.setTrackRole(track.id, role).catch(() => {
|
|
5279
|
+
});
|
|
5280
|
+
await host.writeMidiClip(track.id, clip);
|
|
5281
|
+
let soundLabel = "default";
|
|
5282
|
+
if (host.getTrackSound) {
|
|
5283
|
+
const snap = await host.getTrackSound(selection.dbId);
|
|
5284
|
+
if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
|
|
5285
|
+
soundLabel = await adapter.sound.copySnapshot(track.id, snap);
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
|
|
5289
|
+
appliedFadeAutomationRef.current.add(track.id);
|
|
5290
|
+
const meta = {
|
|
5291
|
+
direction,
|
|
5292
|
+
gesture,
|
|
5293
|
+
sourceTrackDbId: selection.dbId,
|
|
5294
|
+
sourceSceneId,
|
|
5295
|
+
sourceName: selection.name,
|
|
5296
|
+
soundLabel,
|
|
5297
|
+
sliderPos: 0.5
|
|
5298
|
+
};
|
|
5299
|
+
await host.setSceneData(scene, `track:${track.dbId}:fade`, meta);
|
|
5300
|
+
await loadTracks(true);
|
|
5301
|
+
host.showToast(
|
|
5302
|
+
"success",
|
|
5303
|
+
direction === "in" ? "Fade in created" : "Fade out created",
|
|
5304
|
+
selection.name
|
|
5305
|
+
);
|
|
5306
|
+
} catch (err) {
|
|
5307
|
+
for (const h of [...created].reverse()) {
|
|
5308
|
+
try {
|
|
5309
|
+
await host.deleteTrack(h.id);
|
|
5310
|
+
} catch {
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
5314
|
+
} finally {
|
|
5315
|
+
setIsCreatingFade(false);
|
|
5316
|
+
}
|
|
5317
|
+
},
|
|
5318
|
+
[
|
|
5319
|
+
host,
|
|
5320
|
+
adapter,
|
|
5321
|
+
identity,
|
|
5322
|
+
activeSceneId,
|
|
5323
|
+
isConnected,
|
|
5324
|
+
isAuthenticated,
|
|
5325
|
+
tracks.length,
|
|
5326
|
+
sceneContext,
|
|
5327
|
+
applyFadeAutomation,
|
|
5328
|
+
loadTracks
|
|
5329
|
+
]
|
|
5330
|
+
);
|
|
5331
|
+
const handleCrossfadeMute = useCallback13(
|
|
5332
|
+
(pair) => {
|
|
5333
|
+
const newMuted = !pair.origin.runtimeState.muted;
|
|
5334
|
+
for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
|
|
5335
|
+
setTracks(
|
|
5336
|
+
(prev) => prev.map(
|
|
5337
|
+
(t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
|
|
5338
|
+
)
|
|
5339
|
+
);
|
|
5340
|
+
host.setTrackMute(id, newMuted).catch(() => {
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5343
|
+
},
|
|
5344
|
+
[host, setTracks]
|
|
5345
|
+
);
|
|
5346
|
+
const handleCrossfadeSolo = useCallback13(
|
|
5347
|
+
(pair) => {
|
|
5348
|
+
const newSolo = !pair.origin.runtimeState.solo;
|
|
5349
|
+
for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
|
|
5350
|
+
setTracks(
|
|
5351
|
+
(prev) => prev.map(
|
|
5352
|
+
(t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
|
|
5353
|
+
)
|
|
5354
|
+
);
|
|
5355
|
+
host.setTrackSolo(id, newSolo).catch(() => {
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
},
|
|
5359
|
+
[host, setTracks]
|
|
5360
|
+
);
|
|
5361
|
+
const handleCrossfadeDelete = useCallback13(
|
|
5362
|
+
async (pair) => {
|
|
5363
|
+
try {
|
|
5364
|
+
for (const member of [pair.origin, pair.target]) {
|
|
5365
|
+
await host.deleteTrack(member.handle.id);
|
|
5366
|
+
if (activeSceneId) {
|
|
5367
|
+
await host.deleteSceneData(activeSceneId, `track:${member.handle.dbId}:crossfade`);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
setCrossfadePairsMeta((prev) => prev.filter((p) => p.groupId !== pair.groupId));
|
|
5371
|
+
setTracks(
|
|
5372
|
+
(prev) => prev.filter(
|
|
5373
|
+
(t) => t.handle.id !== pair.origin.handle.id && t.handle.id !== pair.target.handle.id
|
|
5374
|
+
)
|
|
5375
|
+
);
|
|
5376
|
+
host.showToast("success", "Crossfade removed");
|
|
5377
|
+
} catch (err) {
|
|
5378
|
+
host.showToast(
|
|
5379
|
+
"error",
|
|
5380
|
+
"Failed to delete crossfade",
|
|
5381
|
+
err instanceof Error ? err.message : String(err)
|
|
5382
|
+
);
|
|
5383
|
+
}
|
|
5384
|
+
},
|
|
5385
|
+
[host, activeSceneId, setCrossfadePairsMeta, setTracks]
|
|
5386
|
+
);
|
|
5387
|
+
const crossfadeSliderTimers = useRef16({});
|
|
5388
|
+
const handleCrossfadeSlider = useCallback13(
|
|
5389
|
+
(pair, pos) => {
|
|
5390
|
+
setCrossfadePairsMeta(
|
|
5391
|
+
(prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
|
|
5392
|
+
);
|
|
5393
|
+
if (crossfadeSliderTimers.current[pair.groupId]) {
|
|
5394
|
+
clearTimeout(crossfadeSliderTimers.current[pair.groupId]);
|
|
5395
|
+
}
|
|
5396
|
+
crossfadeSliderTimers.current[pair.groupId] = setTimeout(() => {
|
|
5397
|
+
void (async () => {
|
|
5398
|
+
const mc = await host.getMusicalContext();
|
|
5399
|
+
await applyCrossfadeAutomation(
|
|
5400
|
+
pair.origin.handle.id,
|
|
5401
|
+
pair.target.handle.id,
|
|
5402
|
+
mc.bars,
|
|
5403
|
+
mc.bpm,
|
|
5404
|
+
pos
|
|
5405
|
+
);
|
|
5406
|
+
if (activeSceneId) {
|
|
5407
|
+
const sceneData = await host.getAllSceneData(activeSceneId);
|
|
5408
|
+
for (const dbId of [pair.originDbId, pair.targetDbId]) {
|
|
5409
|
+
const meta = asCrossfadeMeta(sceneData[`track:${dbId}:crossfade`]);
|
|
5410
|
+
if (meta) {
|
|
5411
|
+
host.setSceneData(activeSceneId, `track:${dbId}:crossfade`, { ...meta, sliderPos: pos }).catch(() => {
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
})();
|
|
5417
|
+
}, 200);
|
|
5418
|
+
},
|
|
5419
|
+
[host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
|
|
5420
|
+
);
|
|
5421
|
+
const handleFadeDelete = useCallback13(
|
|
5422
|
+
async (fade) => {
|
|
5423
|
+
try {
|
|
5424
|
+
await host.deleteTrack(fade.track.handle.id);
|
|
5425
|
+
if (activeSceneId) {
|
|
5426
|
+
await host.deleteSceneData(activeSceneId, `track:${fade.dbId}:fade`);
|
|
5427
|
+
}
|
|
5428
|
+
setFadesMeta((prev) => prev.filter((f) => f.dbId !== fade.dbId));
|
|
5429
|
+
setTracks((prev) => prev.filter((t) => t.handle.id !== fade.track.handle.id));
|
|
5430
|
+
host.showToast("success", "Fade removed");
|
|
5431
|
+
} catch (err) {
|
|
5432
|
+
host.showToast(
|
|
5433
|
+
"error",
|
|
5434
|
+
"Failed to delete fade",
|
|
5435
|
+
err instanceof Error ? err.message : String(err)
|
|
5436
|
+
);
|
|
5437
|
+
}
|
|
5438
|
+
},
|
|
5439
|
+
[host, activeSceneId, setFadesMeta, setTracks]
|
|
5440
|
+
);
|
|
5441
|
+
const fadeSliderTimers = useRef16({});
|
|
5442
|
+
const handleFadeSlider = useCallback13(
|
|
5443
|
+
(fade, pos) => {
|
|
5444
|
+
setFadesMeta(
|
|
5445
|
+
(prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
|
|
5446
|
+
);
|
|
5447
|
+
if (fadeSliderTimers.current[fade.dbId]) clearTimeout(fadeSliderTimers.current[fade.dbId]);
|
|
5448
|
+
fadeSliderTimers.current[fade.dbId] = setTimeout(() => {
|
|
5449
|
+
void (async () => {
|
|
5450
|
+
const mc = await host.getMusicalContext();
|
|
5451
|
+
await applyFadeAutomation(
|
|
5452
|
+
fade.track.handle.id,
|
|
5453
|
+
fade.meta.direction,
|
|
5454
|
+
mc.bars,
|
|
5455
|
+
mc.bpm,
|
|
5456
|
+
pos,
|
|
5457
|
+
fade.meta.gesture
|
|
5458
|
+
);
|
|
5459
|
+
if (activeSceneId) {
|
|
5460
|
+
const sceneData = await host.getAllSceneData(activeSceneId);
|
|
5461
|
+
const meta = asFadeMeta(sceneData[`track:${fade.dbId}:fade`]);
|
|
5462
|
+
if (meta) {
|
|
5463
|
+
host.setSceneData(activeSceneId, `track:${fade.dbId}:fade`, { ...meta, sliderPos: pos }).catch(() => {
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
})();
|
|
5468
|
+
}, 200);
|
|
5469
|
+
},
|
|
5470
|
+
[host, activeSceneId, applyFadeAutomation, setFadesMeta]
|
|
5471
|
+
);
|
|
5472
|
+
const lastResyncKeyRef = useRef16("");
|
|
5473
|
+
useEffect15(() => {
|
|
5474
|
+
if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
|
|
5475
|
+
return;
|
|
5476
|
+
}
|
|
5477
|
+
const resyncKey = [
|
|
5478
|
+
...resolvedCrossfadePairs.map(
|
|
5479
|
+
(p) => `${p.origin.handle.dbId}<${p.originSourceDbId}|${p.target.handle.dbId}<${p.targetSourceDbId}`
|
|
5480
|
+
),
|
|
5481
|
+
...resolvedFades.map((f) => `${f.track.handle.dbId}<${f.meta.sourceTrackDbId}`)
|
|
5482
|
+
].join(",");
|
|
5483
|
+
if (resyncKey === lastResyncKeyRef.current) return;
|
|
5484
|
+
lastResyncKeyRef.current = resyncKey;
|
|
5485
|
+
let cancelled = false;
|
|
5486
|
+
const reapplyIfDrifted = async (layerTrackId, layerDbId, sourceDbId) => {
|
|
5487
|
+
if (!host.getTrackSound || cancelled) return;
|
|
5488
|
+
const [sourceSnap, layerSnap] = await Promise.all([
|
|
5489
|
+
host.getTrackSound(sourceDbId),
|
|
5490
|
+
host.getTrackSound(layerDbId)
|
|
5491
|
+
]);
|
|
5492
|
+
if (cancelled || !sourceSnap || sourceSnap.kind !== adapter.sound.acceptedSnapshotKind) {
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
if (soundIdentity(sourceSnap) === soundIdentity(layerSnap)) return;
|
|
5496
|
+
try {
|
|
5497
|
+
await adapter.sound.copySnapshot(layerTrackId, sourceSnap);
|
|
5498
|
+
} catch {
|
|
5499
|
+
}
|
|
5500
|
+
};
|
|
5501
|
+
void (async () => {
|
|
5502
|
+
for (const pair of resolvedCrossfadePairs) {
|
|
5503
|
+
await reapplyIfDrifted(pair.origin.handle.id, pair.origin.handle.dbId, pair.originSourceDbId);
|
|
5504
|
+
await reapplyIfDrifted(pair.target.handle.id, pair.target.handle.dbId, pair.targetSourceDbId);
|
|
5505
|
+
}
|
|
5506
|
+
for (const fade of resolvedFades) {
|
|
5507
|
+
await reapplyIfDrifted(fade.track.handle.id, fade.track.handle.dbId, fade.meta.sourceTrackDbId);
|
|
5508
|
+
}
|
|
5509
|
+
})();
|
|
5510
|
+
return () => {
|
|
5511
|
+
cancelled = true;
|
|
5512
|
+
};
|
|
5513
|
+
}, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
|
|
5514
|
+
useEffect15(() => {
|
|
5515
|
+
if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
|
|
5516
|
+
void (async () => {
|
|
5517
|
+
const mc = await host.getMusicalContext();
|
|
5518
|
+
for (const fade of resolvedFades) {
|
|
5519
|
+
const id = fade.track.handle.id;
|
|
5520
|
+
if (appliedFadeAutomationRef.current.has(id)) continue;
|
|
5521
|
+
appliedFadeAutomationRef.current.add(id);
|
|
5522
|
+
await applyFadeAutomation(
|
|
5523
|
+
id,
|
|
5524
|
+
fade.meta.direction,
|
|
5525
|
+
mc.bars,
|
|
5526
|
+
mc.bpm,
|
|
5527
|
+
fade.meta.sliderPos,
|
|
5528
|
+
fade.meta.gesture
|
|
5529
|
+
);
|
|
5530
|
+
}
|
|
5531
|
+
})();
|
|
5532
|
+
}, [resolvedFades, host, applyFadeAutomation]);
|
|
5533
|
+
return {
|
|
5534
|
+
isCreatingCrossfade,
|
|
5535
|
+
isCreatingFade,
|
|
5536
|
+
handleCreateCrossfade,
|
|
5537
|
+
handleCreateFade,
|
|
5538
|
+
handleCrossfadeMute,
|
|
5539
|
+
handleCrossfadeSolo,
|
|
5540
|
+
handleCrossfadeDelete,
|
|
5541
|
+
handleCrossfadeSlider,
|
|
5542
|
+
handleFadeDelete,
|
|
5543
|
+
handleFadeSlider
|
|
5544
|
+
};
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
// src/panel-core/useGeneratorPanelCore.tsx
|
|
5548
|
+
import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
5549
|
+
var EMPTY_PLACEHOLDERS = [];
|
|
5550
|
+
function useGeneratorPanelCore({
|
|
5551
|
+
ui,
|
|
5552
|
+
adapter
|
|
5553
|
+
}) {
|
|
5554
|
+
const {
|
|
5555
|
+
host,
|
|
5556
|
+
activeSceneId,
|
|
5557
|
+
isAuthenticated,
|
|
5558
|
+
isConnected,
|
|
5559
|
+
onHeaderContent,
|
|
5560
|
+
onLoading,
|
|
5561
|
+
sceneContext,
|
|
5562
|
+
onOpenContract,
|
|
5563
|
+
onExpandSelf,
|
|
5564
|
+
isExpanded
|
|
5565
|
+
} = ui;
|
|
5566
|
+
const { identity, features } = adapter;
|
|
5567
|
+
const logTag = identity.logTag;
|
|
5568
|
+
const adapterRef = useRef17(adapter);
|
|
5569
|
+
useEffect16(() => {
|
|
5570
|
+
if (adapterRef.current !== adapter) {
|
|
5571
|
+
adapterRef.current = adapter;
|
|
5572
|
+
console.warn(
|
|
5573
|
+
`[${logTag}] GeneratorPanelAdapter identity changed between renders \u2014 wrap it in useMemo(() => createAdapter(host), [host]) to avoid load loops.`
|
|
5574
|
+
);
|
|
5575
|
+
}
|
|
5576
|
+
}, [adapter, logTag]);
|
|
5577
|
+
const supportsMeters = typeof host.getTrackLevels === "function";
|
|
5578
|
+
const trackLevels = useTrackLevels(host, isExpanded);
|
|
5579
|
+
const [tracks, setTracks] = useState19([]);
|
|
5580
|
+
const [isLoadingTracks, setIsLoadingTracks] = useState19(false);
|
|
5581
|
+
const [importOpen, setImportOpen] = useState19(false);
|
|
5582
|
+
const [soundImportTarget, setSoundImportTarget] = useState19(null);
|
|
5583
|
+
const [designerView, setDesignerView] = useState19(false);
|
|
5584
|
+
const [transitionSourceTotal, setTransitionSourceTotal] = useState19(0);
|
|
5585
|
+
const [crossfadePairsMeta, setCrossfadePairsMeta] = useState19([]);
|
|
5586
|
+
const [fadesMeta, setFadesMeta] = useState19([]);
|
|
5587
|
+
const [genericGroupMetas, setGenericGroupMetas] = useState19({});
|
|
5588
|
+
const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
|
|
5589
|
+
const [placeholders, , setPlaceholdersForScene] = useSceneState(
|
|
5590
|
+
activeSceneId,
|
|
5591
|
+
EMPTY_PLACEHOLDERS
|
|
5592
|
+
);
|
|
5593
|
+
const saveTimeoutRefs = useRef17({});
|
|
5594
|
+
const editLoadStartedRef = useRef17(/* @__PURE__ */ new Set());
|
|
5595
|
+
const [availableInstruments, setAvailableInstruments] = useState19([]);
|
|
5596
|
+
const [instrumentsLoading, setInstrumentsLoading] = useState19(false);
|
|
5597
|
+
const engineToDbIdRef = useRef17(/* @__PURE__ */ new Map());
|
|
5598
|
+
const tracksLoadedForSceneRef = useRef17(null);
|
|
5599
|
+
const persistSoundHistory = useCallback14(
|
|
5600
|
+
(trackId, state) => {
|
|
5601
|
+
if (!activeSceneId) return;
|
|
5602
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
5603
|
+
host.setSceneData(activeSceneId, trackDataKey(dbId, "soundHistory"), state).catch(() => {
|
|
5604
|
+
});
|
|
5605
|
+
},
|
|
5606
|
+
[host, activeSceneId]
|
|
5607
|
+
);
|
|
5608
|
+
const soundHistory = useSoundHistory(adapter.sound.applySound, {
|
|
5609
|
+
max: adapter.sound.historyMax,
|
|
5610
|
+
onChange: persistSoundHistory
|
|
5611
|
+
});
|
|
5612
|
+
const anySolo = useAnySolo(host);
|
|
5613
|
+
const reorder = useTrackReorder({
|
|
5614
|
+
host,
|
|
5615
|
+
items: tracks,
|
|
5616
|
+
setItems: setTracks,
|
|
5617
|
+
getId: (t) => t.handle.dbId
|
|
5618
|
+
});
|
|
5619
|
+
const loadTracks = useCallback14(
|
|
5620
|
+
async (incremental = false) => {
|
|
5621
|
+
const sceneAtStart = activeSceneId;
|
|
5622
|
+
if (!sceneAtStart) {
|
|
5623
|
+
setTracks([]);
|
|
5624
|
+
setCrossfadePairsMeta([]);
|
|
5625
|
+
setFadesMeta([]);
|
|
5626
|
+
setGenericGroupMetas({});
|
|
5627
|
+
tracksLoadedForSceneRef.current = null;
|
|
5628
|
+
setIsLoadingTracks(false);
|
|
5629
|
+
return;
|
|
5630
|
+
}
|
|
5631
|
+
if (!incremental && tracksLoadedForSceneRef.current !== sceneAtStart) {
|
|
5632
|
+
setTracks([]);
|
|
5633
|
+
}
|
|
5634
|
+
tracksLoadedForSceneRef.current = sceneAtStart;
|
|
5635
|
+
if (!incremental) soundHistory.reset();
|
|
5636
|
+
const isStale = () => tracksLoadedForSceneRef.current !== sceneAtStart;
|
|
5637
|
+
if (!incremental) setIsLoadingTracks(true);
|
|
5638
|
+
try {
|
|
5639
|
+
await host.adoptSceneTracks();
|
|
5640
|
+
if (isStale()) return;
|
|
5641
|
+
const handles = await host.getPluginTracks();
|
|
5642
|
+
if (isStale()) return;
|
|
5643
|
+
const sceneData = await host.getAllSceneData(sceneAtStart);
|
|
5644
|
+
if (isStale()) return;
|
|
5645
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
5646
|
+
for (const h of handles) {
|
|
5647
|
+
idMap.set(h.id, h.dbId);
|
|
5648
|
+
}
|
|
5649
|
+
engineToDbIdRef.current = idMap;
|
|
5650
|
+
const trackStates = [];
|
|
5651
|
+
for (const handle of handles) {
|
|
5652
|
+
let runtimeState = {
|
|
5653
|
+
id: handle.id,
|
|
5654
|
+
muted: false,
|
|
5655
|
+
solo: false,
|
|
5656
|
+
volume: 0.75,
|
|
5657
|
+
pan: 0
|
|
5658
|
+
};
|
|
5659
|
+
let hasMidi = false;
|
|
5660
|
+
try {
|
|
5661
|
+
const info = await host.getTrackInfo(handle.id);
|
|
5662
|
+
runtimeState = {
|
|
5663
|
+
id: handle.id,
|
|
5664
|
+
muted: info.muted,
|
|
5665
|
+
solo: info.soloed,
|
|
5666
|
+
volume: info.volume,
|
|
5667
|
+
pan: info.pan
|
|
5668
|
+
};
|
|
5669
|
+
hasMidi = info.hasMidi;
|
|
5670
|
+
} catch {
|
|
5671
|
+
}
|
|
5672
|
+
let fxDetailState = newTrackState(handle).fxDetailState;
|
|
5673
|
+
try {
|
|
5674
|
+
const fxState = await host.getTrackFxState(handle.id);
|
|
5675
|
+
fxDetailState = pluginFxToToggleFx(fxState);
|
|
5676
|
+
} catch {
|
|
5677
|
+
}
|
|
5678
|
+
const promptKey = trackDataKey(handle.dbId, "prompt");
|
|
5679
|
+
let prompt = typeof sceneData[promptKey] === "string" ? sceneData[promptKey] : "";
|
|
5680
|
+
if (!prompt && handle.prompt) {
|
|
5681
|
+
prompt = handle.prompt;
|
|
5682
|
+
host.setSceneData(sceneAtStart, promptKey, prompt).catch(() => {
|
|
5683
|
+
});
|
|
5684
|
+
}
|
|
5685
|
+
if (!hasMidi && handle.role) {
|
|
5686
|
+
hasMidi = true;
|
|
5687
|
+
}
|
|
5688
|
+
let instrumentMissing = false;
|
|
5689
|
+
if (handle.instrumentPluginId) {
|
|
5690
|
+
try {
|
|
5691
|
+
const instrDescriptor = await host.getTrackInstrument(handle.id);
|
|
5692
|
+
if (instrDescriptor?.missing) {
|
|
5693
|
+
instrumentMissing = true;
|
|
5694
|
+
}
|
|
5695
|
+
} catch {
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
trackStates.push(
|
|
5699
|
+
newTrackState(handle, {
|
|
5700
|
+
prompt,
|
|
5701
|
+
role: handle.role ?? "",
|
|
5702
|
+
runtimeState,
|
|
5703
|
+
fxDetailState,
|
|
5704
|
+
hasMidi,
|
|
5705
|
+
instrumentMissing
|
|
5706
|
+
})
|
|
5707
|
+
);
|
|
5708
|
+
}
|
|
5709
|
+
if (isStale()) return;
|
|
5710
|
+
setTracks((prev) => {
|
|
5711
|
+
const prevByDbId = new Map(prev.map((p) => [p.handle.dbId, p]));
|
|
5712
|
+
return trackStates.map((ts) => {
|
|
5713
|
+
const carry = prevByDbId.get(ts.handle.dbId);
|
|
5714
|
+
return carry ? { ...ts, editNotes: carry.editNotes, editBars: carry.editBars, editBpm: carry.editBpm } : ts;
|
|
5715
|
+
});
|
|
5716
|
+
});
|
|
5717
|
+
for (const ts of trackStates) {
|
|
5718
|
+
const persisted = sceneData[trackDataKey(ts.handle.dbId, "soundHistory")];
|
|
5719
|
+
if (persisted && typeof persisted === "object") {
|
|
5720
|
+
soundHistory.restore(ts.handle.id, persisted);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
if (!isStale()) {
|
|
5724
|
+
setCrossfadePairsMeta(parseCrossfadePairs(sceneData));
|
|
5725
|
+
setFadesMeta(parseFades(sceneData));
|
|
5726
|
+
if (adapter.groupExtensions && adapter.groupExtensions.length > 0) {
|
|
5727
|
+
const map = {};
|
|
5728
|
+
for (const ext of adapter.groupExtensions) {
|
|
5729
|
+
map[ext.metaKey] = parseTrackGroups(sceneData, ext);
|
|
5730
|
+
}
|
|
5731
|
+
setGenericGroupMetas(map);
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
} catch (error) {
|
|
5735
|
+
console.error(`[${logTag}] Failed to load tracks:`, error);
|
|
5736
|
+
} finally {
|
|
5737
|
+
if (tracksLoadedForSceneRef.current === sceneAtStart) {
|
|
5738
|
+
setIsLoadingTracks(false);
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
},
|
|
5742
|
+
[host, activeSceneId, soundHistory, adapter, logTag]
|
|
5743
|
+
);
|
|
5744
|
+
useEffect16(() => {
|
|
5745
|
+
loadTracks();
|
|
5746
|
+
}, [loadTracks]);
|
|
5747
|
+
useEffect16(() => {
|
|
5748
|
+
const map = /* @__PURE__ */ new Map();
|
|
5749
|
+
for (const t of tracks) {
|
|
5750
|
+
map.set(t.handle.id, t.handle.dbId);
|
|
5751
|
+
}
|
|
5752
|
+
engineToDbIdRef.current = map;
|
|
5753
|
+
}, [tracks]);
|
|
5754
|
+
const loadedCompletedIdsRef = useRef17(/* @__PURE__ */ new Set());
|
|
5755
|
+
useEffect16(() => {
|
|
5756
|
+
if (placeholders.length === 0) {
|
|
5757
|
+
loadedCompletedIdsRef.current.clear();
|
|
5758
|
+
return;
|
|
5759
|
+
}
|
|
5760
|
+
const newCompleted = placeholders.filter(
|
|
5761
|
+
(ph) => ph.status === "completed" && !loadedCompletedIdsRef.current.has(ph.id)
|
|
5762
|
+
);
|
|
5763
|
+
if (newCompleted.length > 0) {
|
|
5764
|
+
for (const ph of newCompleted) {
|
|
5765
|
+
loadedCompletedIdsRef.current.add(ph.id);
|
|
5766
|
+
}
|
|
5767
|
+
console.log(
|
|
5768
|
+
`[${logTag}] ${newCompleted.length} track(s) completed, reloading. IDs:`,
|
|
5769
|
+
newCompleted.map((ph) => ph.id)
|
|
5770
|
+
);
|
|
5771
|
+
loadTracks(true);
|
|
5772
|
+
}
|
|
5773
|
+
}, [placeholders, loadTracks, logTag]);
|
|
5774
|
+
const adoptAndLoad = useCallback14(() => {
|
|
5775
|
+
loadTracks(true);
|
|
5776
|
+
}, [loadTracks]);
|
|
5777
|
+
useEffect16(() => {
|
|
5778
|
+
const unsub = host.onEngineReady(() => {
|
|
5779
|
+
adoptAndLoad();
|
|
5780
|
+
});
|
|
5781
|
+
return unsub;
|
|
5782
|
+
}, [host, adoptAndLoad]);
|
|
5783
|
+
useEffect16(() => {
|
|
5784
|
+
if (typeof host.onAfterAgentMutation !== "function") return;
|
|
5785
|
+
let timer = null;
|
|
5786
|
+
const unsub = host.onAfterAgentMutation(() => {
|
|
5787
|
+
if (timer) clearTimeout(timer);
|
|
5788
|
+
timer = setTimeout(() => {
|
|
5789
|
+
timer = null;
|
|
5790
|
+
loadTracks(true);
|
|
5791
|
+
}, 500);
|
|
5792
|
+
});
|
|
5793
|
+
return () => {
|
|
5794
|
+
unsub?.();
|
|
5795
|
+
if (timer) clearTimeout(timer);
|
|
5796
|
+
};
|
|
5797
|
+
}, [host, loadTracks]);
|
|
5798
|
+
useEffect16(() => {
|
|
5799
|
+
const unsub = host.onTrackStateChange((trackId, state) => {
|
|
5800
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
|
|
5801
|
+
});
|
|
5802
|
+
return unsub;
|
|
5803
|
+
}, [host]);
|
|
5804
|
+
useEffect16(() => {
|
|
5805
|
+
if (!features.bulkComposePlaceholders) return;
|
|
5806
|
+
console.log(`[${logTag}] Subscribing to composeProgress`);
|
|
5807
|
+
const unsub = host.onComposeProgress((event) => {
|
|
5808
|
+
const targetScene = event.sceneId;
|
|
5809
|
+
if (!targetScene) return;
|
|
5810
|
+
console.log(
|
|
5811
|
+
`[${logTag}] composeProgress event:`,
|
|
5812
|
+
event.phase,
|
|
5813
|
+
"sceneId:",
|
|
5814
|
+
targetScene,
|
|
5815
|
+
"placeholders:",
|
|
5816
|
+
event.placeholders?.length ?? "none"
|
|
5817
|
+
);
|
|
5818
|
+
switch (event.phase) {
|
|
5819
|
+
case "planning":
|
|
5820
|
+
setIsComposingForScene(targetScene, true);
|
|
5821
|
+
setPlaceholdersForScene(targetScene, []);
|
|
5822
|
+
break;
|
|
5823
|
+
case "generating":
|
|
5824
|
+
setIsComposingForScene(targetScene, false);
|
|
5825
|
+
if (event.placeholders) {
|
|
5826
|
+
setPlaceholdersForScene(targetScene, event.placeholders);
|
|
5827
|
+
}
|
|
5828
|
+
break;
|
|
5829
|
+
case "complete":
|
|
5830
|
+
case "error":
|
|
5831
|
+
setIsComposingForScene(targetScene, false);
|
|
5832
|
+
setPlaceholdersForScene(targetScene, EMPTY_PLACEHOLDERS);
|
|
5833
|
+
break;
|
|
5834
|
+
}
|
|
5835
|
+
});
|
|
5836
|
+
return unsub;
|
|
5837
|
+
}, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
|
|
5838
|
+
useEffect16(() => {
|
|
5839
|
+
const refs = saveTimeoutRefs;
|
|
5840
|
+
return () => {
|
|
5841
|
+
for (const timeout of Object.values(refs.current)) {
|
|
5842
|
+
clearTimeout(timeout);
|
|
5843
|
+
}
|
|
5844
|
+
};
|
|
5845
|
+
}, []);
|
|
5846
|
+
const isAddingTrackRef = useRef17(false);
|
|
5847
|
+
const [isAddingTrack, setIsAddingTrack] = useState19(false);
|
|
5848
|
+
const handleAddTrack = useCallback14(async () => {
|
|
5849
|
+
if (isAddingTrackRef.current) return;
|
|
5850
|
+
if (!activeSceneId) {
|
|
5851
|
+
host.showToast("warning", "Select SCENE");
|
|
5852
|
+
return;
|
|
5853
|
+
}
|
|
5854
|
+
if (!isConnected) {
|
|
5855
|
+
host.showToast("warning", "Systems not connected");
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
if (!isAuthenticated) {
|
|
5859
|
+
host.showToast("warning", "Sign In Required", "Please sign in to add tracks");
|
|
5860
|
+
return;
|
|
5861
|
+
}
|
|
5862
|
+
if (tracks.length >= identity.maxTracks) return;
|
|
5863
|
+
isAddingTrackRef.current = true;
|
|
5864
|
+
setIsAddingTrack(true);
|
|
5865
|
+
try {
|
|
5866
|
+
const handle = await host.createTrack({
|
|
5867
|
+
name: `${identity.trackNamePrefix}-${Date.now()}`,
|
|
5868
|
+
...adapter.createTrackOptions()
|
|
5869
|
+
});
|
|
5870
|
+
setTracks((prev) => [...prev, newTrackState(handle)]);
|
|
5871
|
+
onExpandSelf?.();
|
|
5872
|
+
setTimeout(() => {
|
|
5873
|
+
const inputs = document.querySelectorAll(
|
|
5874
|
+
`[data-testid="${identity.familyKey}-section"] [data-testid="sdk-prompt-input"]`
|
|
5875
|
+
);
|
|
5876
|
+
if (inputs.length > 0) {
|
|
5877
|
+
inputs[inputs.length - 1].focus();
|
|
5878
|
+
}
|
|
5879
|
+
}, 350);
|
|
5880
|
+
} catch (error) {
|
|
5881
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
5882
|
+
host.showToast("error", "Failed to create track", msg);
|
|
5883
|
+
} finally {
|
|
5884
|
+
isAddingTrackRef.current = false;
|
|
5885
|
+
setIsAddingTrack(false);
|
|
5886
|
+
}
|
|
5887
|
+
}, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
|
|
5888
|
+
const handlePortTrack = useCallback14(
|
|
5889
|
+
async (sel) => {
|
|
5890
|
+
if (!activeSceneId) {
|
|
5891
|
+
host.showToast("warning", "Select SCENE");
|
|
5892
|
+
return;
|
|
5893
|
+
}
|
|
5894
|
+
if (!isConnected) {
|
|
5895
|
+
host.showToast("warning", "Systems not connected");
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
if (tracks.length >= identity.maxTracks) {
|
|
5899
|
+
host.showToast("warning", "Track limit reached");
|
|
5900
|
+
return;
|
|
5901
|
+
}
|
|
5902
|
+
if (!host.readImportableTrackMidi) return;
|
|
5903
|
+
let handle = null;
|
|
5904
|
+
try {
|
|
5905
|
+
handle = await host.createTrack({
|
|
5906
|
+
name: `${identity.trackNamePrefix}-${Date.now()}`,
|
|
5907
|
+
...adapter.createTrackOptions()
|
|
5908
|
+
});
|
|
5909
|
+
if (sel.role) {
|
|
5910
|
+
try {
|
|
5911
|
+
await host.setTrackRole(handle.id, sel.role);
|
|
5912
|
+
} catch {
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
const midi = await host.readImportableTrackMidi(sel.sourceTrackDbId);
|
|
5916
|
+
const notes = midi.clips[0]?.notes ?? [];
|
|
5917
|
+
if (notes.length > 0) {
|
|
5918
|
+
const mc = await host.getMusicalContext();
|
|
5919
|
+
await host.writeMidiClip(handle.id, {
|
|
5920
|
+
startTime: 0,
|
|
5921
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
5922
|
+
tempo: mc.bpm,
|
|
5923
|
+
notes
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
await adapter.applyPortedTrackSound(handle, sel.role);
|
|
5927
|
+
host.showToast(
|
|
5928
|
+
"success",
|
|
5929
|
+
`Imported to ${identity.familyKey}`,
|
|
5930
|
+
notes.length ? `${sel.trackName} \u2192 ${identity.familyKey}` : `${sel.trackName} (no MIDI yet)`
|
|
5931
|
+
);
|
|
5932
|
+
await loadTracks(true);
|
|
5933
|
+
} catch (err) {
|
|
5934
|
+
if (handle) {
|
|
5935
|
+
try {
|
|
5936
|
+
await host.deleteTrack(handle.id);
|
|
5937
|
+
} catch {
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
|
|
5941
|
+
}
|
|
5942
|
+
},
|
|
5943
|
+
[host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
|
|
5944
|
+
);
|
|
5945
|
+
const handleSoundImportPick = useCallback14(
|
|
5946
|
+
async (sel) => {
|
|
5947
|
+
const target = soundImportTarget;
|
|
5948
|
+
if (!target || !host.getTrackSound) {
|
|
5949
|
+
setSoundImportTarget(null);
|
|
5950
|
+
return;
|
|
5951
|
+
}
|
|
5952
|
+
const noun = adapter.sound.importNoun;
|
|
5953
|
+
const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
|
|
5954
|
+
try {
|
|
5955
|
+
const snap = await host.getTrackSound(sel.sourceTrackDbId);
|
|
5956
|
+
if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
|
|
5957
|
+
host.showToast(
|
|
5958
|
+
"error",
|
|
5959
|
+
`No ${noun} to import`,
|
|
5960
|
+
`${sel.trackName} has no ${identity.familyKey} ${noun}.`
|
|
5961
|
+
);
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5964
|
+
const descriptor = adapter.sound.descriptorFromSnapshot(snap);
|
|
5965
|
+
await adapter.sound.applySound(target.handle.id, descriptor);
|
|
5966
|
+
soundHistory.record(target.handle.id, descriptor, snap.label);
|
|
5967
|
+
host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
|
|
5968
|
+
} catch (err) {
|
|
5969
|
+
host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
|
|
5970
|
+
} finally {
|
|
5971
|
+
setSoundImportTarget(null);
|
|
5972
|
+
}
|
|
5973
|
+
},
|
|
5974
|
+
[soundImportTarget, host, adapter, identity.familyKey, soundHistory]
|
|
5975
|
+
);
|
|
5976
|
+
const [isExportingMidi, setIsExportingMidi] = useState19(false);
|
|
5977
|
+
const handleExportMidi = useCallback14(async () => {
|
|
5978
|
+
if (isExportingMidi) return;
|
|
5979
|
+
setIsExportingMidi(true);
|
|
5980
|
+
try {
|
|
5981
|
+
const result = await host.exportTracksAsMidiBundle({
|
|
5982
|
+
defaultName: identity.exportDefaultName ?? "midi-tracks"
|
|
5983
|
+
});
|
|
5984
|
+
if (result.success) {
|
|
5985
|
+
const filename = result.filePath.split("/").pop() || result.filePath;
|
|
5986
|
+
const skippedNote = result.skippedCount > 0 ? ` (${result.skippedCount} empty track${result.skippedCount === 1 ? "" : "s"} skipped)` : "";
|
|
5987
|
+
host.showToast(
|
|
5988
|
+
"success",
|
|
5989
|
+
"MIDI exported",
|
|
5990
|
+
`${result.trackCount} track${result.trackCount === 1 ? "" : "s"} \u2192 ${filename}${skippedNote}`
|
|
5991
|
+
);
|
|
5992
|
+
} else if (!("canceled" in result && result.canceled)) {
|
|
5993
|
+
const errMsg = "error" in result ? result.error : "Unknown error";
|
|
5994
|
+
host.showToast("error", "Export failed", errMsg);
|
|
5995
|
+
}
|
|
5996
|
+
} catch (error) {
|
|
5997
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5998
|
+
host.showToast("error", "Export failed", msg);
|
|
5999
|
+
} finally {
|
|
6000
|
+
setIsExportingMidi(false);
|
|
6001
|
+
}
|
|
6002
|
+
}, [host, identity.exportDefaultName, isExportingMidi]);
|
|
6003
|
+
const isBulkActive = !!(isComposing || placeholders.length > 0);
|
|
6004
|
+
const needsContract = !sceneContext?.hasContract;
|
|
6005
|
+
const xfFromId = sceneContext?.transitionFromSceneId ?? null;
|
|
6006
|
+
const xfToId = sceneContext?.transitionToSceneId ?? null;
|
|
6007
|
+
const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
|
|
6008
|
+
useEffect16(() => {
|
|
6009
|
+
if (!canCrossfade) setDesignerView(false);
|
|
6010
|
+
}, [canCrossfade]);
|
|
6011
|
+
useEffect16(() => {
|
|
6012
|
+
if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
|
|
6013
|
+
setTransitionSourceTotal(0);
|
|
6014
|
+
return;
|
|
6015
|
+
}
|
|
6016
|
+
let cancelled = false;
|
|
6017
|
+
void Promise.all([host.listSceneFamilyTracks(xfFromId), host.listSceneFamilyTracks(xfToId)]).then(([a, b]) => {
|
|
6018
|
+
if (!cancelled) setTransitionSourceTotal(a.length + b.length);
|
|
6019
|
+
}).catch(() => {
|
|
6020
|
+
if (!cancelled) setTransitionSourceTotal(0);
|
|
6021
|
+
});
|
|
6022
|
+
return () => {
|
|
6023
|
+
cancelled = true;
|
|
6024
|
+
};
|
|
6025
|
+
}, [canCrossfade, xfFromId, xfToId, host]);
|
|
6026
|
+
const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
|
|
6027
|
+
useEffect16(() => {
|
|
6028
|
+
if (!onHeaderContent) return;
|
|
6029
|
+
const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
|
|
6030
|
+
onHeaderContent(
|
|
6031
|
+
/* @__PURE__ */ jsxs17("div", { className: "flex gap-1 items-center", children: [
|
|
6032
|
+
features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx23(
|
|
6033
|
+
"button",
|
|
6034
|
+
{
|
|
6035
|
+
"data-testid": `import-from-scene-${identity.familyKey}-button`,
|
|
6036
|
+
onClick: (e) => {
|
|
6037
|
+
e.stopPropagation();
|
|
6038
|
+
onExpandSelf?.();
|
|
6039
|
+
setImportOpen(true);
|
|
6040
|
+
},
|
|
6041
|
+
disabled: !activeSceneId || needsContract,
|
|
6042
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${!activeSceneId || needsContract ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
|
|
6043
|
+
children: identity.importTrackLabel ?? "Import Track"
|
|
6044
|
+
}
|
|
6045
|
+
),
|
|
6046
|
+
(!canCrossfade || !designerView) && /* @__PURE__ */ jsx23(
|
|
6047
|
+
"button",
|
|
6048
|
+
{
|
|
6049
|
+
"data-testid": `add-${identity.familyKey}-track-button`,
|
|
6050
|
+
onClick: (e) => {
|
|
6051
|
+
e.stopPropagation();
|
|
6052
|
+
if (needsContract) {
|
|
6053
|
+
onOpenContract?.();
|
|
6054
|
+
return;
|
|
6055
|
+
}
|
|
6056
|
+
handleAddTrack();
|
|
6057
|
+
},
|
|
6058
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${addDisabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-accent/10 border-sas-accent/30 text-sas-accent hover:bg-sas-accent/20"}`,
|
|
6059
|
+
children: identity.addTrackLabel ?? "Add Track"
|
|
6060
|
+
}
|
|
6061
|
+
),
|
|
6062
|
+
canCrossfade && /* @__PURE__ */ jsxs17(
|
|
6063
|
+
"button",
|
|
6064
|
+
{
|
|
6065
|
+
"data-testid": `${identity.familyKey}-view-toggle`,
|
|
6066
|
+
onClick: (e) => {
|
|
6067
|
+
e.stopPropagation();
|
|
6068
|
+
if (!designerView) {
|
|
6069
|
+
if (needsContract) {
|
|
6070
|
+
onOpenContract?.();
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
onExpandSelf?.();
|
|
6074
|
+
}
|
|
6075
|
+
setDesignerView((v) => !v);
|
|
6076
|
+
},
|
|
6077
|
+
disabled: !designerView && needsContract,
|
|
6078
|
+
title: designerView ? "Back to the track list" : "Open the transition designer",
|
|
6079
|
+
className: "relative overflow-hidden px-2 py-0.5 text-[10px] font-medium rounded-sm border border-sas-accent/40 text-sas-accent transition-colors hover:border-sas-accent disabled:opacity-50",
|
|
6080
|
+
children: [
|
|
6081
|
+
transitionSourceTotal > 0 && /* @__PURE__ */ jsx23(
|
|
6082
|
+
"span",
|
|
6083
|
+
{
|
|
6084
|
+
className: "absolute inset-y-0 left-0 bg-sas-accent/25",
|
|
6085
|
+
style: { width: `${Math.min(100, transitionDone / transitionSourceTotal * 100)}%` },
|
|
6086
|
+
"aria-hidden": true
|
|
6087
|
+
}
|
|
6088
|
+
),
|
|
6089
|
+
/* @__PURE__ */ jsxs17("span", { className: "relative", children: [
|
|
6090
|
+
"\u21C4 ",
|
|
6091
|
+
designerView ? "Transition" : "Tracks",
|
|
6092
|
+
transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
|
|
6093
|
+
] })
|
|
6094
|
+
]
|
|
6095
|
+
}
|
|
6096
|
+
)
|
|
6097
|
+
] })
|
|
6098
|
+
);
|
|
6099
|
+
return () => {
|
|
6100
|
+
onHeaderContent(null);
|
|
6101
|
+
};
|
|
6102
|
+
}, [
|
|
6103
|
+
onHeaderContent,
|
|
6104
|
+
needsContract,
|
|
6105
|
+
isConnected,
|
|
6106
|
+
activeSceneId,
|
|
6107
|
+
tracks.length,
|
|
6108
|
+
isAddingTrack,
|
|
6109
|
+
handleAddTrack,
|
|
6110
|
+
onOpenContract,
|
|
6111
|
+
host,
|
|
6112
|
+
canCrossfade,
|
|
6113
|
+
designerView,
|
|
6114
|
+
transitionDone,
|
|
6115
|
+
transitionSourceTotal,
|
|
6116
|
+
onExpandSelf,
|
|
6117
|
+
identity,
|
|
6118
|
+
features.importTracks
|
|
6119
|
+
]);
|
|
6120
|
+
useEffect16(() => {
|
|
6121
|
+
if (!onLoading) return;
|
|
6122
|
+
const anyGenerating = tracks.some((t) => t.isGenerating);
|
|
6123
|
+
onLoading(isLoadingTracks || anyGenerating || isBulkActive);
|
|
6124
|
+
return () => {
|
|
6125
|
+
onLoading(false);
|
|
6126
|
+
};
|
|
6127
|
+
}, [onLoading, isLoadingTracks, tracks, isBulkActive]);
|
|
6128
|
+
const handleDeleteTrack = useCallback14(
|
|
6129
|
+
async (trackId) => {
|
|
6130
|
+
try {
|
|
6131
|
+
await host.deleteTrack(trackId);
|
|
6132
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
6133
|
+
if (activeSceneId) {
|
|
6134
|
+
await host.deleteSceneData(activeSceneId, trackDataKey(dbId, "prompt"));
|
|
6135
|
+
}
|
|
6136
|
+
setTracks((prev) => prev.filter((t) => t.handle.id !== trackId));
|
|
6137
|
+
} catch (error) {
|
|
6138
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
6139
|
+
host.showToast("error", "Failed to delete track", msg);
|
|
6140
|
+
}
|
|
6141
|
+
},
|
|
6142
|
+
[host, activeSceneId]
|
|
6143
|
+
);
|
|
6144
|
+
const handlePromptChange = useCallback14(
|
|
6145
|
+
(trackId, prompt) => {
|
|
6146
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
|
|
6147
|
+
const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
|
|
6148
|
+
if (saveTimeoutRefs.current[trackId]) {
|
|
6149
|
+
clearTimeout(saveTimeoutRefs.current[trackId]);
|
|
6150
|
+
}
|
|
6151
|
+
saveTimeoutRefs.current[trackId] = setTimeout(() => {
|
|
6152
|
+
if (activeSceneId) {
|
|
6153
|
+
host.setSceneData(activeSceneId, trackDataKey(dbId, "prompt"), prompt).catch(() => {
|
|
6154
|
+
});
|
|
6155
|
+
}
|
|
6156
|
+
}, 500);
|
|
6157
|
+
},
|
|
6158
|
+
[host, activeSceneId]
|
|
6159
|
+
);
|
|
6160
|
+
const resolvedGenericGroups = useMemo8(() => {
|
|
6161
|
+
const out = {};
|
|
6162
|
+
for (const ext of adapter.groupExtensions ?? []) {
|
|
6163
|
+
out[ext.metaKey] = resolveTrackGroups(
|
|
6164
|
+
genericGroupMetas[ext.metaKey] ?? [],
|
|
6165
|
+
tracks,
|
|
6166
|
+
(t) => t.handle.dbId,
|
|
6167
|
+
{
|
|
6168
|
+
isComplete: ext.isComplete
|
|
6169
|
+
}
|
|
6170
|
+
);
|
|
6171
|
+
}
|
|
6172
|
+
return out;
|
|
6173
|
+
}, [adapter, genericGroupMetas, tracks]);
|
|
6174
|
+
const genericGroupMemberDbIds = useMemo8(() => {
|
|
6175
|
+
const s = /* @__PURE__ */ new Set();
|
|
6176
|
+
for (const r of Object.values(resolvedGenericGroups)) {
|
|
6177
|
+
for (const dbId of r.memberDbIds) s.add(dbId);
|
|
6178
|
+
}
|
|
6179
|
+
return s;
|
|
6180
|
+
}, [resolvedGenericGroups]);
|
|
6181
|
+
const engineToDbId = useCallback14(
|
|
6182
|
+
(trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
|
|
6183
|
+
[]
|
|
6184
|
+
);
|
|
6185
|
+
const updateTrack = useCallback14(
|
|
6186
|
+
(trackId, patch) => {
|
|
6187
|
+
setTracks(
|
|
6188
|
+
(prev) => prev.map(
|
|
6189
|
+
(t) => t.handle.id === trackId ? typeof patch === "function" ? patch(t) : { ...t, ...patch } : t
|
|
6190
|
+
)
|
|
6191
|
+
);
|
|
6192
|
+
},
|
|
6193
|
+
[]
|
|
6194
|
+
);
|
|
6195
|
+
const markEditLoaded = useCallback14((trackId) => {
|
|
6196
|
+
editLoadStartedRef.current.add(trackId);
|
|
6197
|
+
}, []);
|
|
6198
|
+
const tracksRef = useRef17(tracks);
|
|
6199
|
+
useEffect16(() => {
|
|
6200
|
+
tracksRef.current = tracks;
|
|
6201
|
+
}, [tracks]);
|
|
6202
|
+
const resolvedGenericGroupsRef = useRef17(resolvedGenericGroups);
|
|
6203
|
+
useEffect16(() => {
|
|
6204
|
+
resolvedGenericGroupsRef.current = resolvedGenericGroups;
|
|
6205
|
+
}, [resolvedGenericGroups]);
|
|
6206
|
+
const makeServices = useCallback14(() => {
|
|
6207
|
+
return {
|
|
6208
|
+
host,
|
|
6209
|
+
activeSceneId,
|
|
6210
|
+
tracks: tracksRef.current,
|
|
6211
|
+
updateTrack,
|
|
6212
|
+
setTracks,
|
|
6213
|
+
reloadTracks: loadTracks,
|
|
6214
|
+
soundHistory,
|
|
6215
|
+
engineToDbId,
|
|
6216
|
+
trackDataKey,
|
|
6217
|
+
markEditLoaded,
|
|
6218
|
+
createFamilyTrack: (nameSuffix = "") => host.createTrack({
|
|
6219
|
+
name: `${identity.trackNamePrefix}-${Date.now()}${nameSuffix}`,
|
|
6220
|
+
...adapter.createTrackOptions()
|
|
6221
|
+
}),
|
|
6222
|
+
resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
|
|
6223
|
+
};
|
|
6224
|
+
}, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
|
|
6225
|
+
const handleGenerate = useCallback14(
|
|
6226
|
+
async (trackId) => {
|
|
6227
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6228
|
+
if (!track || !track.prompt.trim()) return;
|
|
6229
|
+
if (!isAuthenticated) {
|
|
6230
|
+
host.showToast("warning", "Sign In Required", "Please sign in to generate MIDI");
|
|
6231
|
+
return;
|
|
6232
|
+
}
|
|
6233
|
+
setTracks(
|
|
6234
|
+
(prev) => prev.map(
|
|
6235
|
+
(t) => t.handle.id === trackId ? { ...t, isGenerating: true, error: null, generationProgress: 0 } : t
|
|
6236
|
+
)
|
|
6237
|
+
);
|
|
6238
|
+
try {
|
|
6239
|
+
await adapter.generation.generate(track, makeServices());
|
|
6240
|
+
} catch (error) {
|
|
6241
|
+
const msg = error instanceof Error ? error.message : "Generation failed";
|
|
6242
|
+
setTracks(
|
|
6243
|
+
(prev) => prev.map(
|
|
6244
|
+
(t) => t.handle.id === trackId ? { ...t, isGenerating: false, error: msg, generationProgress: 0 } : t
|
|
6245
|
+
)
|
|
6246
|
+
);
|
|
6247
|
+
host.showToast("error", "Generation failed", msg);
|
|
6248
|
+
}
|
|
6249
|
+
},
|
|
6250
|
+
[host, adapter, tracks, isAuthenticated, makeServices]
|
|
6251
|
+
);
|
|
6252
|
+
const handleMuteToggle = useCallback14(
|
|
6253
|
+
(trackId) => {
|
|
6254
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6255
|
+
if (!track) return;
|
|
6256
|
+
const newMuted = !track.runtimeState.muted;
|
|
6257
|
+
setTracks(
|
|
6258
|
+
(prev) => prev.map(
|
|
6259
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
|
|
6260
|
+
)
|
|
6261
|
+
);
|
|
6262
|
+
host.setTrackMute(trackId, newMuted).catch(() => {
|
|
6263
|
+
setTracks(
|
|
6264
|
+
(prev) => prev.map(
|
|
6265
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: !newMuted } } : t
|
|
6266
|
+
)
|
|
6267
|
+
);
|
|
6268
|
+
});
|
|
6269
|
+
},
|
|
6270
|
+
[host, tracks]
|
|
6271
|
+
);
|
|
6272
|
+
const handleSoloToggle = useCallback14(
|
|
6273
|
+
(trackId) => {
|
|
6274
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6275
|
+
if (!track) return;
|
|
6276
|
+
const newSolo = !track.runtimeState.solo;
|
|
6277
|
+
setTracks(
|
|
6278
|
+
(prev) => prev.map(
|
|
6279
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
|
|
6280
|
+
)
|
|
6281
|
+
);
|
|
6282
|
+
host.setTrackSolo(trackId, newSolo).catch(() => {
|
|
6283
|
+
setTracks(
|
|
6284
|
+
(prev) => prev.map(
|
|
6285
|
+
(t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: !newSolo } } : t
|
|
6286
|
+
)
|
|
6287
|
+
);
|
|
6288
|
+
});
|
|
6289
|
+
},
|
|
6290
|
+
[host, tracks]
|
|
6291
|
+
);
|
|
6292
|
+
const handleVolumeChange = useCallback14(
|
|
6293
|
+
(trackId, volume) => {
|
|
6294
|
+
setTracks(
|
|
6295
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
|
|
6296
|
+
);
|
|
6297
|
+
host.setTrackVolume(trackId, volume).catch(() => {
|
|
6298
|
+
});
|
|
6299
|
+
},
|
|
6300
|
+
[host]
|
|
6301
|
+
);
|
|
6302
|
+
const handlePanChange = useCallback14(
|
|
6303
|
+
(trackId, pan) => {
|
|
6304
|
+
setTracks(
|
|
6305
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
|
|
6306
|
+
);
|
|
6307
|
+
host.setTrackPan(trackId, pan).catch(() => {
|
|
6308
|
+
});
|
|
6309
|
+
},
|
|
6310
|
+
[host]
|
|
6311
|
+
);
|
|
6312
|
+
const handleShuffle = useCallback14(
|
|
6313
|
+
async (trackId) => {
|
|
6314
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6315
|
+
if (!track) return;
|
|
6316
|
+
if (soundHistory.list(trackId).entries.length === 0) {
|
|
6317
|
+
try {
|
|
6318
|
+
const cap = await adapter.sound.captureSoundDescriptor(trackId);
|
|
6319
|
+
if (cap) soundHistory.record(trackId, cap.descriptor, adapter.sound.previousSoundLabel);
|
|
6320
|
+
} catch {
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
try {
|
|
6324
|
+
let result;
|
|
6325
|
+
let nextHistory;
|
|
6326
|
+
try {
|
|
6327
|
+
result = await adapter.shuffle.shuffle(track, Array.from(track.shuffleHistory));
|
|
6328
|
+
nextHistory = new Set(track.shuffleHistory);
|
|
6329
|
+
} catch (firstErr) {
|
|
6330
|
+
if (adapter.shuffle.isExhaustedError(firstErr)) {
|
|
6331
|
+
nextHistory = /* @__PURE__ */ new Set();
|
|
6332
|
+
result = await adapter.shuffle.shuffle(track, []);
|
|
6333
|
+
} else {
|
|
6334
|
+
throw firstErr;
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
nextHistory.add(result.appliedName);
|
|
6338
|
+
setTracks(
|
|
6339
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, shuffleHistory: nextHistory } : t)
|
|
6340
|
+
);
|
|
6341
|
+
try {
|
|
6342
|
+
const cap = await adapter.sound.captureSoundDescriptor(trackId);
|
|
6343
|
+
if (cap) soundHistory.record(trackId, cap.descriptor, result.appliedName);
|
|
6344
|
+
} catch {
|
|
6345
|
+
}
|
|
6346
|
+
console.log(`[${logTag}] Sound shuffled: ${result.appliedName} (history ${nextHistory.size})`);
|
|
6347
|
+
} catch (error) {
|
|
6348
|
+
const msg = error instanceof Error ? error.message : "Shuffle failed";
|
|
6349
|
+
host.showToast("error", "Shuffle failed", msg);
|
|
6350
|
+
}
|
|
6351
|
+
},
|
|
6352
|
+
[host, adapter, tracks, soundHistory, logTag]
|
|
6353
|
+
);
|
|
6354
|
+
const handleCopy = useCallback14(
|
|
6355
|
+
async (trackId) => {
|
|
6356
|
+
try {
|
|
6357
|
+
const newHandle = await host.duplicateTrack(trackId);
|
|
6358
|
+
await loadTracks();
|
|
6359
|
+
host.showToast("success", "Track duplicated", newHandle.name);
|
|
6360
|
+
} catch (error) {
|
|
6361
|
+
const msg = error instanceof Error ? error.message : "Copy failed";
|
|
6362
|
+
host.showToast("error", "Copy failed", msg);
|
|
6363
|
+
}
|
|
6364
|
+
},
|
|
6365
|
+
[host, loadTracks]
|
|
6366
|
+
);
|
|
6367
|
+
const handleFxToggle = useCallback14(
|
|
6368
|
+
(trackId, category, enabled) => {
|
|
6369
|
+
setTracks(
|
|
6370
|
+
(prev) => prev.map(
|
|
6371
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], enabled } } } : t
|
|
6372
|
+
)
|
|
6373
|
+
);
|
|
6374
|
+
host.toggleTrackFx(trackId, category, enabled).catch(() => {
|
|
6375
|
+
setTracks(
|
|
6376
|
+
(prev) => prev.map(
|
|
6377
|
+
(t) => t.handle.id === trackId ? {
|
|
6378
|
+
...t,
|
|
6379
|
+
fxDetailState: {
|
|
6380
|
+
...t.fxDetailState,
|
|
6381
|
+
[category]: { ...t.fxDetailState[category], enabled: !enabled }
|
|
6382
|
+
}
|
|
6383
|
+
} : t
|
|
6384
|
+
)
|
|
6385
|
+
);
|
|
6386
|
+
});
|
|
6387
|
+
},
|
|
6388
|
+
[host]
|
|
6389
|
+
);
|
|
6390
|
+
const handleFxPresetChange = useCallback14(
|
|
6391
|
+
(trackId, category, presetIndex) => {
|
|
6392
|
+
setTracks(
|
|
6393
|
+
(prev) => prev.map(
|
|
6394
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], presetIndex } } } : t
|
|
6395
|
+
)
|
|
6396
|
+
);
|
|
6397
|
+
host.setTrackFxPreset(trackId, category, presetIndex).then((result) => {
|
|
6398
|
+
if (result.dryWet !== void 0) {
|
|
6399
|
+
setTracks(
|
|
6400
|
+
(prev) => prev.map(
|
|
6401
|
+
(t) => t.handle.id === trackId ? {
|
|
6402
|
+
...t,
|
|
6403
|
+
fxDetailState: {
|
|
6404
|
+
...t.fxDetailState,
|
|
6405
|
+
[category]: { ...t.fxDetailState[category], dryWet: result.dryWet }
|
|
6406
|
+
}
|
|
6407
|
+
} : t
|
|
6408
|
+
)
|
|
6409
|
+
);
|
|
6410
|
+
}
|
|
6411
|
+
}).catch(() => {
|
|
6412
|
+
});
|
|
6413
|
+
},
|
|
6414
|
+
[host]
|
|
6415
|
+
);
|
|
6416
|
+
const handleFxDryWetChange = useCallback14(
|
|
6417
|
+
(trackId, category, value) => {
|
|
6418
|
+
setTracks(
|
|
6419
|
+
(prev) => prev.map(
|
|
6420
|
+
(t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], dryWet: value } } } : t
|
|
6421
|
+
)
|
|
6422
|
+
);
|
|
6423
|
+
host.setTrackFxDryWet(trackId, category, value).catch(() => {
|
|
6424
|
+
});
|
|
6425
|
+
},
|
|
6426
|
+
[host]
|
|
6427
|
+
);
|
|
6428
|
+
const toggleFxDrawer = useCallback14(
|
|
6429
|
+
(trackId) => {
|
|
6430
|
+
setTracks(
|
|
6431
|
+
(prev) => prev.map((t) => {
|
|
6432
|
+
if (t.handle.id !== trackId) return t;
|
|
6433
|
+
const onFx = t.drawerOpen && t.drawerTab === "fx";
|
|
6434
|
+
return { ...t, drawerOpen: !onFx, drawerTab: "fx", editorStage: false };
|
|
6435
|
+
})
|
|
6436
|
+
);
|
|
6437
|
+
const track = tracks.find((t) => t.handle.id === trackId);
|
|
6438
|
+
const wasOnFx = !!track && track.drawerOpen && track.drawerTab === "fx";
|
|
6439
|
+
if (track && !wasOnFx) {
|
|
6440
|
+
host.getTrackFxState(trackId).then((fxState) => {
|
|
6441
|
+
setTracks(
|
|
6442
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
|
|
6443
|
+
);
|
|
6444
|
+
}).catch(() => {
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
},
|
|
6448
|
+
[host, tracks]
|
|
6449
|
+
);
|
|
6450
|
+
const loadEditNotes = useCallback14(
|
|
6451
|
+
async (trackId) => {
|
|
6452
|
+
try {
|
|
6453
|
+
const mc = await host.getMusicalContext();
|
|
6454
|
+
let notes = [];
|
|
6455
|
+
if (typeof host.readMidiNotes === "function") {
|
|
6456
|
+
const result = await host.readMidiNotes(trackId);
|
|
6457
|
+
notes = result.clips[0]?.notes ?? [];
|
|
6458
|
+
}
|
|
6459
|
+
setTracks(
|
|
6460
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes, editBars: mc.bars, editBpm: mc.bpm } : t)
|
|
6461
|
+
);
|
|
6462
|
+
} catch (err) {
|
|
6463
|
+
console.warn(`[${logTag}] Failed to load MIDI for editing:`, err);
|
|
6464
|
+
}
|
|
6465
|
+
},
|
|
6466
|
+
[host, logTag]
|
|
6467
|
+
);
|
|
6468
|
+
const handleNotesChange = useCallback14(
|
|
6469
|
+
(trackId, notes) => {
|
|
6470
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
|
|
6471
|
+
const key = `edit:${trackId}`;
|
|
6472
|
+
if (saveTimeoutRefs.current[key]) {
|
|
6473
|
+
clearTimeout(saveTimeoutRefs.current[key]);
|
|
6474
|
+
}
|
|
6475
|
+
saveTimeoutRefs.current[key] = setTimeout(() => {
|
|
6476
|
+
void (async () => {
|
|
6477
|
+
try {
|
|
6478
|
+
if (notes.length === 0) {
|
|
6479
|
+
await host.clearMidi(trackId);
|
|
6480
|
+
} else {
|
|
6481
|
+
const mc = await host.getMusicalContext();
|
|
6482
|
+
await host.writeMidiClip(trackId, {
|
|
6483
|
+
startTime: 0,
|
|
6484
|
+
endTime: mc.bars * 4 * 60 / mc.bpm,
|
|
6485
|
+
tempo: mc.bpm,
|
|
6486
|
+
notes
|
|
6487
|
+
});
|
|
6488
|
+
}
|
|
6489
|
+
} catch (err) {
|
|
6490
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6491
|
+
host.showToast("error", "Failed to save edit", msg);
|
|
6492
|
+
}
|
|
6493
|
+
})();
|
|
6494
|
+
}, 300);
|
|
6495
|
+
},
|
|
6496
|
+
[host]
|
|
6497
|
+
);
|
|
6498
|
+
const handleTabChange = useCallback14(
|
|
6499
|
+
(trackId, tab) => {
|
|
6500
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
|
|
6501
|
+
if (tab === "fx") {
|
|
6502
|
+
host.getTrackFxState(trackId).then((fxState) => {
|
|
6503
|
+
setTracks(
|
|
6504
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
|
|
6505
|
+
);
|
|
6506
|
+
}).catch(() => {
|
|
6507
|
+
});
|
|
6508
|
+
} else if (tab === "pick" && availableInstruments.length === 0 && !instrumentsLoading) {
|
|
6509
|
+
setInstrumentsLoading(true);
|
|
6510
|
+
host.getAvailableInstruments().then((instruments) => {
|
|
6511
|
+
setAvailableInstruments(instruments);
|
|
6512
|
+
}).catch(() => {
|
|
6513
|
+
}).finally(() => {
|
|
6514
|
+
setInstrumentsLoading(false);
|
|
6515
|
+
});
|
|
6516
|
+
} else if (tab === "edit" && !editLoadStartedRef.current.has(trackId)) {
|
|
6517
|
+
editLoadStartedRef.current.add(trackId);
|
|
6518
|
+
void loadEditNotes(trackId);
|
|
6519
|
+
}
|
|
6520
|
+
},
|
|
6521
|
+
[host, availableInstruments.length, instrumentsLoading, loadEditNotes]
|
|
6522
|
+
);
|
|
6523
|
+
const handleProgressChange = useCallback14((trackId, pct) => {
|
|
6524
|
+
setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
|
|
6525
|
+
}, []);
|
|
6526
|
+
const handleToggleDrawer = useCallback14((trackId) => {
|
|
6527
|
+
setTracks(
|
|
6528
|
+
(prev) => prev.map((t) => {
|
|
6529
|
+
if (t.handle.id !== trackId) return t;
|
|
6530
|
+
const onSound = t.drawerOpen && t.drawerTab !== "fx";
|
|
6531
|
+
return { ...t, drawerOpen: !onSound, drawerTab: "history", editorStage: false };
|
|
6532
|
+
})
|
|
6533
|
+
);
|
|
6534
|
+
}, []);
|
|
6535
|
+
const handleInstrumentSelect = useCallback14(
|
|
6536
|
+
async (trackId, pluginId) => {
|
|
6537
|
+
const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
|
|
6538
|
+
if (isDefaultInstrument) {
|
|
6539
|
+
setTracks(
|
|
6540
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: false, editorStage: false } : t)
|
|
6541
|
+
);
|
|
6542
|
+
try {
|
|
6543
|
+
await host.setTrackInstrument(trackId, pluginId);
|
|
6544
|
+
const descriptor = await host.getTrackInstrument(trackId);
|
|
6545
|
+
setTracks(
|
|
6546
|
+
(prev) => prev.map(
|
|
6547
|
+
(t) => t.handle.id === trackId ? {
|
|
6548
|
+
...t,
|
|
6549
|
+
instrumentPluginId: descriptor?.pluginId ?? null,
|
|
6550
|
+
instrumentName: descriptor?.name ?? null,
|
|
6551
|
+
instrumentMissing: descriptor?.missing ?? false
|
|
6552
|
+
} : t
|
|
6553
|
+
)
|
|
6554
|
+
);
|
|
6555
|
+
} catch (err) {
|
|
6556
|
+
const msg = err instanceof Error ? err.message : "Failed to load instrument";
|
|
6557
|
+
host.showToast("error", "Instrument load failed", msg);
|
|
6558
|
+
}
|
|
6559
|
+
return;
|
|
6560
|
+
}
|
|
6561
|
+
setTracks(
|
|
6562
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerTab: "pick", editorStage: true } : t)
|
|
6563
|
+
);
|
|
6564
|
+
try {
|
|
6565
|
+
await host.setTrackInstrument(trackId, pluginId);
|
|
6566
|
+
const descriptor = await host.getTrackInstrument(trackId);
|
|
6567
|
+
setTracks(
|
|
6568
|
+
(prev) => prev.map(
|
|
6569
|
+
(t) => t.handle.id === trackId ? {
|
|
6570
|
+
...t,
|
|
6571
|
+
instrumentPluginId: descriptor?.pluginId ?? null,
|
|
6572
|
+
instrumentName: descriptor?.name ?? null,
|
|
6573
|
+
instrumentMissing: descriptor?.missing ?? false
|
|
6574
|
+
} : t
|
|
6575
|
+
)
|
|
6576
|
+
);
|
|
6577
|
+
} catch (err) {
|
|
6578
|
+
const msg = err instanceof Error ? err.message : "Failed to load instrument";
|
|
6579
|
+
console.error(`[${logTag}] Failed to set instrument:`, err);
|
|
6580
|
+
host.showToast("error", "Instrument load failed", msg);
|
|
6581
|
+
setTracks(
|
|
6582
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
|
|
6583
|
+
);
|
|
6584
|
+
}
|
|
6585
|
+
},
|
|
6586
|
+
[host, identity.defaultInstrumentPluginId, logTag]
|
|
6587
|
+
);
|
|
6588
|
+
const handleShowEditor = useCallback14(
|
|
6589
|
+
async (trackId) => {
|
|
6590
|
+
try {
|
|
6591
|
+
await host.showInstrumentEditor(trackId);
|
|
6592
|
+
} catch (err) {
|
|
6593
|
+
const msg = err instanceof Error ? err.message : "Failed to open editor";
|
|
6594
|
+
host.showToast("error", "Editor failed", msg);
|
|
6595
|
+
}
|
|
6596
|
+
},
|
|
6597
|
+
[host]
|
|
6598
|
+
);
|
|
6599
|
+
const handleBackToInstruments = useCallback14((trackId) => {
|
|
6600
|
+
setTracks(
|
|
6601
|
+
(prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
|
|
6602
|
+
);
|
|
6603
|
+
}, []);
|
|
6604
|
+
const handleRefreshInstruments = useCallback14(() => {
|
|
6605
|
+
setInstrumentsLoading(true);
|
|
6606
|
+
host.getAvailableInstruments().then((instruments) => {
|
|
6607
|
+
setAvailableInstruments(instruments);
|
|
6608
|
+
}).catch(() => {
|
|
6609
|
+
}).finally(() => {
|
|
6610
|
+
setInstrumentsLoading(false);
|
|
6611
|
+
});
|
|
6612
|
+
}, [host]);
|
|
6613
|
+
const onAuditionNote = useCallback14(
|
|
6614
|
+
(trackId, pitch, velocity, ms) => {
|
|
6615
|
+
void host.auditionNote(trackId, pitch, velocity, ms);
|
|
6616
|
+
},
|
|
6617
|
+
[host]
|
|
6618
|
+
);
|
|
6619
|
+
const { resolvedCrossfadePairs, crossfadeMemberDbIds } = useMemo8(() => {
|
|
6620
|
+
const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
|
|
6621
|
+
const pairs = [];
|
|
6622
|
+
const members = /* @__PURE__ */ new Set();
|
|
6623
|
+
for (const p of crossfadePairsMeta) {
|
|
6624
|
+
const origin = byDbId.get(p.originDbId);
|
|
6625
|
+
const target = byDbId.get(p.targetDbId);
|
|
6626
|
+
if (origin && target) {
|
|
6627
|
+
pairs.push({ ...p, origin, target });
|
|
6628
|
+
members.add(p.originDbId);
|
|
6629
|
+
members.add(p.targetDbId);
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
|
|
6633
|
+
}, [tracks, crossfadePairsMeta]);
|
|
6634
|
+
const { resolvedFades, fadeMemberDbIds } = useMemo8(() => {
|
|
6635
|
+
const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
|
|
6636
|
+
const list = [];
|
|
6637
|
+
const members = /* @__PURE__ */ new Set();
|
|
6638
|
+
for (const f of fadesMeta) {
|
|
6639
|
+
const track = byDbId.get(f.dbId);
|
|
6640
|
+
if (track) {
|
|
6641
|
+
list.push({ ...f, track });
|
|
6642
|
+
members.add(f.dbId);
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
return { resolvedFades: list, fadeMemberDbIds: members };
|
|
6646
|
+
}, [tracks, fadesMeta]);
|
|
6647
|
+
const transition = useTransitionOps({
|
|
6648
|
+
host,
|
|
6649
|
+
adapter,
|
|
6650
|
+
activeSceneId,
|
|
6651
|
+
isConnected,
|
|
6652
|
+
isAuthenticated,
|
|
6653
|
+
sceneContext,
|
|
6654
|
+
tracks,
|
|
6655
|
+
setTracks,
|
|
6656
|
+
loadTracks,
|
|
6657
|
+
setCrossfadePairsMeta,
|
|
6658
|
+
setFadesMeta,
|
|
6659
|
+
resolvedCrossfadePairs,
|
|
6660
|
+
resolvedFades
|
|
6661
|
+
});
|
|
6662
|
+
const setGroupMute = useCallback14(
|
|
6663
|
+
(trackIds, muted) => {
|
|
6664
|
+
for (const id of trackIds) {
|
|
6665
|
+
setTracks(
|
|
6666
|
+
(prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted } } : t)
|
|
6667
|
+
);
|
|
6668
|
+
host.setTrackMute(id, muted).catch(() => {
|
|
6669
|
+
});
|
|
6670
|
+
}
|
|
6671
|
+
},
|
|
6672
|
+
[host]
|
|
6673
|
+
);
|
|
6674
|
+
const setGroupSolo = useCallback14(
|
|
6675
|
+
(trackIds, solo) => {
|
|
6676
|
+
for (const id of trackIds) {
|
|
6677
|
+
setTracks(
|
|
6678
|
+
(prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo } } : t)
|
|
6679
|
+
);
|
|
6680
|
+
host.setTrackSolo(id, solo).catch(() => {
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6683
|
+
},
|
|
6684
|
+
[host]
|
|
6685
|
+
);
|
|
6686
|
+
const deleteGroup = useCallback14(
|
|
6687
|
+
async (members, cleanupKeySuffixes) => {
|
|
6688
|
+
for (const member of members) {
|
|
6689
|
+
try {
|
|
6690
|
+
await host.deleteTrack(member.engineId);
|
|
6691
|
+
} catch {
|
|
6692
|
+
}
|
|
6693
|
+
if (activeSceneId) {
|
|
6694
|
+
for (const suffix of cleanupKeySuffixes) {
|
|
6695
|
+
await host.deleteSceneData(activeSceneId, trackDataKey(member.dbId, suffix)).catch(() => {
|
|
6696
|
+
});
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
}
|
|
6700
|
+
const gone = new Set(members.map((m) => m.engineId));
|
|
6701
|
+
setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
|
|
6702
|
+
await loadTracks(true);
|
|
6703
|
+
},
|
|
6704
|
+
[host, activeSceneId, loadTracks]
|
|
6705
|
+
);
|
|
6706
|
+
const handlers = useMemo8(
|
|
6707
|
+
() => ({
|
|
6708
|
+
promptChange: handlePromptChange,
|
|
6709
|
+
generate: (trackId) => {
|
|
6710
|
+
void handleGenerate(trackId);
|
|
6711
|
+
},
|
|
6712
|
+
shuffle: (trackId) => {
|
|
6713
|
+
void handleShuffle(trackId);
|
|
6714
|
+
},
|
|
6715
|
+
copy: (trackId) => {
|
|
6716
|
+
void handleCopy(trackId);
|
|
6717
|
+
},
|
|
6718
|
+
delete: (trackId) => {
|
|
6719
|
+
void handleDeleteTrack(trackId);
|
|
6720
|
+
},
|
|
6721
|
+
muteToggle: handleMuteToggle,
|
|
6722
|
+
soloToggle: handleSoloToggle,
|
|
6723
|
+
volumeChange: handleVolumeChange,
|
|
6724
|
+
panChange: handlePanChange,
|
|
6725
|
+
tabChange: handleTabChange,
|
|
6726
|
+
toggleDrawer: handleToggleDrawer,
|
|
6727
|
+
toggleFxDrawer,
|
|
6728
|
+
notesChange: handleNotesChange,
|
|
6729
|
+
progressChange: handleProgressChange
|
|
6730
|
+
}),
|
|
6731
|
+
[
|
|
6732
|
+
handlePromptChange,
|
|
6733
|
+
handleGenerate,
|
|
6734
|
+
handleShuffle,
|
|
6735
|
+
handleCopy,
|
|
6736
|
+
handleDeleteTrack,
|
|
6737
|
+
handleMuteToggle,
|
|
6738
|
+
handleSoloToggle,
|
|
6739
|
+
handleVolumeChange,
|
|
6740
|
+
handlePanChange,
|
|
6741
|
+
handleTabChange,
|
|
6742
|
+
handleToggleDrawer,
|
|
6743
|
+
toggleFxDrawer,
|
|
6744
|
+
handleNotesChange,
|
|
6745
|
+
handleProgressChange
|
|
6746
|
+
]
|
|
6747
|
+
);
|
|
6748
|
+
return {
|
|
6749
|
+
ui,
|
|
6750
|
+
adapter,
|
|
6751
|
+
tracks,
|
|
6752
|
+
setTracks,
|
|
6753
|
+
isLoadingTracks,
|
|
6754
|
+
loadTracks,
|
|
6755
|
+
engineToDbId,
|
|
6756
|
+
supportsMeters,
|
|
6757
|
+
trackLevels,
|
|
6758
|
+
anySolo,
|
|
6759
|
+
reorder,
|
|
6760
|
+
soundHistory,
|
|
6761
|
+
isComposing,
|
|
6762
|
+
placeholders,
|
|
6763
|
+
isAddingTrack,
|
|
6764
|
+
isExportingMidi,
|
|
6765
|
+
designerView,
|
|
6766
|
+
canCrossfade,
|
|
6767
|
+
needsContract,
|
|
6768
|
+
xfFromId,
|
|
6769
|
+
xfToId,
|
|
6770
|
+
importOpen,
|
|
6771
|
+
setImportOpen,
|
|
6772
|
+
soundImportTarget,
|
|
6773
|
+
setSoundImportTarget,
|
|
6774
|
+
handleSoundImportPick,
|
|
6775
|
+
handlePortTrack,
|
|
6776
|
+
transition,
|
|
6777
|
+
crossfadePairsMeta,
|
|
6778
|
+
fadesMeta,
|
|
6779
|
+
resolvedCrossfadePairs,
|
|
6780
|
+
crossfadeMemberDbIds,
|
|
6781
|
+
resolvedFades,
|
|
6782
|
+
fadeMemberDbIds,
|
|
6783
|
+
resolvedGenericGroups,
|
|
6784
|
+
genericGroupMemberDbIds,
|
|
6785
|
+
availableInstruments,
|
|
6786
|
+
instrumentsLoading,
|
|
6787
|
+
handlers,
|
|
6788
|
+
handleGenerate,
|
|
6789
|
+
handleShuffle,
|
|
6790
|
+
handleAddTrack,
|
|
6791
|
+
handleDeleteTrack,
|
|
6792
|
+
handleExportMidi,
|
|
6793
|
+
handlePromptChange,
|
|
6794
|
+
handleMuteToggle,
|
|
6795
|
+
handleSoloToggle,
|
|
6796
|
+
handleVolumeChange,
|
|
6797
|
+
handlePanChange,
|
|
6798
|
+
handleTabChange,
|
|
6799
|
+
handleToggleDrawer,
|
|
6800
|
+
toggleFxDrawer,
|
|
6801
|
+
handleNotesChange,
|
|
6802
|
+
handleProgressChange,
|
|
6803
|
+
handleCopy,
|
|
6804
|
+
handleFxToggle,
|
|
6805
|
+
handleFxPresetChange,
|
|
6806
|
+
handleFxDryWetChange,
|
|
6807
|
+
handleInstrumentSelect,
|
|
6808
|
+
handleShowEditor,
|
|
6809
|
+
handleBackToInstruments,
|
|
6810
|
+
handleRefreshInstruments,
|
|
6811
|
+
onAuditionNote,
|
|
6812
|
+
makeServices,
|
|
6813
|
+
setGroupMute,
|
|
6814
|
+
setGroupSolo,
|
|
6815
|
+
deleteGroup
|
|
6816
|
+
};
|
|
6817
|
+
}
|
|
6818
|
+
|
|
6819
|
+
// src/panel-core/GeneratorPanelShell.tsx
|
|
6820
|
+
import React20, { useCallback as useCallback15 } from "react";
|
|
6821
|
+
import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
6822
|
+
function GeneratorPanelShell({ core, slots }) {
|
|
6823
|
+
const {
|
|
6824
|
+
ui,
|
|
6825
|
+
adapter,
|
|
6826
|
+
tracks,
|
|
6827
|
+
isLoadingTracks,
|
|
6828
|
+
supportsMeters,
|
|
6829
|
+
trackLevels,
|
|
6830
|
+
anySolo,
|
|
6831
|
+
reorder,
|
|
6832
|
+
soundHistory,
|
|
6833
|
+
isComposing,
|
|
6834
|
+
placeholders,
|
|
6835
|
+
designerView,
|
|
6836
|
+
canCrossfade,
|
|
6837
|
+
xfFromId,
|
|
6838
|
+
xfToId,
|
|
6839
|
+
importOpen,
|
|
6840
|
+
setImportOpen,
|
|
6841
|
+
soundImportTarget,
|
|
6842
|
+
setSoundImportTarget,
|
|
6843
|
+
handleSoundImportPick,
|
|
6844
|
+
handlePortTrack,
|
|
6845
|
+
transition,
|
|
6846
|
+
crossfadePairsMeta,
|
|
6847
|
+
fadesMeta,
|
|
6848
|
+
resolvedCrossfadePairs,
|
|
6849
|
+
crossfadeMemberDbIds,
|
|
6850
|
+
resolvedFades,
|
|
6851
|
+
fadeMemberDbIds,
|
|
6852
|
+
resolvedGenericGroups,
|
|
6853
|
+
genericGroupMemberDbIds,
|
|
6854
|
+
availableInstruments,
|
|
6855
|
+
instrumentsLoading,
|
|
6856
|
+
handlers,
|
|
6857
|
+
isExportingMidi,
|
|
6858
|
+
handleExportMidi,
|
|
6859
|
+
handleFxToggle,
|
|
6860
|
+
handleFxPresetChange,
|
|
6861
|
+
handleFxDryWetChange,
|
|
6862
|
+
handleInstrumentSelect,
|
|
6863
|
+
handleShowEditor,
|
|
6864
|
+
handleBackToInstruments,
|
|
6865
|
+
handleRefreshInstruments,
|
|
6866
|
+
onAuditionNote,
|
|
6867
|
+
loadTracks,
|
|
6868
|
+
makeServices,
|
|
6869
|
+
setGroupMute,
|
|
6870
|
+
setGroupSolo,
|
|
6871
|
+
deleteGroup
|
|
6872
|
+
} = core;
|
|
6873
|
+
const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
|
|
6874
|
+
const { identity, features } = adapter;
|
|
6875
|
+
const buildRowProps = useCallback15(
|
|
6876
|
+
(track, drag) => {
|
|
6877
|
+
const id = track.handle.id;
|
|
6878
|
+
const pickerProps = features.instrumentPicker ? {
|
|
6879
|
+
instrumentName: track.instrumentName,
|
|
6880
|
+
instrumentMissing: track.instrumentMissing,
|
|
6881
|
+
onToggleDrawer: () => handlers.toggleDrawer(id),
|
|
6882
|
+
availableInstruments,
|
|
6883
|
+
currentInstrumentPluginId: track.instrumentPluginId,
|
|
6884
|
+
onInstrumentSelect: (pluginId) => handleInstrumentSelect(id, pluginId),
|
|
6885
|
+
instrumentsLoading,
|
|
6886
|
+
onRefreshInstruments: handleRefreshInstruments,
|
|
6887
|
+
editorStage: track.editorStage,
|
|
6888
|
+
onShowEditor: () => handleShowEditor(id),
|
|
6889
|
+
onBackToInstruments: () => handleBackToInstruments(id)
|
|
6890
|
+
} : {};
|
|
6891
|
+
const importSoundProps = features.importTracks ? {
|
|
6892
|
+
onImportSound: () => setSoundImportTarget(track),
|
|
6893
|
+
importSoundLabel: adapter.sound.importSoundLabel
|
|
6894
|
+
} : {};
|
|
6895
|
+
const props = {
|
|
6896
|
+
...drag ? { drag } : {},
|
|
6897
|
+
track: { id, name: track.handle.name, role: track.role },
|
|
6898
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
6899
|
+
prompt: track.prompt,
|
|
6900
|
+
runtimeState: {
|
|
6901
|
+
muted: track.runtimeState.muted,
|
|
6902
|
+
solo: track.runtimeState.solo,
|
|
6903
|
+
volume: track.runtimeState.volume,
|
|
6904
|
+
pan: track.runtimeState.pan
|
|
6905
|
+
},
|
|
6906
|
+
soloedOut: anySolo && !track.runtimeState.solo,
|
|
6907
|
+
fxDetailState: track.fxDetailState,
|
|
6908
|
+
drawerOpen: track.drawerOpen,
|
|
6909
|
+
drawerTab: track.drawerTab,
|
|
6910
|
+
onTabChange: (tab) => handlers.tabChange(id, tab),
|
|
6911
|
+
isGenerating: track.isGenerating,
|
|
6912
|
+
isAuthenticated,
|
|
6913
|
+
error: track.error,
|
|
6914
|
+
hasMidi: track.hasMidi,
|
|
6915
|
+
generationProgress: track.generationProgress,
|
|
6916
|
+
estimatedGenerationMs: identity.estimatedGenerationMs,
|
|
6917
|
+
onPromptChange: (prompt) => handlers.promptChange(id, prompt),
|
|
6918
|
+
onGenerate: () => handlers.generate(id),
|
|
6919
|
+
onShuffle: () => handlers.shuffle(id),
|
|
6920
|
+
onCopy: () => handlers.copy(id),
|
|
6921
|
+
onDelete: () => handlers.delete(id),
|
|
6922
|
+
onMuteToggle: () => handlers.muteToggle(id),
|
|
6923
|
+
onSoloToggle: () => handlers.soloToggle(id),
|
|
6924
|
+
onVolumeChange: (vol) => handlers.volumeChange(id, vol),
|
|
6925
|
+
onPanChange: (pan) => handlers.panChange(id, pan),
|
|
6926
|
+
onFxToggle: (cat, enabled) => handleFxToggle(id, cat, enabled),
|
|
6927
|
+
onFxPresetChange: (cat, idx) => handleFxPresetChange(id, cat, idx),
|
|
6928
|
+
onFxDryWetChange: (cat, val) => handleFxDryWetChange(id, cat, val),
|
|
6929
|
+
onToggleFxDrawer: () => handlers.toggleFxDrawer(id),
|
|
6930
|
+
onProgressChange: (pct) => handlers.progressChange(id, pct),
|
|
6931
|
+
accentColor: identity.accentColor,
|
|
6932
|
+
...pickerProps,
|
|
6933
|
+
soundHistory: soundHistory.list(id).entries,
|
|
6934
|
+
soundHistoryCursor: soundHistory.list(id).cursor,
|
|
6935
|
+
onRestoreSound: (i) => {
|
|
6936
|
+
void soundHistory.restoreTo(id, i);
|
|
6937
|
+
},
|
|
6938
|
+
onToggleFavorite: (i) => soundHistory.toggleFavorite(id, i),
|
|
6939
|
+
...importSoundProps,
|
|
6940
|
+
editNotes: track.editNotes,
|
|
6941
|
+
onNotesChange: (notes) => handlers.notesChange(id, notes),
|
|
6942
|
+
editBars: track.editBars,
|
|
6943
|
+
editBpm: track.editBpm,
|
|
6944
|
+
editSnap: 0.25,
|
|
6945
|
+
onAuditionNote: (pitch, vel, ms) => onAuditionNote(id, pitch, vel, ms)
|
|
6946
|
+
};
|
|
6947
|
+
return adapter.mapTrackRowProps ? adapter.mapTrackRowProps(track, props) : props;
|
|
6948
|
+
},
|
|
6949
|
+
[
|
|
6950
|
+
features.instrumentPicker,
|
|
6951
|
+
features.importTracks,
|
|
6952
|
+
adapter,
|
|
6953
|
+
supportsMeters,
|
|
6954
|
+
trackLevels,
|
|
6955
|
+
anySolo,
|
|
6956
|
+
isAuthenticated,
|
|
6957
|
+
identity,
|
|
6958
|
+
handlers,
|
|
6959
|
+
availableInstruments,
|
|
6960
|
+
instrumentsLoading,
|
|
6961
|
+
handleInstrumentSelect,
|
|
6962
|
+
handleRefreshInstruments,
|
|
6963
|
+
handleShowEditor,
|
|
6964
|
+
handleBackToInstruments,
|
|
6965
|
+
setSoundImportTarget,
|
|
6966
|
+
soundHistory,
|
|
6967
|
+
handleFxToggle,
|
|
6968
|
+
handleFxPresetChange,
|
|
6969
|
+
handleFxDryWetChange,
|
|
6970
|
+
onAuditionNote
|
|
6971
|
+
]
|
|
6972
|
+
);
|
|
6973
|
+
if (!activeSceneId) {
|
|
6974
|
+
return /* @__PURE__ */ jsx24(
|
|
6975
|
+
"div",
|
|
6976
|
+
{
|
|
6977
|
+
"data-testid": `no-scene-placeholder-${identity.familyKey}`,
|
|
6978
|
+
className: "flex items-center justify-center py-8",
|
|
6979
|
+
children: /* @__PURE__ */ jsx24(
|
|
6980
|
+
"button",
|
|
6981
|
+
{
|
|
6982
|
+
onClick: () => onSelectScene?.(),
|
|
6983
|
+
className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
|
|
6984
|
+
children: "Select a Scene"
|
|
6985
|
+
}
|
|
6986
|
+
)
|
|
6987
|
+
}
|
|
6988
|
+
);
|
|
6989
|
+
}
|
|
6990
|
+
if (!sceneContext?.hasContract) {
|
|
6991
|
+
return /* @__PURE__ */ jsx24(
|
|
6992
|
+
"div",
|
|
6993
|
+
{
|
|
6994
|
+
"data-testid": `no-contract-placeholder-${identity.familyKey}`,
|
|
6995
|
+
className: "flex items-center justify-center py-8",
|
|
6996
|
+
children: /* @__PURE__ */ jsx24(
|
|
6997
|
+
"button",
|
|
6998
|
+
{
|
|
6999
|
+
onClick: () => onOpenContract?.(),
|
|
7000
|
+
className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
|
|
7001
|
+
children: "Generate a Contract"
|
|
7002
|
+
}
|
|
7003
|
+
)
|
|
7004
|
+
}
|
|
7005
|
+
);
|
|
7006
|
+
}
|
|
7007
|
+
if (features.bulkComposePlaceholders && isComposing) {
|
|
7008
|
+
return /* @__PURE__ */ jsx24("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx24(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
|
|
7009
|
+
}
|
|
7010
|
+
const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
|
|
7011
|
+
if (activePlaceholders.length > 0) {
|
|
7012
|
+
const tracksByDbId = /* @__PURE__ */ new Map();
|
|
7013
|
+
for (const t of tracks) {
|
|
7014
|
+
tracksByDbId.set(t.handle.dbId, t);
|
|
7015
|
+
if (t.handle.id !== t.handle.dbId) {
|
|
7016
|
+
tracksByDbId.set(t.handle.id, t);
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
return /* @__PURE__ */ jsx24("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
|
|
7020
|
+
const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
|
|
7021
|
+
if (loadedTrack) {
|
|
7022
|
+
return /* @__PURE__ */ jsx24(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
|
|
7023
|
+
}
|
|
7024
|
+
return /* @__PURE__ */ jsx24(
|
|
7025
|
+
"div",
|
|
7026
|
+
{
|
|
7027
|
+
"data-testid": "bulk-placeholder-track",
|
|
7028
|
+
className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
|
|
7029
|
+
style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
|
|
7030
|
+
children: /* @__PURE__ */ jsx24(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
|
|
7031
|
+
},
|
|
7032
|
+
ph.id
|
|
7033
|
+
);
|
|
7034
|
+
}) });
|
|
7035
|
+
}
|
|
7036
|
+
const groupCtx = {
|
|
7037
|
+
services: makeServices(),
|
|
7038
|
+
anySolo,
|
|
7039
|
+
supportsMeters,
|
|
7040
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7041
|
+
handlers,
|
|
7042
|
+
renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx24(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
|
|
7043
|
+
setGroupMute,
|
|
7044
|
+
setGroupSolo,
|
|
7045
|
+
deleteGroup
|
|
7046
|
+
};
|
|
7047
|
+
return /* @__PURE__ */ jsxs18("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
|
|
7048
|
+
features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx24(
|
|
7049
|
+
ImportTrackModal,
|
|
7050
|
+
{
|
|
7051
|
+
host,
|
|
7052
|
+
open: importOpen,
|
|
7053
|
+
onClose: () => setImportOpen(false),
|
|
7054
|
+
onImported: () => {
|
|
7055
|
+
void loadTracks(true);
|
|
7056
|
+
},
|
|
7057
|
+
onPortTrack: host.readImportableTrackMidi ? handlePortTrack : void 0,
|
|
7058
|
+
testIdPrefix: `${identity.familyKey}-import`
|
|
7059
|
+
}
|
|
7060
|
+
),
|
|
7061
|
+
features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx24(
|
|
7062
|
+
ImportTrackModal,
|
|
7063
|
+
{
|
|
7064
|
+
host,
|
|
7065
|
+
mode: "sound",
|
|
7066
|
+
open: !!soundImportTarget,
|
|
7067
|
+
title: adapter.sound.importSoundLabel,
|
|
7068
|
+
onClose: () => setSoundImportTarget(null),
|
|
7069
|
+
onImported: () => {
|
|
7070
|
+
},
|
|
7071
|
+
onPick: handleSoundImportPick,
|
|
7072
|
+
testIdPrefix: `${identity.familyKey}-sound-import`
|
|
7073
|
+
}
|
|
7074
|
+
),
|
|
7075
|
+
slots?.modals,
|
|
7076
|
+
canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx24("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx24(
|
|
7077
|
+
TransitionDesigner,
|
|
7078
|
+
{
|
|
7079
|
+
host,
|
|
7080
|
+
fromSceneId: xfFromId,
|
|
7081
|
+
toSceneId: xfToId,
|
|
7082
|
+
transitionSceneId: activeSceneId ?? "",
|
|
7083
|
+
excludeSourceDbIds: [
|
|
7084
|
+
...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
|
|
7085
|
+
...fadesMeta.map((f) => f.meta.sourceTrackDbId)
|
|
7086
|
+
],
|
|
7087
|
+
onCreateCrossfade: transition.handleCreateCrossfade,
|
|
7088
|
+
onCreateFade: transition.handleCreateFade,
|
|
7089
|
+
familyLabel: identity.familyLabel,
|
|
7090
|
+
testIdPrefix: `${identity.familyKey}-transition-designer`
|
|
7091
|
+
}
|
|
7092
|
+
) }),
|
|
7093
|
+
!(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx24("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs18(Fragment6, { children: [
|
|
7094
|
+
slots?.beforeRows,
|
|
7095
|
+
resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx24(
|
|
7096
|
+
CrossfadeTrackRow,
|
|
7097
|
+
{
|
|
7098
|
+
accentColor: identity.transitionAccentColor,
|
|
7099
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7100
|
+
sliderPos: pair.sliderPos,
|
|
7101
|
+
origin: {
|
|
7102
|
+
trackId: pair.origin.handle.id,
|
|
7103
|
+
name: pair.origin.handle.name,
|
|
7104
|
+
role: pair.origin.role,
|
|
7105
|
+
sourceName: pair.originSourceName,
|
|
7106
|
+
soundLabel: pair.originSoundLabel,
|
|
7107
|
+
runtimeState: pair.origin.runtimeState
|
|
7108
|
+
},
|
|
7109
|
+
target: {
|
|
7110
|
+
trackId: pair.target.handle.id,
|
|
7111
|
+
name: pair.target.handle.name,
|
|
7112
|
+
role: pair.target.role,
|
|
7113
|
+
sourceName: pair.targetSourceName,
|
|
7114
|
+
soundLabel: pair.targetSoundLabel,
|
|
7115
|
+
runtimeState: pair.target.runtimeState
|
|
7116
|
+
},
|
|
7117
|
+
onMuteToggle: () => transition.handleCrossfadeMute(pair),
|
|
7118
|
+
onSoloToggle: () => transition.handleCrossfadeSolo(pair),
|
|
7119
|
+
onVolumeChange: (slot, vol) => handlers.volumeChange(
|
|
7120
|
+
slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
|
|
7121
|
+
vol
|
|
7122
|
+
),
|
|
7123
|
+
onPanChange: (slot, pan) => handlers.panChange(
|
|
7124
|
+
slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
|
|
7125
|
+
pan
|
|
7126
|
+
),
|
|
7127
|
+
onSliderChange: (pos) => transition.handleCrossfadeSlider(pair, pos),
|
|
7128
|
+
onDelete: () => transition.handleCrossfadeDelete(pair)
|
|
7129
|
+
},
|
|
7130
|
+
pair.groupId
|
|
7131
|
+
)),
|
|
7132
|
+
resolvedFades.map((fade) => /* @__PURE__ */ jsx24(
|
|
7133
|
+
FadeTrackRow,
|
|
7134
|
+
{
|
|
7135
|
+
accentColor: identity.transitionAccentColor,
|
|
7136
|
+
levels: supportsMeters ? trackLevels : void 0,
|
|
7137
|
+
direction: fade.meta.direction,
|
|
7138
|
+
gesture: fade.meta.gesture,
|
|
7139
|
+
sliderPos: fade.meta.sliderPos,
|
|
7140
|
+
layer: {
|
|
7141
|
+
trackId: fade.track.handle.id,
|
|
7142
|
+
name: fade.track.handle.name,
|
|
7143
|
+
role: fade.track.role,
|
|
7144
|
+
sourceName: fade.meta.sourceName,
|
|
7145
|
+
soundLabel: fade.meta.soundLabel,
|
|
7146
|
+
runtimeState: fade.track.runtimeState
|
|
7147
|
+
},
|
|
7148
|
+
onMuteToggle: () => handlers.muteToggle(fade.track.handle.id),
|
|
7149
|
+
onSoloToggle: () => handlers.soloToggle(fade.track.handle.id),
|
|
7150
|
+
onVolumeChange: (vol) => handlers.volumeChange(fade.track.handle.id, vol),
|
|
7151
|
+
onPanChange: (pan) => handlers.panChange(fade.track.handle.id, pan),
|
|
7152
|
+
onSliderChange: (pos) => transition.handleFadeSlider(fade, pos),
|
|
7153
|
+
onDelete: () => transition.handleFadeDelete(fade)
|
|
7154
|
+
},
|
|
7155
|
+
fade.dbId
|
|
7156
|
+
)),
|
|
7157
|
+
(adapter.groupExtensions ?? []).flatMap(
|
|
7158
|
+
(ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx24(React20.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
|
|
7159
|
+
),
|
|
7160
|
+
tracks.map((track, index) => {
|
|
7161
|
+
if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
|
|
7162
|
+
return null;
|
|
7163
|
+
}
|
|
7164
|
+
return /* @__PURE__ */ jsx24(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
|
|
7165
|
+
}),
|
|
7166
|
+
slots?.afterRows
|
|
7167
|
+
] })),
|
|
7168
|
+
features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
|
|
7169
|
+
const hasAnyMidi = tracks.some((t) => t.hasMidi);
|
|
7170
|
+
const exportDisabled = isExportingMidi || !hasAnyMidi;
|
|
7171
|
+
return /* @__PURE__ */ jsx24("div", { className: "pt-2", children: /* @__PURE__ */ jsx24(
|
|
7172
|
+
"button",
|
|
7173
|
+
{
|
|
7174
|
+
"data-testid": "export-midi-tracks-button",
|
|
7175
|
+
onClick: handleExportMidi,
|
|
7176
|
+
disabled: exportDisabled,
|
|
7177
|
+
title: isExportingMidi ? "Exporting..." : !hasAnyMidi ? "Generate MIDI on at least one track first" : "Export all tracks as a ZIP of .mid files",
|
|
7178
|
+
className: `w-full px-2 py-1.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors ${exportDisabled ? "text-sas-muted/40 border-transparent hover:border-sas-accent cursor-not-allowed" : "text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent"}`,
|
|
7179
|
+
children: isExportingMidi ? "Exporting..." : "Export Tracks"
|
|
7180
|
+
}
|
|
7181
|
+
) });
|
|
7182
|
+
})()
|
|
7183
|
+
] });
|
|
7184
|
+
}
|
|
7185
|
+
|
|
7186
|
+
// src/panel-core/surge-sound-adapter.ts
|
|
7187
|
+
async function getInstrument(host, trackId) {
|
|
7188
|
+
try {
|
|
7189
|
+
const plugins = await host.getTrackPlugins(trackId);
|
|
7190
|
+
const instrument = plugins.find(
|
|
7191
|
+
(p) => !p.name.includes("Volume") && !p.name.includes("Pan") && !p.name.includes("Level")
|
|
7192
|
+
);
|
|
7193
|
+
if (!instrument) return null;
|
|
7194
|
+
return { index: instrument.index, isRaw: !instrument.name.includes("Surge") };
|
|
7195
|
+
} catch {
|
|
7196
|
+
return null;
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
function createSurgeSoundAdapter(host, overrides = {}) {
|
|
7200
|
+
const applySound = async (trackId, descriptor) => {
|
|
7201
|
+
const { state, stateType } = descriptor;
|
|
7202
|
+
const inst = await getInstrument(host, trackId);
|
|
7203
|
+
if (!inst) return;
|
|
7204
|
+
if (stateType === "raw") await host.setRawPluginState(trackId, inst.index, state);
|
|
7205
|
+
else await host.setPluginState(trackId, inst.index, state);
|
|
7206
|
+
};
|
|
7207
|
+
return {
|
|
7208
|
+
applySound,
|
|
7209
|
+
captureSoundDescriptor: async (trackId) => {
|
|
7210
|
+
const inst = await getInstrument(host, trackId);
|
|
7211
|
+
if (!inst) return null;
|
|
7212
|
+
const state = inst.isRaw ? await host.getRawPluginState(trackId, inst.index) : await host.getPluginState(trackId, inst.index);
|
|
7213
|
+
return { descriptor: { state, stateType: inst.isRaw ? "raw" : "valuetree" } };
|
|
7214
|
+
},
|
|
7215
|
+
copySnapshot: async (trackId, snap) => {
|
|
7216
|
+
if (snap.kind !== "preset") return "default";
|
|
7217
|
+
await applySound(trackId, { state: snap.state, stateType: snap.stateType });
|
|
7218
|
+
await host.persistTrackPresetState?.(trackId, {
|
|
7219
|
+
state: snap.state,
|
|
7220
|
+
stateType: snap.stateType ?? "valuetree",
|
|
7221
|
+
name: snap.label
|
|
7222
|
+
}).catch(() => {
|
|
7223
|
+
});
|
|
7224
|
+
return snap.label;
|
|
7225
|
+
},
|
|
7226
|
+
descriptorFromSnapshot: (snap) => {
|
|
7227
|
+
const preset = snap;
|
|
7228
|
+
return { state: preset.state, stateType: preset.stateType };
|
|
7229
|
+
},
|
|
7230
|
+
acceptedSnapshotKind: "preset",
|
|
7231
|
+
historyMax: overrides.historyMax ?? 12,
|
|
7232
|
+
importSoundLabel: overrides.importSoundLabel ?? "Import Preset",
|
|
7233
|
+
importNoun: "preset",
|
|
7234
|
+
previousSoundLabel: "Previous preset"
|
|
7235
|
+
};
|
|
7236
|
+
}
|
|
7237
|
+
|
|
4917
7238
|
// src/constants/sdk-version.ts
|
|
4918
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
7239
|
+
var PLUGIN_SDK_VERSION = "2.35.0";
|
|
4919
7240
|
|
|
4920
7241
|
// src/utils/format-concurrent-tracks.ts
|
|
4921
7242
|
function formatConcurrentTracks(ctx) {
|
|
@@ -5081,6 +7402,7 @@ export {
|
|
|
5081
7402
|
FadeTrackRow,
|
|
5082
7403
|
FxToggleBar,
|
|
5083
7404
|
GUTTER_W,
|
|
7405
|
+
GeneratorPanelShell,
|
|
5084
7406
|
ImportTrackModal,
|
|
5085
7407
|
TrackDrawer as InstrumentDrawer,
|
|
5086
7408
|
LevelMeter,
|
|
@@ -5118,6 +7440,7 @@ export {
|
|
|
5118
7440
|
cellToPx,
|
|
5119
7441
|
centerScrollTop,
|
|
5120
7442
|
computePeaks,
|
|
7443
|
+
createSurgeSoundAdapter,
|
|
5121
7444
|
dbIdsFromKeys,
|
|
5122
7445
|
dbToSlider,
|
|
5123
7446
|
defaultFadeGesture,
|
|
@@ -5125,16 +7448,21 @@ export {
|
|
|
5125
7448
|
formatConcurrentTracks,
|
|
5126
7449
|
hashString,
|
|
5127
7450
|
moveItem,
|
|
7451
|
+
newTrackState,
|
|
5128
7452
|
normalizeSlots,
|
|
5129
7453
|
padPair,
|
|
5130
7454
|
padSlots,
|
|
5131
7455
|
parseCrossfadePairs,
|
|
5132
7456
|
parseFades,
|
|
7457
|
+
parseLLMNoteResponse,
|
|
7458
|
+
parseTrackGroups,
|
|
5133
7459
|
pickTopKWeighted,
|
|
5134
7460
|
pitchToName,
|
|
7461
|
+
pluginFxToToggleFx,
|
|
5135
7462
|
pxToCell,
|
|
5136
7463
|
reconcileSlots,
|
|
5137
7464
|
resizeNoteDuration,
|
|
7465
|
+
resolveTrackGroups,
|
|
5138
7466
|
rowKey,
|
|
5139
7467
|
rowType,
|
|
5140
7468
|
scorePromptMatch,
|
|
@@ -5143,14 +7471,17 @@ export {
|
|
|
5143
7471
|
soundIdentity,
|
|
5144
7472
|
synthesizeCuePoints,
|
|
5145
7473
|
tokenizePrompt,
|
|
7474
|
+
trackDataKey,
|
|
5146
7475
|
transposeNotes,
|
|
5147
7476
|
useAnySolo,
|
|
7477
|
+
useGeneratorPanelCore,
|
|
5148
7478
|
useSceneState,
|
|
5149
7479
|
useSoundHistory,
|
|
5150
7480
|
useTrackLevel,
|
|
5151
7481
|
useTrackLevels,
|
|
5152
7482
|
useTrackMeter,
|
|
5153
7483
|
useTrackReorder,
|
|
7484
|
+
useTransitionOps,
|
|
5154
7485
|
useTransportPlaying
|
|
5155
7486
|
};
|
|
5156
7487
|
//# sourceMappingURL=index.mjs.map
|