@map-audio/pam-mcp-server 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -10
- package/dist/bridge.d.ts +5 -1
- package/dist/bridge.js +41 -40
- package/dist/bridge.js.map +1 -1
- package/dist/daemon.d.ts +32 -0
- package/dist/daemon.js +871 -0
- package/dist/daemon.js.map +7 -0
- package/dist/drumSynth.d.ts +12 -0
- package/dist/drumSynth.js +14 -2
- package/dist/drumSynth.js.map +1 -1
- package/dist/drumSynthEngines.json +9 -1
- package/dist/{fgemma-format.d.ts → functiongemma-format.d.ts} +25 -7
- package/dist/{fgemma-format.js → functiongemma-format.js} +34 -11
- package/dist/functiongemma-format.js.map +1 -0
- package/dist/functiongemma-grammar.d.ts +14 -0
- package/dist/functiongemma-grammar.js +140 -0
- package/dist/functiongemma-grammar.js.map +1 -0
- package/dist/functiongemma-params.d.ts +33 -0
- package/dist/functiongemma-params.js +128 -0
- package/dist/functiongemma-params.js.map +1 -0
- package/dist/{fgemma-runner.d.ts → functiongemma-runner.d.ts} +8 -5
- package/dist/{fgemma-runner.js → functiongemma-runner.js} +87 -21
- package/dist/functiongemma-runner.js.map +1 -0
- package/dist/generativePlayerPresets.json +9 -0
- package/dist/generativePlayers.json +14 -0
- package/dist/launcher.d.ts +2 -0
- package/dist/launcher.js +36 -0
- package/dist/launcher.js.map +1 -0
- package/dist/manifest-mfx.json +12920 -277
- package/dist/manifest-msq.json +1989 -107
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +7 -27
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.json +24194 -345
- package/dist/mfxCombos.d.ts +2 -0
- package/dist/mfxCombos.js +179 -56
- package/dist/mfxCombos.js.map +1 -1
- package/dist/midiFile.d.ts +19 -5
- package/dist/midiFile.js +21 -13
- package/dist/midiFile.js.map +1 -1
- package/dist/mixAssist.js +3 -2
- package/dist/mixAssist.js.map +1 -1
- package/dist/players.d.ts +22 -0
- package/dist/players.js +70 -0
- package/dist/players.js.map +1 -1
- package/dist/productProfile.d.ts +4 -4
- package/dist/productProfile.js +10 -1
- package/dist/productProfile.js.map +1 -1
- package/dist/qwen-format.d.ts +85 -0
- package/dist/qwen-format.js +355 -0
- package/dist/qwen-format.js.map +1 -0
- package/dist/registration.mjs +41 -0
- package/dist/server.js +1309 -187
- package/dist/server.js.map +1 -1
- package/dist/variationSessionKeys.json +6 -4
- package/package.json +13 -7
- package/dist/fgemma-format.js.map +0 -1
- package/dist/fgemma-runner.js.map +0 -1
- package/dist/manifest-cell1.json +0 -12968
package/dist/server.js
CHANGED
|
@@ -16,16 +16,16 @@ import { inflateSync } from "node:zlib";
|
|
|
16
16
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { createDrumSynthCatalog, } from "./drumSynth.js";
|
|
19
|
-
import { loadManifest, cellEffectNames, cellEffectOrder, getParameterIndex, getParametersByCell, getGlobalParameters, findPresetDirs, resolvePresetPath, setActiveProductHint, walkPresets, } from "./manifest.js";
|
|
19
|
+
import { loadManifest, cellEffectNames, cellEffectOrder, getParameterIndex, getParametersByCell, getGlobalParameters, findPresetDirs, resolvePresetPath, productExportDir, setActiveProductHint, walkPresets, } from "./manifest.js";
|
|
20
20
|
import { PluginBridge } from "./bridge.js";
|
|
21
21
|
import { LANE_TARGET_FIELD, PATTERN_LANE_IDS, PATTERN_LANE_SUFFIX, CC_LANES, PARAM_LANES, ccLaneMayLand, ccLaneIndex, ccLaneSlotField, ccLaneSlotOf, CC_OUT_CONFIG_KEYS, CC_OUT_SLOTS, isCcLane, isCcOutValueSuffix, isParamLane, normalizeCcLaneEvents, normalizeTargetLaneEvents, productHasLane, resolveLaneTarget, } from "./patternLanes.js";
|
|
22
22
|
import { buildMixMeasurementFromState, currentFlatFromState, parseMixAnalysisPayload, suggestMix, } from "./mixAssist.js";
|
|
23
23
|
import { isFactoryPresetPath, nativePresetPath, normalizePresetLookupPath, parsePresetFile, selectSaveVariationSourcePath, } from "./presetSave.js";
|
|
24
|
-
import { STOP_SEQUENCES, buildGenerationPrompt, parseFunctionCalls, renderContextLine, renderFunctionCall, renderFunctionResponse, truncateAtFabricatedResponse, withContext, } from "./
|
|
24
|
+
import { STOP_SEQUENCES, buildGenerationPrompt, parseFunctionCalls, renderContextLine, renderFunctionCall, renderFunctionResponse, truncateAtFabricatedResponse, withContext, } from "./functiongemma-format.js";
|
|
25
25
|
import { mcpProductProfile } from "./productProfile.js";
|
|
26
|
-
import { MFX_COMBO_COUNT, MFX_COMBO_MANUAL_STRENGTH, buildMfxComboTarget, isMfxComboTargetParameter, mfxComboTargetValueFromCurrent, parseMfxComboSlots, reanchorMfxComboTarget, resolveMfxCombos, setMfxComboTargetStop, } from "./mfxCombos.js";
|
|
27
|
-
import { GENERATIVE_PLAYERS, LISTEN_MODES, MODULATABLE_PLAYER_FIELDS, NO_POLY_ARM, PATTERN_MODES, PLAYER_GENRES, PLAYER_NAMES, PLAYER_SLOT_FIELDS, cellPlayerType, describeCellPlayer, describePlayerSpec, getPlayerSpec, isMelodicPlayer, playerDefaultsFor, playerDensityDisplay, playerGenreConfig, playerMutateDisplay, playerPresetsFor, playerSlotDisplay, presetConfig, randomPlayerSeed, resolvePlayerPreset, resolvePlayerSpec, resolveSlotValue, rolePlayerConfigFor, roleSpreadSummary, snapPlayerValue, } from "./players.js";
|
|
28
|
-
import { buildMidiFile, patternSpeedMultiplier, tilePatternEvents } from "./midiFile.js";
|
|
26
|
+
import { MFX_COMBO_COUNT, MFX_COMBO_MANUAL_STRENGTH, buildMfxComboTarget, isMfxComboTargetParameter, mfxComboTargetValueFromCurrent, mfxComboTargetValueAfterRangeEdit, parseMfxComboSlots, reanchorMfxComboTarget, resolveMfxCombos, setMfxComboTargetStop, } from "./mfxCombos.js";
|
|
27
|
+
import { GENERATIVE_PLAYERS, LISTEN_MODES, LISTEN_PAIRS, MODULATABLE_PLAYER_FIELDS, NO_POLY_ARM, NO_SCALE_ARM, PATTERN_MODES, PLAYER_GENRES, PLAYER_GRIDS, PLAYER_NAMES, PLAYER_SLOT_FIELDS, cellPlayerType, describeCellPlayer, describePlayerSpec, getPlayerSpec, isMelodicPlayer, crowdedComplementCell, listenPartnerFor, soleComplementPartner, playerDefaultsFor, playerDensityDisplay, playerGenreConfig, playerGridValue, playerMutateDisplay, playerPresetsFor, playerSlotDisplay, presetConfig, randomPlayerSeed, resolvePlayerPreset, resolvePlayerSpec, resolveSlotValue, rolePlayerConfigFor, roleSpreadSummary, snapPlayerValue, } from "./players.js";
|
|
28
|
+
import { beatsPerBar, buildMidiFile, patternSpeedMultiplier, tilePatternEvents } from "./midiFile.js";
|
|
29
29
|
import { MAX_SLICES, detectOnsetBoundaries, equalBoundaries, normalizeBoundaries } from "./sampleSlicing.js";
|
|
30
30
|
// --- State ---
|
|
31
31
|
// Variation slot count. Must stay in sync with PRESET_VARIATION_SLOTS in
|
|
@@ -34,7 +34,56 @@ import { MAX_SLICES, detectOnsetBoundaries, equalBoundaries, normalizeBoundaries
|
|
|
34
34
|
const PRESET_VARIATION_SLOTS = 8;
|
|
35
35
|
const SERVER_PRODUCT = mcpProductProfile();
|
|
36
36
|
const IS_MFX_SERVER = SERVER_PRODUCT.id === "mfx";
|
|
37
|
+
const IS_MSQ_SERVER = SERVER_PRODUCT.id === "msq";
|
|
38
|
+
// A product launcher attaches to its own product only; pam-mcp follows whichever instance is connected.
|
|
39
|
+
const PINNED_PRODUCT = IS_MFX_SERVER || IS_MSQ_SERVER ? SERVER_PRODUCT.id : undefined;
|
|
40
|
+
if (PINNED_PRODUCT)
|
|
41
|
+
process.env.PAM_BRIDGE_PRODUCT = SERVER_PRODUCT.name;
|
|
42
|
+
const DAEMON_SESSION = process.argv.includes("--pamd-session");
|
|
43
|
+
function sessionOption(name) {
|
|
44
|
+
return process.argv.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
45
|
+
}
|
|
46
|
+
const SESSION_DATA_ROOT = sessionOption("data-root");
|
|
47
|
+
const SESSION_OUTPUT_DIR = sessionOption("out-dir");
|
|
48
|
+
const SESSION_DISCOVERY = sessionOption("discovery-file");
|
|
49
|
+
for (const [name, path] of [["data-root", SESSION_DATA_ROOT], ["out-dir", SESSION_OUTPUT_DIR], ["discovery-file", SESSION_DISCOVERY]])
|
|
50
|
+
if (path && !isAbsolute(path))
|
|
51
|
+
throw new Error(`${name} must be absolute`);
|
|
52
|
+
if (SESSION_DATA_ROOT)
|
|
53
|
+
process.env.PAM_AI_DATA_ROOT = SESSION_DATA_ROOT;
|
|
54
|
+
if (DAEMON_SESSION && (!SESSION_DATA_ROOT || !SESSION_OUTPUT_DIR || !SESSION_DISCOVERY))
|
|
55
|
+
throw new Error("pamd session requires captured data-root, out-dir and discovery-file");
|
|
56
|
+
// Loaded only for pam mcp sessions, so the plugin-only server starts without a built pamd client.
|
|
57
|
+
const daemon = DAEMON_SESSION
|
|
58
|
+
? new (await import("./daemon.js")).DaemonSession(SESSION_DISCOVERY, SESSION_DATA_ROOT, SESSION_OUTPUT_DIR) : undefined;
|
|
59
|
+
const daemonDownloads = new Map();
|
|
60
|
+
function daemonModel(engine, model) {
|
|
61
|
+
if (engine === "aigen")
|
|
62
|
+
return "stable-audio-3-small-sfx";
|
|
63
|
+
if (engine === "magenta")
|
|
64
|
+
return model === "base" ? "mrt2-base" : "mrt2-small";
|
|
65
|
+
throw new Error("unsupported_model: notochord remains managed by the plugin");
|
|
66
|
+
}
|
|
37
67
|
const bridge = new PluginBridge();
|
|
68
|
+
async function pinnedGeneration(args, pinned, signal) {
|
|
69
|
+
const destination = pinned ?? new PluginBridge();
|
|
70
|
+
const instance = bridge.getConnectedInstance();
|
|
71
|
+
if (!pinned && (!instance || !await destination.connect({ instanceId: instance.instanceId, product: instance.product })))
|
|
72
|
+
throw new Error("plugin_unavailable: selected instance disconnected");
|
|
73
|
+
try {
|
|
74
|
+
const result = await daemon.generate(args, signal);
|
|
75
|
+
if (args.cellId !== undefined) {
|
|
76
|
+
if (typeof result.wavPath !== "string" || !isAbsolute(result.wavPath))
|
|
77
|
+
throw new Error("invalid_result: generated WAV path missing");
|
|
78
|
+
await destination.send("loadSampleToCell", result.wavPath, args.cellId);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (!pinned)
|
|
84
|
+
destination.disconnect();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
38
87
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
39
88
|
const cellModeChangeSeq = {};
|
|
40
89
|
let lastSavedPresetName;
|
|
@@ -75,11 +124,38 @@ function structuredFromBridge(raw) {
|
|
|
75
124
|
function error(msg) {
|
|
76
125
|
return { content: [{ type: "text", text: msg }], isError: true };
|
|
77
126
|
}
|
|
127
|
+
function daemonFailureCode(cause) {
|
|
128
|
+
try {
|
|
129
|
+
const detail = JSON.parse(cause instanceof Error ? cause.message : String(cause));
|
|
130
|
+
return isRecord(detail) && typeof detail.code === "string" ? detail.code : undefined;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const LOST_DAEMON_JOB = new Set(["job_expired", "not_found", "daemon_restarted", "unavailable"]);
|
|
137
|
+
function daemonFailure(cause, prefix = "") {
|
|
138
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
139
|
+
const result = error(prefix + message);
|
|
140
|
+
try {
|
|
141
|
+
const detail = JSON.parse(message);
|
|
142
|
+
if (isRecord(detail) && detail.ok === false && typeof detail.code === "string" && typeof detail.error === "string")
|
|
143
|
+
return { ...result, structuredContent: detail };
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
78
148
|
function isMfxManifest(manifest) {
|
|
79
149
|
return (manifest.name?.toLowerCase() === "mfx" ||
|
|
80
150
|
manifest.bundleId?.toLowerCase() === "map.audio.mfx" ||
|
|
81
151
|
manifest.pluginCode?.toLowerCase() === "amfx");
|
|
82
152
|
}
|
|
153
|
+
function isMsqManifest(manifest) {
|
|
154
|
+
return (manifest.name?.toLowerCase() === "msq" ||
|
|
155
|
+
manifest.bundleId?.toLowerCase() === "map.audio.msq" ||
|
|
156
|
+
manifest.pluginCode?.toLowerCase() === "amsq");
|
|
157
|
+
}
|
|
158
|
+
const PACK_TOOL_NAMES = new Set(["list_packs", "install_pack", "get_pack_install_status", "uninstall_pack"]);
|
|
83
159
|
// MSQ sequences MIDI only. These tools drive a part of PAM it does not have, so on an
|
|
84
160
|
// MSQ instance they answer with the reason instead of writing state nothing reads.
|
|
85
161
|
const MSQ_MISSING_BY_TOOL = new Map([
|
|
@@ -90,9 +166,10 @@ const MSQ_MISSING_BY_TOOL = new Map([
|
|
|
90
166
|
"preview_sample", "get_sample_usage", "repair_preset_samples", "set_drum_synth", "set_drum_synth_wave",
|
|
91
167
|
"set_lane_target",
|
|
92
168
|
].map((tool) => [tool, "sampler"]),
|
|
93
|
-
...["update_effect_config", "set_effect_order"].map((tool) => [tool, "cell effects"]),
|
|
169
|
+
...["update_effect_config", "set_effects_enabled", "set_effect_order", "set_spectral_delay_curve"].map((tool) => [tool, "cell effects"]),
|
|
94
170
|
...["set_master_effects", "get_master_analysis", "get_mix_analysis", "suggest_mix", "set_metronome"].map((tool) => [tool, "audio output"]),
|
|
95
171
|
...["export_audio", "export_song", "song_mode", "live_resample"].map((tool) => [tool, "audio render or Song Mode"]),
|
|
172
|
+
["dj_set", "DJ view"],
|
|
96
173
|
...[
|
|
97
174
|
"arm_looper_track", "set_looper_track", "clear_looper_track", "set_looper_track_selection",
|
|
98
175
|
"start_looper_recording", "stop_looper_recording", "capture_looper_now", "load_audio_to_looper_track",
|
|
@@ -115,12 +192,19 @@ const MSQ_MISSING_BY_TOOL = new Map([
|
|
|
115
192
|
"workbench_list_takes", "workbench_load_take", "workbench_undo", "workbench_reset_chain",
|
|
116
193
|
"workbench_sidecar_status", "ask_assistant",
|
|
117
194
|
].map((tool) => [tool, "AI stack"]),
|
|
195
|
+
...[...PACK_TOOL_NAMES].map((tool) => [tool, "preset pack store"]),
|
|
196
|
+
]);
|
|
197
|
+
// Tools whose failures carry a code answer MSQ's refusal with one too.
|
|
198
|
+
const MSQ_MISSING_CODES = new Map([
|
|
199
|
+
...[...PACK_TOOL_NAMES].map((tool) => [tool, "pack_unsupported_product"]),
|
|
200
|
+
["dj_set", "dj_unsupported_product"],
|
|
118
201
|
]);
|
|
119
|
-
const MFX_CELL_CONFIG_KEYS = new Set(["sidechainFrom"]);
|
|
202
|
+
const MFX_CELL_CONFIG_KEYS = new Set(["sidechainFrom", "cellmixFrom", "cellmixPreFx"]);
|
|
120
203
|
// MSQ cells are MIDI tracks: their note, channel, scale and output wiring, never sampler config.
|
|
121
204
|
const MSQ_CELL_CONFIG_KEYS = new Set([
|
|
122
205
|
"isPoly", "midiNote", "midiChannel", "midiIsolate", "armed", "polyRootNote", "scaleName", "scaleRoot",
|
|
123
|
-
"midiOutChannel", "sendToHost", "midiOutPort",
|
|
206
|
+
"midiOutChannel", "sendToHost", "midiOutPort", "polyNoteMin", "polyNoteMax", ...CC_OUT_CONFIG_KEYS,
|
|
207
|
+
...CC_LANES.map(ccLaneSlotField),
|
|
124
208
|
]);
|
|
125
209
|
// A CC out slot's controller (cell config ccOutCc{K}); 120-127 are channel-mode messages.
|
|
126
210
|
const ccOutControllerSchema = (slot) => z
|
|
@@ -166,7 +250,11 @@ function validateCellIdForManifest(cellId, manifest) {
|
|
|
166
250
|
return true;
|
|
167
251
|
}
|
|
168
252
|
function readPackageVersion() {
|
|
169
|
-
const override = IS_MFX_SERVER
|
|
253
|
+
const override = IS_MFX_SERVER
|
|
254
|
+
? process.env.MFX_MCP_SERVER_VERSION?.trim()
|
|
255
|
+
: IS_MSQ_SERVER
|
|
256
|
+
? process.env.MSQ_MCP_SERVER_VERSION?.trim()
|
|
257
|
+
: undefined;
|
|
170
258
|
if (override)
|
|
171
259
|
return override;
|
|
172
260
|
try {
|
|
@@ -213,12 +301,12 @@ async function requireBridge() {
|
|
|
213
301
|
if (!ok) {
|
|
214
302
|
const pin = process.env.PAM_BRIDGE_PRODUCT;
|
|
215
303
|
return error(`Not connected to ${SERVER_PRODUCT.name}. Start ${SERVER_PRODUCT.name} (standalone or in a DAW) with MCP bridge enabled. ` +
|
|
216
|
-
(
|
|
217
|
-
?
|
|
218
|
-
: "The bridge writes its
|
|
304
|
+
(PINNED_PRODUCT
|
|
305
|
+
? `The launcher finds ${SERVER_PRODUCT.name} through the local bridge registry; the plugin records its actual port on startup.`
|
|
306
|
+
: "The bridge writes an authenticated registration under its data folder on startup. If PAM is already running with the bridge enabled, update PAM to a version that supports authenticated MCP connections.") +
|
|
219
307
|
(pin
|
|
220
|
-
?
|
|
221
|
-
?
|
|
308
|
+
? PINNED_PRODUCT
|
|
309
|
+
? ` The ${SERVER_PRODUCT.name} launcher is product-pinned, but no matching instance was found.`
|
|
222
310
|
: ` Note: PAM_BRIDGE_PRODUCT pins this server to '${pin}' — no matching instance was found (connection_status lists what is running).`
|
|
223
311
|
: ""));
|
|
224
312
|
}
|
|
@@ -229,7 +317,7 @@ async function requireBridge() {
|
|
|
229
317
|
* requireBridge() plus product-aware cell bounds, for tools that ADDRESS a cell.
|
|
230
318
|
*
|
|
231
319
|
* The zod schemas cap cellId at 8 because they are built before we know which
|
|
232
|
-
* product is on the other end.
|
|
320
|
+
* product is on the other end. MFX has 4 cells, so without this
|
|
233
321
|
* an agent aims at cell 7 and gets a bridge-level failure or a silent no-op
|
|
234
322
|
* instead of being told the cell does not exist. Read filters (get_state,
|
|
235
323
|
* get_design_overview) deliberately stay unguarded — returning only the cells
|
|
@@ -640,6 +728,29 @@ function filterSectionResult(section, result, opts) {
|
|
|
640
728
|
function countEvents(value) {
|
|
641
729
|
return Array.isArray(value) ? value.length : 0;
|
|
642
730
|
}
|
|
731
|
+
// What an MSQ cell sends: its output wiring, what plays it, and the CC it carries.
|
|
732
|
+
function msqCellSummary(id, cell, state) {
|
|
733
|
+
const prefix = `cell${id}_`;
|
|
734
|
+
const channel = Math.round(Number(cell.midiOutChannel ?? 0));
|
|
735
|
+
return {
|
|
736
|
+
cellId: id,
|
|
737
|
+
muted: state[`${prefix}mute`] === 1 || state[`${prefix}mute`] === true,
|
|
738
|
+
send: Number(cell.sendToHost ?? 1) > 0.5,
|
|
739
|
+
channel: channel >= 1 && channel <= 16 ? channel : `auto (${((id - 1) % 16) + 1})`,
|
|
740
|
+
port: Math.round(Number(cell.midiOutPort ?? 0)),
|
|
741
|
+
triggerNote: cell.midiNote,
|
|
742
|
+
poly: Number(cell.isPoly) > 0.5 ? { low: cell.polyNoteMin ?? 0, high: cell.polyNoteMax ?? 127 } : false,
|
|
743
|
+
player: describeCellPlayer(id, cell, state),
|
|
744
|
+
noteEvents: countEvents(state[`${prefix}${PATTERN_LANE_SUFFIX.note}`]),
|
|
745
|
+
ccLaneEvents: Object.fromEntries(CC_LANES.map((lane) => [lane, countEvents(state[`${prefix}${PATTERN_LANE_SUFFIX[lane]}`])])),
|
|
746
|
+
ccOut: CC_OUT_SLOTS.flatMap((slot) => {
|
|
747
|
+
const controller = Number(cell[`ccOutCc${slot}`]);
|
|
748
|
+
return Number.isInteger(controller) && controller >= 0 && controller <= 119
|
|
749
|
+
? [{ slot, controller, value: state[`${prefix}ccOut${slot}`] }]
|
|
750
|
+
: [];
|
|
751
|
+
}),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
643
754
|
function stemName(path) {
|
|
644
755
|
if (typeof path !== "string" || path.length === 0)
|
|
645
756
|
return undefined;
|
|
@@ -654,14 +765,19 @@ const server = new McpServer({
|
|
|
654
765
|
instructions: IS_MFX_SERVER
|
|
655
766
|
? "MFX is a four-cell modular FX router: cells are patched with cables and each cell runs its own effect chain. " +
|
|
656
767
|
"Use cells 1-4. Treat tool annotations as authoritative and use select_instance when several products are running."
|
|
657
|
-
:
|
|
658
|
-
"
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
"
|
|
663
|
-
|
|
664
|
-
|
|
768
|
+
: IS_MSQ_SERVER
|
|
769
|
+
? "MSQ is an eight-cell MIDI sequencer: each cell sends its pattern or Player's notes and its CC out on its own " +
|
|
770
|
+
"MIDI channel to the host's instruments or, standalone, to MIDI devices. It makes no sound itself. Notes need a " +
|
|
771
|
+
"running transport; a CC out slot sends whenever its value moves. Treat tool annotations as authoritative and " +
|
|
772
|
+
"use select_instance when several MSQ instances are running."
|
|
773
|
+
: "PAM is an eight-cell sampler. " +
|
|
774
|
+
"Sequencer notes require a running transport; a cell's CC out slots send whenever their value moves. " +
|
|
775
|
+
"UI, video, and visual tools require the plugin " +
|
|
776
|
+
"window open; experimental plugin-host and AI tools require experimentalMode plus the matching " +
|
|
777
|
+
"feature flag. Treat tool annotations as authoritative and use select_instance when several " +
|
|
778
|
+
"products are running. A 'build does not support' error means update the plugin, not only this server. " +
|
|
779
|
+
"An MSQ instance sequences MIDI only: its cells send notes and CC to the host MIDI out, so sample, effect, " +
|
|
780
|
+
"master, looper, video, plugin-host and AI tools refuse it.",
|
|
665
781
|
});
|
|
666
782
|
// --- tool annotation policy ---------------------------------------------------
|
|
667
783
|
// Single audit surface for MCP tool annotations. Every tool MUST have an entry;
|
|
@@ -724,6 +840,8 @@ const TOOL_ANNOTATIONS = {
|
|
|
724
840
|
clear_modulations: DI,
|
|
725
841
|
reset_patterns: DI,
|
|
726
842
|
update_effect_config: WI,
|
|
843
|
+
set_effects_enabled: WI,
|
|
844
|
+
set_spectral_delay_curve: WI,
|
|
727
845
|
update_cell_config: WI,
|
|
728
846
|
set_lane_target: WI,
|
|
729
847
|
set_cell_mute: WI,
|
|
@@ -763,6 +881,11 @@ const TOOL_ANNOTATIONS = {
|
|
|
763
881
|
save_preset: WI,
|
|
764
882
|
new_preset: DI,
|
|
765
883
|
import_preset: WI,
|
|
884
|
+
// Preset packs
|
|
885
|
+
list_packs: RO,
|
|
886
|
+
get_pack_install_status: RO,
|
|
887
|
+
install_pack: WI,
|
|
888
|
+
uninstall_pack: DI, // deletes the pack's presets, samples and media
|
|
766
889
|
export_preset: W,
|
|
767
890
|
update_preset_metadata: WI,
|
|
768
891
|
delete_preset: DI,
|
|
@@ -804,6 +927,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
804
927
|
live_resample: D, // finalize replaces the destination cell; finalize:false discards the take
|
|
805
928
|
// Song mode / export
|
|
806
929
|
song_mode: W,
|
|
930
|
+
dj_set: DI,
|
|
807
931
|
export_song: W,
|
|
808
932
|
export_audio: W,
|
|
809
933
|
// Settings / devices
|
|
@@ -886,6 +1010,8 @@ const MFX_TOOL_NAMES = new Set([
|
|
|
886
1010
|
"update_modulation",
|
|
887
1011
|
"clear_modulations",
|
|
888
1012
|
"update_effect_config",
|
|
1013
|
+
"set_effects_enabled",
|
|
1014
|
+
"set_spectral_delay_curve",
|
|
889
1015
|
"update_cell_config",
|
|
890
1016
|
"set_cell_mute",
|
|
891
1017
|
"set_master_effects",
|
|
@@ -937,8 +1063,6 @@ function mfxSurfaceText(value) {
|
|
|
937
1063
|
.replaceAll(" Check corr in get_master_analysis after widening.", "")
|
|
938
1064
|
.replaceAll(" (same constraint as the video tools)", "")
|
|
939
1065
|
.replaceAll("so the preset_novelty scale does", "so a whole-preset novelty scale does")
|
|
940
|
-
.replaceAll("PAM/Cell1-only", "sampler-product-only")
|
|
941
|
-
.replaceAll("PAM/Cell1", "sampler products")
|
|
942
1066
|
.replaceAll("PAM-only", "sampler-product-only")
|
|
943
1067
|
.replaceAll("PAM's", "MFX's")
|
|
944
1068
|
.replaceAll("PAM", "MFX");
|
|
@@ -987,6 +1111,7 @@ const MFX_CELL_FIELDS = new Set([
|
|
|
987
1111
|
"cellIds",
|
|
988
1112
|
"selectedCellIds",
|
|
989
1113
|
"sidechainFrom",
|
|
1114
|
+
"cellmixFrom",
|
|
990
1115
|
]);
|
|
991
1116
|
// Enum members MFX has no surface for: Vary is removed from its modulation stack,
|
|
992
1117
|
// neither song_mode nor a looper tool is registered on this launcher, and the MIDI
|
|
@@ -998,7 +1123,7 @@ const MFX_ENUM_DROPS = {
|
|
|
998
1123
|
set_midi_mapping: { target: ["looper", "resample", "vxRecord", "looperVariation"] },
|
|
999
1124
|
};
|
|
1000
1125
|
const MFX_KEPT_FIELDS = {
|
|
1001
|
-
update_cell_config: ["cellId", "sidechainFrom"],
|
|
1126
|
+
update_cell_config: ["cellId", "sidechainFrom", "cellmixFrom", "cellmixPreFx"],
|
|
1002
1127
|
};
|
|
1003
1128
|
const MFX_DROPPED_FIELDS = {
|
|
1004
1129
|
select_instance: ["product"],
|
|
@@ -1009,7 +1134,8 @@ const MFX_DROPPED_FIELDS = {
|
|
|
1009
1134
|
};
|
|
1010
1135
|
const MFX_TOOL_DESCRIPTIONS = {
|
|
1011
1136
|
select_instance: "Choose which running MFX instance the live tools drive. Match by instanceId or port from connection_status. The launcher refuses non-MFX instances.",
|
|
1012
|
-
update_cell_config: "Set one MFX cell's sidechain source. A non-null source also enables that cell's
|
|
1137
|
+
update_cell_config: "Set one MFX cell's sidechain or Cell Mix source. A non-null sidechain source also enables that cell's " +
|
|
1138
|
+
"compressor, and a Cell Mix source other than -1 enables its Cell Mix card.",
|
|
1013
1139
|
capture_ui: "Render MFX's plugin UI (#app_root) to a PNG via html2canvas and return the saved file path (read it to " +
|
|
1014
1140
|
"view the image). Optionally compose the view first: gridSize (1 or 4), mainView (CellGrid|MfxRouting|" +
|
|
1015
1141
|
"CtrlAll|Envelope|LFO|ParamSeq|Mixer|PresetBrowser; omit/null = cell grid). REQUIRES MFX's window OPEN — " +
|
|
@@ -1020,7 +1146,8 @@ const MFX_TOOL_DESCRIPTIONS = {
|
|
|
1020
1146
|
"target's. Not copied: parameter sequencers and the cell config block (sidechain source) — re-apply that with " +
|
|
1021
1147
|
"update_cell_config.",
|
|
1022
1148
|
set_midi_mapping: "Configure the global MIDI-controller mappings that trigger MFX actions: variation switch and preset change. " +
|
|
1023
|
-
"Set the enable flag, MIDI channel, CC number,
|
|
1149
|
+
"Set the enable flag, MIDI channel, CC number, source (Program Change or CC), or bar-sync for each target. " +
|
|
1150
|
+
"One field per call.",
|
|
1024
1151
|
};
|
|
1025
1152
|
const MFX_FIELD_DESCRIPTIONS = {
|
|
1026
1153
|
capture_ui: { gridSize: "Cell grid size to set before capture: 1 or 4." },
|
|
@@ -1029,7 +1156,7 @@ const MFX_FIELD_DESCRIPTIONS = {
|
|
|
1029
1156
|
},
|
|
1030
1157
|
set_midi_mapping: {
|
|
1031
1158
|
target: "Which action's mapping to configure.",
|
|
1032
|
-
field: "Field to set. 'source'
|
|
1159
|
+
field: "Field to set. 'source' (0 = Program Change, 1 = CC) applies to preset and variation; 'barSync' is variation-only.",
|
|
1033
1160
|
},
|
|
1034
1161
|
queue_variation: {
|
|
1035
1162
|
fadeMs: "Crossfade duration in ms. Omit all transition options to inherit the native Bar Sync settings.",
|
|
@@ -1112,6 +1239,202 @@ function mfxToolConfig(name, config) {
|
|
|
1112
1239
|
...(inputSchema ? { inputSchema } : {}),
|
|
1113
1240
|
};
|
|
1114
1241
|
}
|
|
1242
|
+
// MSQ's launcher advertises the pattern layer only. The sampler, effect, master, looper, video,
|
|
1243
|
+
// plugin-host, AI and MFX handlers live in this file too; discovery must not offer them.
|
|
1244
|
+
const MSQ_TOOL_NAMES = new Set([
|
|
1245
|
+
"get_parameter_info", "list_presets", "read_preset", "get_manifest_summary", "variation_distinctness",
|
|
1246
|
+
"get_state", "get_design_overview", "connection_status", "situation", "select_instance", "get_default_locations",
|
|
1247
|
+
"capture_ui", "set_view", "set_parameters", "trigger_midi", "trigger_midi_cc", "trigger_midi_program_change",
|
|
1248
|
+
"stop_cell", "reset_cell", "copy_cell", "update_cell_config", "set_cell_mute", "update_pattern", "reset_patterns",
|
|
1249
|
+
"randomize", "randomize_patterns", "list_players", "set_player", "keep_player", "add_modulation",
|
|
1250
|
+
"update_modulation", "clear_modulations", "add_param_seq", "update_param_seq", "remove_param_seq",
|
|
1251
|
+
"set_free_sequencer", "start_transport", "stop_transport", "full_stop_transport", "get_transport", "set_bpm",
|
|
1252
|
+
"configure_transport", "load_preset", "get_preset_load_status", "new_preset", "save_preset",
|
|
1253
|
+
"update_preset_metadata", "export_preset", "import_preset", "delete_preset", "set_preset_factory_status",
|
|
1254
|
+
"load_variation", "save_variation", "queue_variation", "cancel_queued_variation", "update_variations",
|
|
1255
|
+
"variation_hold", "export_midi", "midi_recording", "update_settings", "get_audio_device_setup",
|
|
1256
|
+
"set_audio_device", "set_midi_io", "set_midi_mapping",
|
|
1257
|
+
]);
|
|
1258
|
+
// No audio means no envelope follower; the looper, Song Mode, resample and VX mappings have no MSQ surface.
|
|
1259
|
+
const MSQ_ENUM_DROPS = {
|
|
1260
|
+
update_pattern: { type: ["pitch", "slice", "stretch", "paramLane1", "paramLane2", "paramLane3", "paramLane4"] },
|
|
1261
|
+
add_modulation: { type: ["envelope"] },
|
|
1262
|
+
update_modulation: { type: ["envelope"] },
|
|
1263
|
+
get_state: { section: ["envelopes", "loopers", "songMode"] },
|
|
1264
|
+
set_midi_mapping: { target: ["looper", "resample", "vxRecord", "looperVariation"] },
|
|
1265
|
+
set_view: { fullscreen: ["effects"] },
|
|
1266
|
+
};
|
|
1267
|
+
const MSQ_KEPT_FIELDS = {
|
|
1268
|
+
update_cell_config: ["cellId", ...MSQ_CELL_CONFIG_KEYS],
|
|
1269
|
+
};
|
|
1270
|
+
const MSQ_DROPPED_FIELDS = {
|
|
1271
|
+
select_instance: ["product"],
|
|
1272
|
+
capture_ui: ["instrumentView"],
|
|
1273
|
+
get_parameter_info: ["effect"],
|
|
1274
|
+
add_modulation: ["pitch", "pitchDuck"],
|
|
1275
|
+
// Without a lane runtime every queued flip lands hard at its boundary: nothing to morph or crossfade.
|
|
1276
|
+
queue_variation: ["fadeMs", "morphBars", "morphMode", "morphCurve", "transition"],
|
|
1277
|
+
};
|
|
1278
|
+
const MSQ_FIELD_SCHEMAS = {
|
|
1279
|
+
randomize: {
|
|
1280
|
+
target: z
|
|
1281
|
+
.enum(["sequences", "lfos", "mods"])
|
|
1282
|
+
.default("sequences")
|
|
1283
|
+
.describe("What to randomize: the cells' note patterns, or the global LFOs / modulation links."),
|
|
1284
|
+
},
|
|
1285
|
+
set_view: {
|
|
1286
|
+
panel: z
|
|
1287
|
+
.enum(["pattern", "player", "midi"])
|
|
1288
|
+
.optional()
|
|
1289
|
+
.describe("Cell card tab to show, on cellId or else every cell: pattern, player or midi."),
|
|
1290
|
+
},
|
|
1291
|
+
};
|
|
1292
|
+
const MSQ_TOOL_DESCRIPTIONS = {
|
|
1293
|
+
select_instance: "Choose which running MSQ instance the live tools drive. Match by instanceId or port from connection_status. The launcher refuses other products' instances.",
|
|
1294
|
+
get_design_overview: "Return a compact snapshot of what each cell sends: Send, output channel (Auto = cell number), port, trigger " +
|
|
1295
|
+
"note, poly range, Player, note and CC-lane event counts, CC out slots and modulation link counts, plus BPM and " +
|
|
1296
|
+
"transport. Use this instead of full get_state.",
|
|
1297
|
+
randomize: "Trigger MSQ's randomizer. target:'sequences' writes new note patterns natively; 'lfos'/'mods' reshape the " +
|
|
1298
|
+
"global modulation and need MSQ's window open. Returns queued, not completed: poll get_state for the requested " +
|
|
1299
|
+
"changes before another randomize, edit, or save. A preset or variation load can discard pending work.",
|
|
1300
|
+
randomize_patterns: "Generate new note patterns with MSQ's native pattern engine from the genre, BPM and each cell's role. " +
|
|
1301
|
+
"Identical to randomize target:'sequences'. Returns queued: poll get_state for changed cellN_midiSequence " +
|
|
1302
|
+
"values before another randomize, edit, or save.",
|
|
1303
|
+
export_preset: "Export a preset as a zip file written to MSQ's Exports folder as <outputFolderName>.zip. An existing folder " +
|
|
1304
|
+
"or zip of that name is replaced, and presets belonging to a preset pack are refused.",
|
|
1305
|
+
update_settings: "Merge a partial settings delta into MSQ's plugin settings (the Settings panel state) and apply it live. " +
|
|
1306
|
+
"Returns the full merged settings object, so call it with {} to discover the keys; experimentalMode and " +
|
|
1307
|
+
"mcpBridgeEnabled are among them (mcpBridgeEnabled:false closes this bridge after it replies).",
|
|
1308
|
+
set_view: "Navigate MSQ's UI: switch the main view, focus a cell, or set the pad-grid size. Routed through the " +
|
|
1309
|
+
"WebView, so MSQ's window must be OPEN.",
|
|
1310
|
+
capture_ui: "Render MSQ's plugin UI (#app_root) to a PNG via html2canvas and return the saved file path (read it to view " +
|
|
1311
|
+
"the image). Optionally compose the view first: gridSize (1|4|8), mainView (CellGrid|CtrlAll|LFO|Vary|" +
|
|
1312
|
+
"ParamSeq|Mixer|PresetBrowser; omit/null = cell grid). REQUIRES MSQ's window OPEN: html2canvas needs the live " +
|
|
1313
|
+
"DOM. View arguments switch the live editor and the previous view is not restored.",
|
|
1314
|
+
copy_cell: "Copy a cell's manifest parameters and remap its LFO and vary links onto another cell, overwriting the " +
|
|
1315
|
+
"target's. A CC lane lands only on a target slot that is Off or on the same controller; CC out values and " +
|
|
1316
|
+
"controllers, output channel, Send and port stay the target's. Not copied: parameter sequencers and the cell " +
|
|
1317
|
+
"config block (poly, trigger note, scale), which update_cell_config sets.",
|
|
1318
|
+
reset_cell: "Reset a single cell's manifest parameters to their defaults. Untouched: pattern lanes (reset_patterns), the " +
|
|
1319
|
+
"cell config block (update_cell_config) and modulation targeting the cell from elsewhere (clear_modulations). " +
|
|
1320
|
+
"Does not touch other cells.",
|
|
1321
|
+
update_cell_config: "Set one MSQ cell's MIDI config: trigger note, poly and scale, output channel (0 = Auto), Send, standalone " +
|
|
1322
|
+
"port, the CC out slots' controllers and the CC lanes' slots.",
|
|
1323
|
+
get_manifest_summary: "Get a high-level summary of MSQ's capabilities: number of cells, modulators, and parameter counts.",
|
|
1324
|
+
add_modulation: "Add a modulation link (LFO or vary) to a parameter. The parameter must be modulatable (check with " +
|
|
1325
|
+
"get_parameter_info).",
|
|
1326
|
+
update_modulation: "Apply a delta to a modulation source: LFO, Vary, Macro, ParamSeq config, ParamSeq sequence data, or the Free " +
|
|
1327
|
+
"Sequencer master pattern. Use add_modulation to first attach a source to a target, then this tool to edit it.",
|
|
1328
|
+
clear_modulations: "Remove all LFO and vary modulation links targeting a cell's parameters (or driven by that cell, for vary). " +
|
|
1329
|
+
"Parameter sequencers survive; use remove_param_seq for those.",
|
|
1330
|
+
load_variation: "Request a switch to a saved variation slot (0-7), including parameters, patterns, players, cell wiring and " +
|
|
1331
|
+
"modulation. Empty slots are rejected. Verify currentVariation and a slot-specific value with get_state before " +
|
|
1332
|
+
"editing or saving.",
|
|
1333
|
+
new_preset: "Reset MSQ to its initial/default state, equivalent to creating a new empty preset. All cells, modulation and " +
|
|
1334
|
+
"sequences are cleared.",
|
|
1335
|
+
set_cell_mute: "Mute, unmute, solo, or unsolo a cell. A muted cell, or every other cell while one is soloed, sends no notes " +
|
|
1336
|
+
"or CC and releases the notes it holds. cell{N}_mute / cell{N}_solo are live-polled state variables.",
|
|
1337
|
+
read_preset: "Read an MSQ preset file and return its state: parameters, patterns, cell config and variations.",
|
|
1338
|
+
variation_distinctness: "Measure whether a preset's variation slots are actually different, and name what is identical about the " +
|
|
1339
|
+
"pairs that are not. Structural comparison of the saved slot snapshots: no render, no transport, no audition " +
|
|
1340
|
+
"loop, and no risk of the load_variation race. Each failing pair reports the dimensions it duplicates plus the " +
|
|
1341
|
+
"move that would fix it.",
|
|
1342
|
+
set_parameters: "Set one or more MSQ parameters. Values are validated against the manifest; get_parameter_info lists valid " +
|
|
1343
|
+
"IDs and ranges. Writing a macro_* id also enables that macro, matching the UI.",
|
|
1344
|
+
update_pattern: "Write one lane of a cell's pattern: the notes, or a CC lane (ccOut1-ccOut4, the timeline of that CC out " +
|
|
1345
|
+
"slot: each event sends its value on the slot's controller for its duration, then the slot returns to its knob).",
|
|
1346
|
+
save_variation: "Capture the current plugin state and save it to a variation slot (0-7). Use this to build presets with " +
|
|
1347
|
+
"multiple variations, e.g. a build-up from sparse to full. Set patterns, players, parameters and modulation " +
|
|
1348
|
+
"first, then snapshot them into a slot. The preset must have been saved once via save_preset, or pass a name " +
|
|
1349
|
+
"to create it.",
|
|
1350
|
+
situation: "One-call orientation snapshot: discovered instances, the connected instance's product/pid/executable (plus " +
|
|
1351
|
+
"build mtime and a dev-build heuristic when resolvable), server and manifest identity, active preset, " +
|
|
1352
|
+
"transport, and what each cell sends. Start a session here; get_design_overview reads deeper.",
|
|
1353
|
+
get_default_locations: "Get the standard folders MSQ knows (home, desktop, documents, music, downloads, presets), for an export_midi " +
|
|
1354
|
+
"outputDir or an import_preset path.",
|
|
1355
|
+
import_preset: "Import a preset from a zip file or folder into MSQ's preset library. Returns the relative path of the first " +
|
|
1356
|
+
"imported preset, or an empty string on failure. A name already in the library is never overwritten: the " +
|
|
1357
|
+
"import lands as <name>_1, <name>_2 and so on.",
|
|
1358
|
+
reset_patterns: "Clear every pattern lane of one cell: the note lane and the four CC lanes. CC out controllers stay.",
|
|
1359
|
+
queue_variation: "Schedule a global variation switch on the next bar boundary; for an immediate switch use load_variation. " +
|
|
1360
|
+
"MSQ switches at the boundary without a morph.",
|
|
1361
|
+
trigger_midi_cc: "Send a MIDI Control Change message into MSQ's MIDI router. Maps to any MIDI-learned parameter or variation " +
|
|
1362
|
+
"CC binding.",
|
|
1363
|
+
set_midi_mapping: "Configure the global MIDI-controller mappings that trigger MSQ actions: variation switch and preset change. " +
|
|
1364
|
+
"Set the enable flag, MIDI channel, CC number, source (Program Change or CC), or bar-sync for each target. " +
|
|
1365
|
+
"One field per call.",
|
|
1366
|
+
export_midi: "Write the cells' note patterns to a Standard MIDI File (type 1, 480 PPQ, tempo from the transport): one track " +
|
|
1367
|
+
"per cell, across `bars`, on the cell's output channel (Auto = cell number). Each sending CC out slot's value " +
|
|
1368
|
+
"opens the track at tick 0 and its CC lane's locks and releases follow on the lane's own clock; a cell with Send " +
|
|
1369
|
+
"off is left out, as are notes outside a poly cell's Low-High. A Player cell exports the cycles it generates, " +
|
|
1370
|
+
"rendered by MSQ itself; Listen responses are left out. Grid timing from base values: swing, probability, CTRL " +
|
|
1371
|
+
"macros and modulators are not applied. Nothing changes in MSQ. Default location is MSQ's Export folder.",
|
|
1372
|
+
};
|
|
1373
|
+
const MSQ_FIELD_DESCRIPTIONS = {
|
|
1374
|
+
get_parameter_info: { paramId: "Specific parameter ID (e.g. 'cell1_patternSwing'). Omit for all." },
|
|
1375
|
+
get_state: { keys: "Exact top-level state keys to return, e.g. ['cell1_patternSwing','cell1_midiSequence']." },
|
|
1376
|
+
add_modulation: {
|
|
1377
|
+
paramId: "Target parameter ID (e.g. 'cell1_ccOut1').",
|
|
1378
|
+
cellId: "Required for vary: the cell whose triggers drive it (1-8). The target cell is inferred from paramId.",
|
|
1379
|
+
},
|
|
1380
|
+
add_param_seq: { paramId: "Target parameter ID (e.g. 'cell1_ccOut1')." },
|
|
1381
|
+
update_modulation: {
|
|
1382
|
+
payload: "Update payload object, one property per call. lfo, vary, macro and paramSeq all take { index, property, " +
|
|
1383
|
+
"value }, where index is the 0-based position in that state array (get_state section 'lfos' / 'vary' / " +
|
|
1384
|
+
"'macros' / 'paramSeqs') and property is the field name to overwrite. paramSeqSequence takes { index, " +
|
|
1385
|
+
"sequence: number[] }. freeSeqSequence takes { sequence: number[] }, the master step contour every " +
|
|
1386
|
+
"free-mode ParamSeq follows.",
|
|
1387
|
+
},
|
|
1388
|
+
set_parameters: { parameters: "Map of paramId to value. Example: { 'cell1_ccOut1': 100, 'cell2_patternSwing': 0.2 }" },
|
|
1389
|
+
update_pattern: {
|
|
1390
|
+
sequence: "Array of sequence events. For 'note': [{time, note, duration, velocity, prob}]. For CC lanes: " +
|
|
1391
|
+
"[{time, value, duration, prob?}] (values 0-127; times and durations in beats).",
|
|
1392
|
+
},
|
|
1393
|
+
randomize: { genreHint: "Genre hint for the generated patterns (e.g. 'techno', 'hip-hop')." },
|
|
1394
|
+
save_preset: { image: "Relative path to a thumbnail image for the preset browser." },
|
|
1395
|
+
update_preset_metadata: { image: "Relative path to a thumbnail image for the preset browser." },
|
|
1396
|
+
update_cell_config: { polyRootNote: "Root note for poly pitch calculation." },
|
|
1397
|
+
variation_distinctness: { threshold: "Similarity above which a pair fails. Default 0.9." },
|
|
1398
|
+
set_midi_mapping: {
|
|
1399
|
+
target: "Which action's mapping to configure.",
|
|
1400
|
+
field: "Field to set. 'source' (0 = Program Change, 1 = CC) applies to preset and variation; 'barSync' is variation-only.",
|
|
1401
|
+
},
|
|
1402
|
+
set_view: {
|
|
1403
|
+
view: "Main view: cells, mixer, ctrl, lfo, vary, paramseq, presets.",
|
|
1404
|
+
fullscreen: "CTRL page rack to show alone (sequencer, players), or none.",
|
|
1405
|
+
},
|
|
1406
|
+
};
|
|
1407
|
+
function msqSurfaceText(value) {
|
|
1408
|
+
return value
|
|
1409
|
+
.replace(/PAM ?(?:≥|>=) ?\d+(?:\.\d+)*/g, "a current MSQ build")
|
|
1410
|
+
.replaceAll("a PAM build", "an MSQ build")
|
|
1411
|
+
.replaceAll("PAM", "MSQ");
|
|
1412
|
+
}
|
|
1413
|
+
function msqToolConfig(name, config) {
|
|
1414
|
+
const source = config;
|
|
1415
|
+
const kept = MSQ_KEPT_FIELDS[name];
|
|
1416
|
+
const droppedFields = MSQ_DROPPED_FIELDS[name] ?? [];
|
|
1417
|
+
const enumDrops = MSQ_ENUM_DROPS[name] ?? {};
|
|
1418
|
+
const fieldText = MSQ_FIELD_DESCRIPTIONS[name] ?? {};
|
|
1419
|
+
const fieldSchemas = MSQ_FIELD_SCHEMAS[name] ?? {};
|
|
1420
|
+
const inputSchema = source.inputSchema
|
|
1421
|
+
? Object.fromEntries(Object.entries(source.inputSchema)
|
|
1422
|
+
.filter(([key]) => (!kept || kept.includes(key)) && !droppedFields.includes(key))
|
|
1423
|
+
.map(([key, schema]) => {
|
|
1424
|
+
if (fieldSchemas[key])
|
|
1425
|
+
return [key, fieldSchemas[key]];
|
|
1426
|
+
const narrowed = enumDrops[key] ? rebuildSchema(schema, dropEnumMembers(enumDrops[key])) : schema;
|
|
1427
|
+
const described = fieldText[key] ?? (narrowed.description ? msqSurfaceText(narrowed.description) : undefined);
|
|
1428
|
+
return [key, described ? narrowed.describe(described) : narrowed];
|
|
1429
|
+
}))
|
|
1430
|
+
: undefined;
|
|
1431
|
+
const description = MSQ_TOOL_DESCRIPTIONS[name] ?? (source.description ? msqSurfaceText(source.description) : undefined);
|
|
1432
|
+
return {
|
|
1433
|
+
...source,
|
|
1434
|
+
...(description ? { description } : {}),
|
|
1435
|
+
...(inputSchema ? { inputSchema } : {}),
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1115
1438
|
{
|
|
1116
1439
|
const origRegisterTool = server.registerTool.bind(server);
|
|
1117
1440
|
server.registerTool = ((name, config, handler) => {
|
|
@@ -1119,13 +1442,17 @@ function mfxToolConfig(name, config) {
|
|
|
1119
1442
|
// tools therefore have no runtime handle to return.
|
|
1120
1443
|
if (IS_MFX_SERVER && !MFX_TOOL_NAMES.has(name))
|
|
1121
1444
|
return undefined;
|
|
1122
|
-
|
|
1445
|
+
if (IS_MSQ_SERVER && !MSQ_TOOL_NAMES.has(name))
|
|
1446
|
+
return undefined;
|
|
1447
|
+
const productConfig = (IS_MFX_SERVER ? mfxToolConfig(name, config) : IS_MSQ_SERVER ? msqToolConfig(name, config) : config);
|
|
1123
1448
|
const missing = MSQ_MISSING_BY_TOOL.get(name);
|
|
1124
1449
|
// Judged by the instance actually connected, not a manifest hint a previous session left behind.
|
|
1125
1450
|
const productHandler = missing
|
|
1126
|
-
? async (args) => (await requireBridge()) === true && bridge.getConnectedInstance()?.product?.toUpperCase() === "MSQ"
|
|
1127
|
-
?
|
|
1128
|
-
|
|
1451
|
+
? async (args, extra) => (await requireBridge()) === true && bridge.getConnectedInstance()?.product?.toUpperCase() === "MSQ"
|
|
1452
|
+
? MSQ_MISSING_CODES.has(name)
|
|
1453
|
+
? packFailure(MSQ_MISSING_CODES.get(name), `${name} is not available in MSQ: it has no ${missing}.`)
|
|
1454
|
+
: error(`${name} is not available in MSQ: it has no ${missing}.`)
|
|
1455
|
+
: handler(args, extra)
|
|
1129
1456
|
: handler;
|
|
1130
1457
|
internalTools.set(name, { shape: productConfig.inputSchema, handler: productHandler });
|
|
1131
1458
|
// Merge the central annotation policy. Inline annotations at the call site
|
|
@@ -1548,7 +1875,7 @@ server.registerTool("design_context", {
|
|
|
1548
1875
|
filterCutoff: "Use real Hz values; move at least 1.5 octaves for obviously audible modulation.",
|
|
1549
1876
|
delayDelayTime: "With delaySyncEnabled=1, normalized 0..1 maps to musical divisions; do not use cellN_delayTime.",
|
|
1550
1877
|
flanger: "cellN_flangerTime and cellN_flangerFreq are normalized 0..1, not ms/Hz. flangerFeedback is where the character lives — the corpus median is 70 and the top quartile is at 95+.",
|
|
1551
|
-
eqGains: "cellN_eq{Low,Mid,High}Gain are -60..12 dB. The -60 floor
|
|
1878
|
+
eqGains: "cellN_eq{Low,Mid,High}Gain are -60..12 dB. The -60 floor cuts the band fully, not just a tilt.",
|
|
1552
1879
|
lfoRate: "LFO IDs are 1-based: lfo1_... through lfo6_.... Sync ON uses index/22.",
|
|
1553
1880
|
lfoFeel: "lfoN_jitter and lfoN_offset (both 0..100, both default 0) are what stop an LFO sounding like a metronome — jitter randomizes the period, offset moves the starting phase so two LFOs at the same rate stop moving in lockstep. Only 8% and 6% of shipped LFOs set them.",
|
|
1554
1881
|
granular: "The granular engine runs under cellN_stretch. cellN_stretchGrainMs (1..2000) is grain size — small values buzz, 200+ smears into a pad; cellN_grainDensity (1..32) is grains in flight; cellN_grainScan (-100..100) moves the read head independently of playback, so a negative scan plays a sample backwards through itself; cellN_grainFeedback (0..100) recirculates grains; cellN_grainTone (-100..100) tilts them; cellN_grainWindow (0..4) is the grain envelope shape.",
|
|
@@ -1605,7 +1932,7 @@ server.registerTool("design_context", {
|
|
|
1605
1932
|
})),
|
|
1606
1933
|
variationSlots: "API is 0-based (0..7); PAM UI labels slots 1..8. Save/load variation index 0 = UI slot 1.",
|
|
1607
1934
|
generativePlayers: "set_player turns a cell into a live generator (list_players for the roster: EUCLID, KIT, TURING, WALK, MARKOV, " +
|
|
1608
|
-
"RATCHET, PHASE, RIFF, MASK). cell{N}_playerDensity/Mutate/P1..P5 are modulatable params with macro_player* twins; " +
|
|
1935
|
+
"RATCHET, PHASE, RIFF, MASK, PERC). cell{N}_playerDensity/Mutate/P1..P5 are modulatable params with macro_player* twins; " +
|
|
1609
1936
|
"write them through set_player, never set_parameters alone, so the cell config stays in step.",
|
|
1610
1937
|
modulationGuidance: "Primary movement should target per-cell params for distinct audible changes. Use macro_* for performance-level fan-out across cells. " +
|
|
1611
1938
|
"Shipped presets that modulate at all carry a mean of 8.2 sources; the top third carry 8-46. Four sources is the corpus MEDIAN, i.e. the unremarkable middle — not a target.",
|
|
@@ -1735,7 +2062,7 @@ server.registerTool("analyze_motion", {
|
|
|
1735
2062
|
return error("This build did not report the render's output path. Re-run with an explicit wavPath.");
|
|
1736
2063
|
const outcome = await pollExportStatus();
|
|
1737
2064
|
if (outcome !== "success")
|
|
1738
|
-
return error(`Render failed before analysis could run (export ${outcome})
|
|
2065
|
+
return error(`Render failed before analysis could run (export ${outcome}).${exportFailureDetail()}`);
|
|
1739
2066
|
path = outputPath;
|
|
1740
2067
|
const renderedSeconds = isRecord(started) ? Number(started.durationInSeconds) : NaN;
|
|
1741
2068
|
barSeconds = renderedSeconds > 0 ? renderedSeconds / bars : undefined;
|
|
@@ -1967,6 +2294,29 @@ server.registerTool("get_design_overview", {
|
|
|
1967
2294
|
vary: Array.isArray(state.vary) ? state.vary : [],
|
|
1968
2295
|
paramSeq: Array.isArray(state.paramSeqs) ? state.paramSeqs : [],
|
|
1969
2296
|
};
|
|
2297
|
+
if (isMsqManifest(manifest)) {
|
|
2298
|
+
const cells = ids.map((id) => {
|
|
2299
|
+
const rawCell = cellsObj[String(id)];
|
|
2300
|
+
const targets = new Set([`cell${id}_`]);
|
|
2301
|
+
return {
|
|
2302
|
+
...msqCellSummary(id, isRecord(rawCell) ? rawCell : {}, state),
|
|
2303
|
+
modulation: Object.fromEntries(Object.entries(modArrays).map(([kind, arr]) => [kind, arr.filter((entry) => paramTargetsCell(entry, targets)).length])),
|
|
2304
|
+
};
|
|
2305
|
+
});
|
|
2306
|
+
return json({
|
|
2307
|
+
product: manifest.name,
|
|
2308
|
+
bpm: state.bpm ?? transport.bpm,
|
|
2309
|
+
playing: transport.isPlaying,
|
|
2310
|
+
totals: {
|
|
2311
|
+
cellsSending: cells.filter((cell) => cell.send && !cell.muted).length,
|
|
2312
|
+
playersRunning: cells.filter((cell) => cell.player).length,
|
|
2313
|
+
noteEvents: cells.reduce((sum, cell) => sum + cell.noteEvents, 0),
|
|
2314
|
+
ccSlots: cells.reduce((sum, cell) => sum + cell.ccOut.length, 0),
|
|
2315
|
+
modulationLinks: cells.reduce((sum, cell) => sum + Object.values(cell.modulation).reduce((subtotal, count) => subtotal + count, 0), 0),
|
|
2316
|
+
},
|
|
2317
|
+
cells,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
1970
2320
|
if (IS_MFX_SERVER) {
|
|
1971
2321
|
const cells = ids.map((id) => {
|
|
1972
2322
|
const prefix = `cell${id}_`;
|
|
@@ -1996,6 +2346,7 @@ server.registerTool("get_design_overview", {
|
|
|
1996
2346
|
bpm: state.bpm ?? transport.bpm,
|
|
1997
2347
|
playing: transport.isPlaying,
|
|
1998
2348
|
mix: state.mfx_mix,
|
|
2349
|
+
outputGain: state.mfx_outputGain,
|
|
1999
2350
|
routing: state.mfx_routing,
|
|
2000
2351
|
edgeGains: state.mfx_edge_gains,
|
|
2001
2352
|
split: { lowHz: state.mfx_split_lo_hz, highHz: state.mfx_split_hi_hz },
|
|
@@ -2180,9 +2531,25 @@ const MODEL_VARIANT = z
|
|
|
2180
2531
|
.describe("Magenta only: which variant (base = 2.4B, needs ~M2 Max/M3 Pro+; small = 230M, default). Ignored for notochord/aigen (single model).");
|
|
2181
2532
|
server.registerTool("download_model", {
|
|
2182
2533
|
title: "Download AI Model",
|
|
2183
|
-
description: "Start (or resume) downloading model weights for an AI engine.
|
|
2534
|
+
description: "Start (or resume) downloading model weights for an AI engine. Returns at once with the download's progress (in a pam mcp session, the daemon job's jobId); poll get_model_status. Downloads through PAM need the engine's experimental flags, and aigen + notochord then auto-load on completion; magenta and pam mcp session downloads need a separate load_model.",
|
|
2184
2535
|
inputSchema: { engine: MODEL_ENGINE, model: MODEL_VARIANT },
|
|
2185
2536
|
}, async ({ engine, model }) => {
|
|
2537
|
+
if (DAEMON_SESSION && engine !== "notochord") {
|
|
2538
|
+
try {
|
|
2539
|
+
const running = daemonDownloads.get(engine);
|
|
2540
|
+
if (running?.model === daemonModel(engine, model)) {
|
|
2541
|
+
const job = await daemon.downloadStatus(running).catch(() => undefined);
|
|
2542
|
+
if (job?.inFlight)
|
|
2543
|
+
return json({ ok: true, ...job });
|
|
2544
|
+
}
|
|
2545
|
+
const download = await daemon.startDownload(daemonModel(engine, model));
|
|
2546
|
+
daemonDownloads.set(engine, download);
|
|
2547
|
+
return json(download);
|
|
2548
|
+
}
|
|
2549
|
+
catch (cause) {
|
|
2550
|
+
return daemonFailure(cause);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2186
2553
|
const check = await requireBridge();
|
|
2187
2554
|
if (check !== true)
|
|
2188
2555
|
return check;
|
|
@@ -2201,8 +2568,30 @@ server.registerTool("get_model_status", {
|
|
|
2201
2568
|
version: z.string().optional(),
|
|
2202
2569
|
state: z.string().optional(),
|
|
2203
2570
|
progress: z.number().optional(),
|
|
2571
|
+
jobId: z.string().optional(),
|
|
2572
|
+
daemonEpoch: z.string().optional(),
|
|
2204
2573
|
},
|
|
2205
2574
|
}, async ({ engine }) => {
|
|
2575
|
+
const download = DAEMON_SESSION ? daemonDownloads.get(engine) : undefined;
|
|
2576
|
+
let job;
|
|
2577
|
+
if (download) {
|
|
2578
|
+
try {
|
|
2579
|
+
job = await daemon.downloadStatus(download);
|
|
2580
|
+
}
|
|
2581
|
+
catch (cause) {
|
|
2582
|
+
// A job pamd no longer knows stops shadowing the plugin's own status.
|
|
2583
|
+
if (!LOST_DAEMON_JOB.has(daemonFailureCode(cause) ?? ""))
|
|
2584
|
+
return daemonFailure(cause);
|
|
2585
|
+
daemonDownloads.delete(engine);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (job) {
|
|
2589
|
+
const plugin = bridge.isConnected() || await bridge.connect()
|
|
2590
|
+
? await bridge.send("modelStatus", { engine }).then((raw) => typeof raw === "string" ? JSON.parse(raw) : raw).catch(() => undefined)
|
|
2591
|
+
: undefined;
|
|
2592
|
+
const live = isRecord(plugin) ? plugin : { engine };
|
|
2593
|
+
return structured(job.inFlight ? { ...live, ...job } : { ...job, ...live });
|
|
2594
|
+
}
|
|
2206
2595
|
const check = await requireBridge();
|
|
2207
2596
|
if (check !== true)
|
|
2208
2597
|
return check;
|
|
@@ -2230,9 +2619,33 @@ server.registerTool("unload_model", {
|
|
|
2230
2619
|
});
|
|
2231
2620
|
server.registerTool("delete_model", {
|
|
2232
2621
|
title: "Delete AI Model",
|
|
2233
|
-
description: "Delete downloaded model weights from disk. Unloads first automatically. DESTRUCTIVE — re-download to restore. magenta
|
|
2622
|
+
description: "Delete downloaded model weights from disk. Unloads first automatically. DESTRUCTIVE — re-download to restore. For magenta, model (small|base) removes that variant and keeps the resources the other installed variant shares; omit model to remove both variants and the shared resources.",
|
|
2234
2623
|
inputSchema: { engine: MODEL_ENGINE, model: MODEL_VARIANT },
|
|
2235
|
-
}, async ({ engine, model }) => {
|
|
2624
|
+
}, async ({ engine, model }, extra) => {
|
|
2625
|
+
if (DAEMON_SESSION && engine !== "notochord") {
|
|
2626
|
+
const models = engine === "magenta" && !model ? ["mrt2-small", "mrt2-base"] : [daemonModel(engine, model)];
|
|
2627
|
+
try {
|
|
2628
|
+
if (bridge.isConnected() || await bridge.connect()) {
|
|
2629
|
+
try {
|
|
2630
|
+
await bridge.send("modelDelete", { engine, model });
|
|
2631
|
+
}
|
|
2632
|
+
catch (cause) {
|
|
2633
|
+
const name = engine === "aigen" ? "AIGen" : "Magenta";
|
|
2634
|
+
if (!(cause instanceof Error) || !cause.message.startsWith(`[modelDelete] ${name} disabled —`))
|
|
2635
|
+
throw cause;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
const results = [];
|
|
2639
|
+
for (const id of models)
|
|
2640
|
+
results.push(await daemon.remove(id, extra?.signal));
|
|
2641
|
+
if (models.includes(daemonDownloads.get(engine)?.model ?? ""))
|
|
2642
|
+
daemonDownloads.delete(engine);
|
|
2643
|
+
return json(results);
|
|
2644
|
+
}
|
|
2645
|
+
catch (cause) {
|
|
2646
|
+
return daemonFailure(cause);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2236
2649
|
const check = await requireBridge();
|
|
2237
2650
|
if (check !== true)
|
|
2238
2651
|
return check;
|
|
@@ -2256,9 +2669,10 @@ server.registerTool("generate_sample", {
|
|
|
2256
2669
|
.enum(["kick", "snare", "hat", "perc", "bass", "synth", "pad", "fx", "loop"])
|
|
2257
2670
|
.describe("Sound category — drives prompt template defaults."),
|
|
2258
2671
|
prompt: z.string().describe("Text prompt describing the desired sound."),
|
|
2672
|
+
// The plugin renders Small SFX: OnnxAIGenEngine::kMinDurationSeconds and its Sa3Layout maximum.
|
|
2259
2673
|
durationSeconds: z
|
|
2260
2674
|
.number()
|
|
2261
|
-
.min(0.
|
|
2675
|
+
.min(0.05)
|
|
2262
2676
|
.max(120)
|
|
2263
2677
|
.describe("Output length in seconds (the model's window is ~120 s). For loops, pass bar-converted duration: bars * beatsPerBar * 60 / bpm."),
|
|
2264
2678
|
cellId: z
|
|
@@ -2268,18 +2682,27 @@ server.registerTool("generate_sample", {
|
|
|
2268
2682
|
.max(8)
|
|
2269
2683
|
.optional()
|
|
2270
2684
|
.describe("If provided, load result directly into this cell."),
|
|
2271
|
-
seed: z
|
|
2685
|
+
seed: z
|
|
2686
|
+
.number()
|
|
2687
|
+
.int()
|
|
2688
|
+
.min(0)
|
|
2689
|
+
.max(4294967295)
|
|
2690
|
+
.optional()
|
|
2691
|
+
.describe("Deterministic seed. Random if omitted or 0; the result's seed reproduces the take."),
|
|
2272
2692
|
},
|
|
2273
|
-
}, async (args) => {
|
|
2693
|
+
}, async (args, extra) => {
|
|
2274
2694
|
const ok = await requireBridge();
|
|
2275
2695
|
if (ok !== true)
|
|
2276
2696
|
return ok;
|
|
2277
2697
|
try {
|
|
2278
|
-
const
|
|
2698
|
+
const cells = await requireBridgeForCells(args.cellId);
|
|
2699
|
+
if (cells !== true)
|
|
2700
|
+
return cells;
|
|
2701
|
+
const result = DAEMON_SESSION ? await pinnedGeneration(args, undefined, extra?.signal) : await bridge.send("aiGenerate", args);
|
|
2279
2702
|
return json(result);
|
|
2280
2703
|
}
|
|
2281
2704
|
catch (e) {
|
|
2282
|
-
return error(`AI generation failed: ${e.message}`);
|
|
2705
|
+
return DAEMON_SESSION ? daemonFailure(e, "AI generation failed: ") : error(`AI generation failed: ${e.message}`);
|
|
2283
2706
|
}
|
|
2284
2707
|
});
|
|
2285
2708
|
server.registerTool("generate_kit", {
|
|
@@ -2296,7 +2719,7 @@ server.registerTool("generate_kit", {
|
|
|
2296
2719
|
.max(8)
|
|
2297
2720
|
.describe("Cells to populate. Each gets one sample matching the role."),
|
|
2298
2721
|
},
|
|
2299
|
-
}, async (args) => {
|
|
2722
|
+
}, async (args, extra) => {
|
|
2300
2723
|
const ok = await requireBridge();
|
|
2301
2724
|
if (ok !== true)
|
|
2302
2725
|
return ok;
|
|
@@ -2312,11 +2735,29 @@ server.registerTool("generate_kit", {
|
|
|
2312
2735
|
durationSeconds: durationForRole(c.role),
|
|
2313
2736
|
}));
|
|
2314
2737
|
try {
|
|
2738
|
+
if (DAEMON_SESSION) {
|
|
2739
|
+
const check = await requireBridgeForCells(...cells.map((cell) => cell.id));
|
|
2740
|
+
if (check !== true)
|
|
2741
|
+
return check;
|
|
2742
|
+
const instance = bridge.getConnectedInstance();
|
|
2743
|
+
const destination = new PluginBridge();
|
|
2744
|
+
if (!instance || !await destination.connect({ instanceId: instance.instanceId, product: instance.product }))
|
|
2745
|
+
return error("plugin_unavailable: selected instance disconnected");
|
|
2746
|
+
try {
|
|
2747
|
+
const results = [];
|
|
2748
|
+
for (const request of requests)
|
|
2749
|
+
results.push(await pinnedGeneration(request, destination, extra?.signal));
|
|
2750
|
+
return json(results);
|
|
2751
|
+
}
|
|
2752
|
+
finally {
|
|
2753
|
+
destination.disconnect();
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2315
2756
|
const results = await bridge.send("aiGenerateKit", { requests });
|
|
2316
2757
|
return json(results);
|
|
2317
2758
|
}
|
|
2318
2759
|
catch (e) {
|
|
2319
|
-
return error(`Kit generation failed: ${e.message}`);
|
|
2760
|
+
return DAEMON_SESSION ? daemonFailure(e, "Kit generation failed: ") : error(`Kit generation failed: ${e.message}`);
|
|
2320
2761
|
}
|
|
2321
2762
|
});
|
|
2322
2763
|
// --- AI Audio Workbench (Phase A, dev-gated) ---------------------------------
|
|
@@ -2578,7 +3019,8 @@ server.registerTool("add_modulation", {
|
|
|
2578
3019
|
return check;
|
|
2579
3020
|
// Compute effect-enabled params to auto-enable disabled effects (mirrors UI behavior)
|
|
2580
3021
|
const effectEnableParams = {};
|
|
2581
|
-
|
|
3022
|
+
// Only cells have per-effect flags; native enables a macro target itself (MCPBridge::handleAddModulation).
|
|
3023
|
+
if (param.effect && /^cell\d+$/.test(param.cell ?? "")) {
|
|
2582
3024
|
effectEnableParams[`${param.cell}_${param.effect}Enabled`] = 1;
|
|
2583
3025
|
}
|
|
2584
3026
|
if (type === "lfo") {
|
|
@@ -2952,12 +3394,14 @@ server.registerTool("full_stop_transport", {
|
|
|
2952
3394
|
});
|
|
2953
3395
|
server.registerTool("get_transport", {
|
|
2954
3396
|
title: "Get Transport",
|
|
2955
|
-
description: "Get current transport state (BPM, playing, position).",
|
|
3397
|
+
description: "Get current transport state (BPM, playing, position, meter).",
|
|
2956
3398
|
inputSchema: {},
|
|
2957
3399
|
outputSchema: {
|
|
2958
3400
|
bpm: z.number().optional(),
|
|
2959
3401
|
isPlaying: z.boolean().optional(),
|
|
2960
3402
|
ppqPosition: z.number().optional(),
|
|
3403
|
+
timeSigNumerator: z.number().optional(),
|
|
3404
|
+
timeSigDenominator: z.number().optional(),
|
|
2961
3405
|
},
|
|
2962
3406
|
annotations: { readOnlyHint: true },
|
|
2963
3407
|
}, async () => {
|
|
@@ -3449,15 +3893,15 @@ server.registerTool("connection_status", {
|
|
|
3449
3893
|
annotations: { readOnlyHint: true },
|
|
3450
3894
|
}, async () => {
|
|
3451
3895
|
const discoveredInstances = await bridge.discover();
|
|
3452
|
-
const instances =
|
|
3453
|
-
? discoveredInstances.filter((instance) => instance.product?.trim().toLowerCase() ===
|
|
3896
|
+
const instances = PINNED_PRODUCT
|
|
3897
|
+
? discoveredInstances.filter((instance) => instance.product?.trim().toLowerCase() === PINNED_PRODUCT)
|
|
3454
3898
|
: discoveredInstances;
|
|
3455
3899
|
if (instances.length === 0) {
|
|
3456
3900
|
return structured({
|
|
3457
3901
|
instances: [],
|
|
3458
3902
|
connected: false,
|
|
3459
3903
|
note: `No running ${SERVER_PRODUCT.name} instances found. To use live tools, ${SERVER_PRODUCT.name} needs the MCP bridge enabled: ` +
|
|
3460
|
-
`start ${SERVER_PRODUCT.name} (standalone or in a DAW) and enable it in Settings →
|
|
3904
|
+
`start ${SERVER_PRODUCT.name} (standalone or in a DAW) and enable it in Settings → Integrations → MCP Bridge. ` +
|
|
3461
3905
|
"Offline tools (get_parameter_info, list_presets, read_preset) work without a connection.",
|
|
3462
3906
|
});
|
|
3463
3907
|
}
|
|
@@ -3603,10 +4047,23 @@ server.registerTool("situation", {
|
|
|
3603
4047
|
.filter((cell) => typeof cell === "string" && /^cell\d+$/.test(cell))
|
|
3604
4048
|
.map((cell) => Number(cell.replace("cell", ""))))].sort((a, b) => a - b);
|
|
3605
4049
|
const cellsObj = isRecord(state.cells) ? state.cells : {};
|
|
4050
|
+
const msq = isMsqManifest(manifest);
|
|
3606
4051
|
const cells = ids.map((id) => {
|
|
3607
4052
|
const prefix = `cell${id}_`;
|
|
3608
4053
|
const rawCellState = cellsObj[String(id)];
|
|
3609
4054
|
const cellState = isRecord(rawCellState) ? rawCellState : {};
|
|
4055
|
+
if (msq) {
|
|
4056
|
+
const summary = msqCellSummary(id, cellState, state);
|
|
4057
|
+
return {
|
|
4058
|
+
cellId: id,
|
|
4059
|
+
muted: summary.muted,
|
|
4060
|
+
send: summary.send,
|
|
4061
|
+
channel: summary.channel,
|
|
4062
|
+
player: summary.player?.name,
|
|
4063
|
+
noteEvents: summary.noteEvents,
|
|
4064
|
+
ccSlots: summary.ccOut.length,
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
3610
4067
|
const samplePath = cellState.filePath ??
|
|
3611
4068
|
cellState.samplePath ??
|
|
3612
4069
|
state[`${prefix}filePath`] ??
|
|
@@ -3625,7 +4082,7 @@ server.registerTool("situation", {
|
|
|
3625
4082
|
bpm: state.bpm ?? transport.bpm,
|
|
3626
4083
|
playing: transport.isPlaying,
|
|
3627
4084
|
cells,
|
|
3628
|
-
...(IS_MFX_SERVER ? { mix: state.mfx_mix, routing: state.mfx_routing } : {}),
|
|
4085
|
+
...(IS_MFX_SERVER ? { mix: state.mfx_mix, outputGain: state.mfx_outputGain, routing: state.mfx_routing } : {}),
|
|
3629
4086
|
};
|
|
3630
4087
|
}
|
|
3631
4088
|
}
|
|
@@ -3644,13 +4101,13 @@ server.registerTool("situation", {
|
|
|
3644
4101
|
});
|
|
3645
4102
|
server.registerTool("select_instance", {
|
|
3646
4103
|
title: "Select Plugin Instance",
|
|
3647
|
-
description: "Choose which running plugin instance (PAM /
|
|
4104
|
+
description: "Choose which running plugin instance (PAM / MFX / MSQ) the live tools drive when more than " +
|
|
3648
4105
|
"one is running. Match by product name, instanceId, or port — connection_status lists them. " +
|
|
3649
4106
|
"Reconnects if currently connected to a different instance, and points offline parameter " +
|
|
3650
4107
|
"validation at the selected product's manifest. The PAM_BRIDGE_PRODUCT env var pins the same " +
|
|
3651
4108
|
"choice at server launch.",
|
|
3652
4109
|
inputSchema: {
|
|
3653
|
-
product: z.string().optional().describe("Product name: PAM,
|
|
4110
|
+
product: z.string().optional().describe("Product name: PAM, MFX, or MSQ (case-insensitive)."),
|
|
3654
4111
|
instanceId: z.string().optional().describe("Exact instanceId from connection_status."),
|
|
3655
4112
|
port: z.number().int().optional().describe("Exact TCP port from connection_status."),
|
|
3656
4113
|
},
|
|
@@ -3663,22 +4120,23 @@ server.registerTool("select_instance", {
|
|
|
3663
4120
|
if (!product && !instanceId && port === undefined) {
|
|
3664
4121
|
return error("Pass at least one of product, instanceId, or port. Call connection_status to list running instances.");
|
|
3665
4122
|
}
|
|
3666
|
-
if (
|
|
3667
|
-
return error(
|
|
4123
|
+
if (PINNED_PRODUCT && product && product.trim().toLowerCase() !== PINNED_PRODUCT) {
|
|
4124
|
+
return error(`The ${SERVER_PRODUCT.command} launcher can select ${SERVER_PRODUCT.name} instances only.`);
|
|
3668
4125
|
}
|
|
3669
|
-
const targetProduct =
|
|
4126
|
+
const targetProduct = PINNED_PRODUCT ? SERVER_PRODUCT.name : product;
|
|
3670
4127
|
const ok = await bridge.connect({ product: targetProduct, instanceId, port });
|
|
3671
4128
|
if (!ok) {
|
|
3672
4129
|
const discovered = await bridge.discover();
|
|
3673
|
-
const instances =
|
|
3674
|
-
? discovered.filter((instance) => instance.product?.trim().toLowerCase() ===
|
|
4130
|
+
const instances = PINNED_PRODUCT
|
|
4131
|
+
? discovered.filter((instance) => instance.product?.trim().toLowerCase() === PINNED_PRODUCT)
|
|
3675
4132
|
: discovered;
|
|
3676
4133
|
const listing = instances.length
|
|
3677
4134
|
? ` Running instances: ${instances
|
|
3678
4135
|
.map((i) => `${i.product} (port ${i.port}, id ${i.instanceId})`)
|
|
3679
4136
|
.join("; ")}.`
|
|
3680
4137
|
: " No running instances found.";
|
|
3681
|
-
|
|
4138
|
+
// pamd keys selection recovery on this code (PluginSession selectorNotFound); keep it with any rewording.
|
|
4139
|
+
return { ...error(`No running instance matches that selector.${listing}`), _meta: { code: "instance_not_found" } };
|
|
3682
4140
|
}
|
|
3683
4141
|
syncProductHint();
|
|
3684
4142
|
return structured({
|
|
@@ -3751,9 +4209,10 @@ async function defaultLocationPaths() {
|
|
|
3751
4209
|
.map((entry) => [entry.name, entry.path]));
|
|
3752
4210
|
}
|
|
3753
4211
|
// Native exports to <base>/Export beside <base>/Samples ("Plugin Samples"); check-parity pins both.
|
|
4212
|
+
// MSQ keeps no sample folder, so its Export resolves from the product's own base folder.
|
|
3754
4213
|
async function exportsFolder() {
|
|
3755
4214
|
const pluginSamples = (await defaultLocationPaths()).get("Plugin Samples");
|
|
3756
|
-
return pluginSamples ? join(dirname(pluginSamples), "Export") :
|
|
4215
|
+
return pluginSamples ? join(dirname(pluginSamples), "Export") : productExportDir();
|
|
3757
4216
|
}
|
|
3758
4217
|
async function collectLibraryByFolder(roots, maxDirReads = 120) {
|
|
3759
4218
|
const byFolder = new Map();
|
|
@@ -3950,9 +4409,10 @@ server.registerTool("import_preset", {
|
|
|
3950
4409
|
description: "Import a preset from a zip file or folder into PAM's preset library. Returns the relative path of the " +
|
|
3951
4410
|
"first imported preset, or an empty string on failure. Referenced samples are copied into the user sample " +
|
|
3952
4411
|
"folder and the preset's paths are rewritten. A name already in the library is never overwritten: the " +
|
|
3953
|
-
"import lands as <name>_1, <name>_2 and so on."
|
|
4412
|
+
"import lands as <name>_1, <name>_2 and so on. A downloaded .pampack file installs as a whole pack and " +
|
|
4413
|
+
"returns true on success.",
|
|
3954
4414
|
inputSchema: {
|
|
3955
|
-
path: z.string().describe("Absolute path to the preset zip file or
|
|
4415
|
+
path: z.string().describe("Absolute path to the preset zip file, folder or .pampack to import."),
|
|
3956
4416
|
},
|
|
3957
4417
|
}, async ({ path: presetPath }) => {
|
|
3958
4418
|
const check = await requireBridge();
|
|
@@ -3961,6 +4421,158 @@ server.registerTool("import_preset", {
|
|
|
3961
4421
|
const result = await bridge.send("importPreset", presetPath);
|
|
3962
4422
|
return json(result);
|
|
3963
4423
|
});
|
|
4424
|
+
const PACK_JOB_TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
|
4425
|
+
const PACK_INSTALL_WAIT_MS = 15 * 60_000;
|
|
4426
|
+
function isPackFailure(value) {
|
|
4427
|
+
return isRecord(value) && value.ok === false && typeof value.code === "string" && typeof value.error === "string";
|
|
4428
|
+
}
|
|
4429
|
+
// The failure shape pamd's CLI-backed tools return: structuredContent {ok:false, code, error}.
|
|
4430
|
+
function packFailure(code, message) {
|
|
4431
|
+
const detail = { ok: false, code, error: message };
|
|
4432
|
+
return {
|
|
4433
|
+
content: [{ type: "text", text: JSON.stringify(detail, null, 2) }],
|
|
4434
|
+
structuredContent: detail,
|
|
4435
|
+
isError: true,
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
function parseBridgeValue(raw) {
|
|
4439
|
+
if (typeof raw !== "string")
|
|
4440
|
+
return raw;
|
|
4441
|
+
try {
|
|
4442
|
+
return JSON.parse(raw);
|
|
4443
|
+
}
|
|
4444
|
+
catch {
|
|
4445
|
+
return raw;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
/** The connected instance's product; only a build that reports none falls back to the manifest. */
|
|
4449
|
+
async function connectedProduct() {
|
|
4450
|
+
const reported = bridge.getConnectedInstance()?.product?.toUpperCase();
|
|
4451
|
+
if (reported)
|
|
4452
|
+
return reported;
|
|
4453
|
+
const manifest = await loadManifestForProductGuard();
|
|
4454
|
+
if (!manifest)
|
|
4455
|
+
return undefined;
|
|
4456
|
+
return isMfxManifest(manifest) ? "MFX" : isMsqManifest(manifest) ? "MSQ" : "PAM";
|
|
4457
|
+
}
|
|
4458
|
+
async function requirePackStore() {
|
|
4459
|
+
const check = await requireBridge();
|
|
4460
|
+
if (check !== true)
|
|
4461
|
+
return check;
|
|
4462
|
+
return (await connectedProduct()) === "MFX"
|
|
4463
|
+
? packFailure("pack_unsupported_product", "MFX has no preset pack store; packs are a PAM feature.")
|
|
4464
|
+
: true;
|
|
4465
|
+
}
|
|
4466
|
+
const PACK_ENTRY = z.object({}).passthrough();
|
|
4467
|
+
const PACK_JOB_OUTPUT = {
|
|
4468
|
+
jobId: z.string().optional(),
|
|
4469
|
+
daemonEpoch: z.string().optional(),
|
|
4470
|
+
state: z.string().optional(),
|
|
4471
|
+
progress: z.number().optional(),
|
|
4472
|
+
result: z.object({ packs: z.array(PACK_ENTRY).optional() }).passthrough().optional(),
|
|
4473
|
+
error: z.object({ code: z.string().optional(), message: z.string().optional() }).passthrough().optional(),
|
|
4474
|
+
};
|
|
4475
|
+
server.registerTool("list_packs", {
|
|
4476
|
+
title: "List Preset Packs",
|
|
4477
|
+
description: "List the map.audio preset pack catalog with this machine's state: installed and catalog version, whether an " +
|
|
4478
|
+
"update is available, whether the licence owns the pack or it is free with PAM, price and download size. " +
|
|
4479
|
+
"Installed packs the catalog no longer lists appear with listed:false.",
|
|
4480
|
+
inputSchema: {},
|
|
4481
|
+
outputSchema: { packs: z.array(PACK_ENTRY) },
|
|
4482
|
+
}, async () => {
|
|
4483
|
+
const check = await requirePackStore();
|
|
4484
|
+
if (check !== true)
|
|
4485
|
+
return check;
|
|
4486
|
+
const listing = parseBridgeValue(await bridge.send("listPacks"));
|
|
4487
|
+
if (isPackFailure(listing))
|
|
4488
|
+
return packFailure(listing.code, listing.error);
|
|
4489
|
+
return structured(listing);
|
|
4490
|
+
});
|
|
4491
|
+
server.registerTool("install_pack", {
|
|
4492
|
+
title: "Install Preset Packs",
|
|
4493
|
+
description: "Download and install preset packs the licence owns (free packs need an active PAM licence), one at a time. " +
|
|
4494
|
+
"Pass packIds, or allOwned:true for every owned pack that is missing or outdated. Waits for the installs and " +
|
|
4495
|
+
"returns {packs:[{packId, version, state}]}; detach:true returns the job at once. A requestKey makes a retry " +
|
|
4496
|
+
"return the original job instead of downloading again. An unowned pack fails with pack_license_required.",
|
|
4497
|
+
inputSchema: {
|
|
4498
|
+
packIds: z.array(z.string().min(1)).min(1).max(64).optional().describe("Pack ids from the catalog, e.g. 'eko'."),
|
|
4499
|
+
allOwned: z.boolean().optional().describe("Install every owned pack that is missing or outdated."),
|
|
4500
|
+
requestKey: z.string().max(128).optional().describe("Stable caller key; reuse it to recover the same job."),
|
|
4501
|
+
detach: z.boolean().optional().describe("Return the job without waiting for the installs."),
|
|
4502
|
+
},
|
|
4503
|
+
outputSchema: { ...PACK_JOB_OUTPUT, packs: z.array(PACK_ENTRY).optional(), note: z.string().optional() },
|
|
4504
|
+
}, async ({ packIds, allOwned, requestKey, detach }) => {
|
|
4505
|
+
if (Boolean(packIds?.length) === Boolean(allOwned))
|
|
4506
|
+
return packFailure("usage", "Pass packIds or allOwned:true, not both.");
|
|
4507
|
+
const check = await requirePackStore();
|
|
4508
|
+
if (check !== true)
|
|
4509
|
+
return check;
|
|
4510
|
+
const started = parseBridgeValue(await bridge.send("installPack", {
|
|
4511
|
+
...(packIds?.length ? { packIds } : {}),
|
|
4512
|
+
...(allOwned ? { allOwned: true } : {}),
|
|
4513
|
+
...(requestKey ? { requestKey } : {}),
|
|
4514
|
+
}));
|
|
4515
|
+
if (isPackFailure(started))
|
|
4516
|
+
return packFailure(started.code, started.error);
|
|
4517
|
+
if (!isRecord(started) || typeof started.jobId !== "string")
|
|
4518
|
+
return error(`Unexpected installPack reply: ${JSON.stringify(started)}`);
|
|
4519
|
+
if (detach)
|
|
4520
|
+
return structured(started);
|
|
4521
|
+
// The job lives in the instance that accepted it; a select_instance meanwhile must not redirect the polls.
|
|
4522
|
+
const accepting = bridge.getConnectedInstance()?.instanceId;
|
|
4523
|
+
let job = started;
|
|
4524
|
+
const deadline = Date.now() + PACK_INSTALL_WAIT_MS;
|
|
4525
|
+
while (!PACK_JOB_TERMINAL.has(String(job.state)) && Date.now() < deadline) {
|
|
4526
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
4527
|
+
if (bridge.getConnectedInstance()?.instanceId !== accepting)
|
|
4528
|
+
return structured({
|
|
4529
|
+
...job,
|
|
4530
|
+
note: `Instance ${accepting} is still installing; select it again, then poll get_pack_install_status with this jobId.`,
|
|
4531
|
+
});
|
|
4532
|
+
const next = parseBridgeValue(await bridge.send("getPackInstallStatus", started.jobId));
|
|
4533
|
+
if (isPackFailure(next))
|
|
4534
|
+
return packFailure(next.code, next.error);
|
|
4535
|
+
if (isRecord(next))
|
|
4536
|
+
job = next;
|
|
4537
|
+
}
|
|
4538
|
+
if (job.state === "succeeded")
|
|
4539
|
+
return structured(isRecord(job.result) ? job.result : { packs: [] });
|
|
4540
|
+
if (job.state === "failed" || job.state === "cancelled") {
|
|
4541
|
+
const failure = isRecord(job.error) ? job.error : {};
|
|
4542
|
+
return packFailure(typeof failure.code === "string" ? failure.code : job.state === "cancelled" ? "cancelled" : "pack_install_failed", typeof failure.message === "string" ? failure.message : `Pack install job ${started.jobId} ${String(job.state)}.`);
|
|
4543
|
+
}
|
|
4544
|
+
return structured({ ...job, note: "Still installing; poll get_pack_install_status with this jobId." });
|
|
4545
|
+
});
|
|
4546
|
+
server.registerTool("get_pack_install_status", {
|
|
4547
|
+
title: "Get Pack Install Status",
|
|
4548
|
+
description: "Report a pack install job: state (queued, running, succeeded, failed, cancelled), progress 0..1, per-pack " +
|
|
4549
|
+
"state in result.packs, and error {code, message} when it failed.",
|
|
4550
|
+
inputSchema: { jobId: z.string().min(1).describe("The jobId install_pack returned.") },
|
|
4551
|
+
outputSchema: PACK_JOB_OUTPUT,
|
|
4552
|
+
}, async ({ jobId }) => {
|
|
4553
|
+
const check = await requirePackStore();
|
|
4554
|
+
if (check !== true)
|
|
4555
|
+
return check;
|
|
4556
|
+
const job = parseBridgeValue(await bridge.send("getPackInstallStatus", jobId));
|
|
4557
|
+
if (isPackFailure(job))
|
|
4558
|
+
return packFailure(job.code, job.error);
|
|
4559
|
+
return structured(job);
|
|
4560
|
+
});
|
|
4561
|
+
server.registerTool("uninstall_pack", {
|
|
4562
|
+
title: "Uninstall Preset Pack",
|
|
4563
|
+
description: "Remove an installed preset pack: every preset carrying its pack id, its samples and media, and its installed " +
|
|
4564
|
+
"record. Fails with pack_not_installed when nothing of the pack is on disk or recorded.",
|
|
4565
|
+
inputSchema: { packId: z.string().min(1).describe("Pack id from the catalog, e.g. 'eko'.") },
|
|
4566
|
+
outputSchema: { packId: z.string(), removed: z.boolean() },
|
|
4567
|
+
}, async ({ packId }) => {
|
|
4568
|
+
const check = await requirePackStore();
|
|
4569
|
+
if (check !== true)
|
|
4570
|
+
return check;
|
|
4571
|
+
const removed = parseBridgeValue(await bridge.send("uninstallPack", packId));
|
|
4572
|
+
if (isPackFailure(removed))
|
|
4573
|
+
return packFailure(removed.code, removed.error);
|
|
4574
|
+
return structured(removed);
|
|
4575
|
+
});
|
|
3964
4576
|
server.registerTool("set_visual", {
|
|
3965
4577
|
title: "Set Visual",
|
|
3966
4578
|
description: "Save an image file as a preset visual/thumbnail in PAM's media library. " +
|
|
@@ -4083,9 +4695,6 @@ const VIDEO_LAYER_FX = "hueRotate, glow, audioReact, vibration, chromaticAberrat
|
|
|
4083
4695
|
"polarSlitScan, cellVoronoi, spectralZones, paletteLut, asciiDither, pixelSort, " +
|
|
4084
4696
|
"cameraShake, blur, cymatics, lineJitter, crt, vhs";
|
|
4085
4697
|
// Output/global FX chain ids (videoStore, DEFAULT_VIDEO_FX_CONFIG).
|
|
4086
|
-
const VIDEO_OUTPUT_FX = "lines, barrelBlur, brightnessContrast, edge, glow, shake, rgbShift, halftone, mirror, " +
|
|
4087
|
-
"patches, pixelate, rolls, feedback, warp, datamosh, slicer, positionXY, speed, " +
|
|
4088
|
-
"cellVoronoi, polarSlitScan, spectralZones, audioReact";
|
|
4089
4698
|
// Binding source vocabulary (sourceKey string form OR a ModSource object).
|
|
4090
4699
|
const VIDEO_BIND_SOURCES = "Pass `source` as a sourceKey string: " +
|
|
4091
4700
|
"`g:<id>` global (level, bandLow, bandMid, bandHigh, transient, transientLow, " +
|
|
@@ -4173,7 +4782,7 @@ server.registerTool("capture_ui", {
|
|
|
4173
4782
|
.optional()
|
|
4174
4783
|
.describe("Switch all cells to the synth or sampler tab before capture."),
|
|
4175
4784
|
settleMs: z.number().optional().describe("Delay (ms) after view changes before rasterizing. Default 350."),
|
|
4176
|
-
timeoutMs: z.number().optional().default(
|
|
4785
|
+
timeoutMs: z.number().optional().default(30000).describe("Max ms to wait for the WebView to return the image. Default 30000."),
|
|
4177
4786
|
},
|
|
4178
4787
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
4179
4788
|
}, async ({ gridSize, mainView, instrumentView, settleMs, timeoutMs }) => {
|
|
@@ -4192,15 +4801,15 @@ server.registerTool("capture_ui", {
|
|
|
4192
4801
|
setup.settleMs = settleMs;
|
|
4193
4802
|
// Forward is fire-and-forget; poll the C++ cache until the WebView posts the PNG back.
|
|
4194
4803
|
await bridge.send("captureUi", setup);
|
|
4195
|
-
const deadline = Date.now() + (timeoutMs ??
|
|
4804
|
+
const deadline = Date.now() + (timeoutMs ?? 30000);
|
|
4196
4805
|
for (;;) {
|
|
4197
4806
|
const res = (await bridge.send("getUiCapture", { requestId }));
|
|
4198
4807
|
if (res.ready && res.requestId === requestId && res.path) {
|
|
4199
4808
|
return text(`Saved UI screenshot: ${res.path}`);
|
|
4200
4809
|
}
|
|
4201
4810
|
if (Date.now() > deadline) {
|
|
4202
|
-
return error(
|
|
4203
|
-
"
|
|
4811
|
+
return error(`capture_ui got no image within ${timeoutMs ?? 30000} ms. Either PAM's window is closed or suspended ` +
|
|
4812
|
+
"(html2canvas needs the live DOM), or a dense view is still rendering on a loaded machine: retry with a larger timeoutMs.");
|
|
4204
4813
|
}
|
|
4205
4814
|
await new Promise((r) => setTimeout(r, 150));
|
|
4206
4815
|
}
|
|
@@ -4221,14 +4830,14 @@ server.registerTool("video_command", {
|
|
|
4221
4830
|
"addLayerFx{layerId,fxType,params?,beforeFxId?}, removeLayerFx{layerId,fxId}, " +
|
|
4222
4831
|
"removeAllLayerFx{layerId}, setLayerFxParam{layerId,fxId,param,value|params}, " +
|
|
4223
4832
|
"setLayerFxEnabled{layerId,fxId,enabled?}, reorderLayerFx{layerId,fxIds}, " +
|
|
4224
|
-
"resetLayerFx{layerId,fxId}
|
|
4833
|
+
"resetLayerFx{layerId,fxId}, setFxParamMeta{layerId,fxId,param,lock?,limit?:{min,max}|null,snap?:step|null}, " +
|
|
4834
|
+
"randomizeFx{layerId,fxId}, setRandomizeAuto{config} (sanitized AutoRandomizeConfig; see get_video_state); " +
|
|
4835
|
+
"bind{layerId,fxId?,paramKey,source,...}, " +
|
|
4225
4836
|
"unbind{layerId,fxId?,paramKey}, setBindingField{layerId,fxId?,paramKey,field,value}, " +
|
|
4226
4837
|
"fireImpulse{layerId,fxId?,paramKey}, removeBindingsForLayer{layerId}, clearBindings; " +
|
|
4227
4838
|
"addOutputFx{fxId}, removeOutputFx{instanceId}, setOutputFxParam{instanceId|fxId,param,value}, " +
|
|
4228
|
-
"setOutputFxEnabled{instanceId,enabled}
|
|
4229
|
-
"
|
|
4230
|
-
"setModEnabled{enabled}; setModel{sourcePath?,visible?,blendMode?,audioReactive?,autoRotate?,transform?}, " +
|
|
4231
|
-
"resetModel; setSettings{...incl vxVariationFollowAudio,vxBarSyncEnabled,vxBarSyncIntervalBars}, " +
|
|
4839
|
+
"setOutputFxEnabled{instanceId,enabled} (the output chain is the master layer's FX); " +
|
|
4840
|
+
"setSettings{...incl vxVariationFollowAudio,vxBarSyncEnabled,vxBarSyncIntervalBars}, " +
|
|
4232
4841
|
"setActiveCell{cellId} (deprecated no-op), resetWorkspace; " +
|
|
4233
4842
|
"VX variation slots: saveVxVariation{index 0-7}, loadVxVariation{index}, clearVxVariation{index}, " +
|
|
4234
4843
|
"fillVxVariationsRandomized (occupancy/current in get_video_state; or use the vx_variation tool); " +
|
|
@@ -4391,11 +5000,9 @@ server.registerTool("apply_video_layout", {
|
|
|
4391
5000
|
});
|
|
4392
5001
|
server.registerTool("add_video_fx", {
|
|
4393
5002
|
title: "Add Video FX",
|
|
4394
|
-
description: "Append an FX to a layer's chain (target='layer', needs layerId) or to the output
|
|
4395
|
-
"
|
|
5003
|
+
description: "Append an FX to a layer's chain (target='layer', needs layerId) or to the output " +
|
|
5004
|
+
"chain, which is the master layer's chain (target='output'), and return the new id. FX: " +
|
|
4396
5005
|
VIDEO_LAYER_FX +
|
|
4397
|
-
". Output FX: " +
|
|
4398
|
-
VIDEO_OUTPUT_FX +
|
|
4399
5006
|
". FX params seed from registry defaults; override via `params`. Use set_video_fx " +
|
|
4400
5007
|
"to tweak afterwards.",
|
|
4401
5008
|
inputSchema: {
|
|
@@ -4449,7 +5056,7 @@ server.registerTool("set_video_fx", {
|
|
|
4449
5056
|
description: "Set FX params / enabled / strength on a per-layer FX (target='layer', needs " +
|
|
4450
5057
|
"layerId + fxId) or an output FX (target='output', needs instanceId, or fxId to " +
|
|
4451
5058
|
"auto-create). Pass a single {param,value}, multiple {params}, `enabled`, and/or " +
|
|
4452
|
-
"`strength` (
|
|
5059
|
+
"`strength` (unsupported: set the FX mix param). Get current ids/params from get_video_state.",
|
|
4453
5060
|
inputSchema: {
|
|
4454
5061
|
target: z.enum(["layer", "output"]).default("layer"),
|
|
4455
5062
|
layerId: z.string().optional().describe("target='layer': owning layer id."),
|
|
@@ -4459,7 +5066,7 @@ server.registerTool("set_video_fx", {
|
|
|
4459
5066
|
value: z.number().optional().describe("Value for `param`."),
|
|
4460
5067
|
params: z.record(z.string(), z.number()).optional().describe("Multiple params {key: value}."),
|
|
4461
5068
|
enabled: z.boolean().optional().describe("Enable/bypass this FX."),
|
|
4462
|
-
strength: z.number().optional().describe("
|
|
5069
|
+
strength: z.number().optional().describe("Unsupported; output FX run on the master layer and have no strength."),
|
|
4463
5070
|
},
|
|
4464
5071
|
}, async ({ target, layerId, fxId, instanceId, param, value, params, enabled, strength }) => {
|
|
4465
5072
|
const check = await requireBridge();
|
|
@@ -4541,7 +5148,7 @@ server.registerTool("bind_video_param", {
|
|
|
4541
5148
|
.union([z.string(), z.record(z.string(), z.any())])
|
|
4542
5149
|
.describe("sourceKey string (e.g. 'g:bandLow', 'c:1:onset', 't:beat') or ModSource object."),
|
|
4543
5150
|
fxId: z.string().optional().describe("FX instance id; omit/'' for a layer-field target."),
|
|
4544
|
-
amount: z.number().min(-1).max(1).optional().describe("Modulation depth (-1..1), default
|
|
5151
|
+
amount: z.number().min(-1).max(1).optional().describe("Modulation depth (-1..1), default 0.5 (the VX UI default)."),
|
|
4545
5152
|
curve: z.enum(["linear", "exp", "expInverse", "bipolar"]).optional(),
|
|
4546
5153
|
mode: z.enum(["additive", "replace", "multiply", "centered"]).optional(),
|
|
4547
5154
|
attackMs: z.number().min(0).optional(),
|
|
@@ -4595,7 +5202,6 @@ server.registerTool("set_video_settings", {
|
|
|
4595
5202
|
.describe("Canvas design ratio as width/height (e.g. 1.7777 for 16:9). null clears it and follows the window."),
|
|
4596
5203
|
letterboxColor: z.string().describe("#hex fill for the bars outside the design aspect.").optional(),
|
|
4597
5204
|
canvasColor: z.string().describe("#hex background behind every VX layer.").optional(),
|
|
4598
|
-
modEnabled: z.boolean().optional().describe("Mod-matrix master enable."),
|
|
4599
5205
|
vxVariationFollowAudio: z
|
|
4600
5206
|
.boolean()
|
|
4601
5207
|
.optional()
|
|
@@ -4611,7 +5217,7 @@ server.registerTool("set_video_settings", {
|
|
|
4611
5217
|
activeCell: z.number().int().optional().describe("Deprecated/no-op: VX is a single global scene (no per-cell video)."),
|
|
4612
5218
|
resetWorkspace: z.boolean().optional().describe("If true, clear the whole scene first."),
|
|
4613
5219
|
},
|
|
4614
|
-
}, async ({ syncMode, loopBars, aspectRatio, renderQuality, designAspect, letterboxColor, canvasColor,
|
|
5220
|
+
}, async ({ syncMode, loopBars, aspectRatio, renderQuality, designAspect, letterboxColor, canvasColor, vxVariationFollowAudio, vxBarSyncEnabled, vxBarSyncIntervalBars, activeCell, resetWorkspace }) => {
|
|
4615
5221
|
const check = await requireBridge();
|
|
4616
5222
|
if (check !== true)
|
|
4617
5223
|
return check;
|
|
@@ -4619,8 +5225,6 @@ server.registerTool("set_video_settings", {
|
|
|
4619
5225
|
await sendVideoCommand({ op: "resetWorkspace" });
|
|
4620
5226
|
if (activeCell !== undefined)
|
|
4621
5227
|
await sendVideoCommand({ op: "setActiveCell", cellId: activeCell });
|
|
4622
|
-
if (modEnabled !== undefined)
|
|
4623
|
-
await sendVideoCommand({ op: "setModEnabled", enabled: modEnabled });
|
|
4624
5228
|
const settings = {};
|
|
4625
5229
|
if (syncMode !== undefined)
|
|
4626
5230
|
settings.syncMode = syncMode;
|
|
@@ -4959,10 +5563,48 @@ server.registerTool("reset_patterns", {
|
|
|
4959
5563
|
await applyKv(delta, PATTERN_EDIT_KV_OPTIONS);
|
|
4960
5564
|
return text(`Cleared all patterns for cell ${cellId}.`);
|
|
4961
5565
|
});
|
|
5566
|
+
server.registerTool("set_effects_enabled", {
|
|
5567
|
+
title: "Enable or Bypass Cell Effects",
|
|
5568
|
+
description: "Enable or bypass cell effects. Omit cellId for every cell (CTRL ALL); omit effect for the entire cell FX chain. " +
|
|
5569
|
+
"This changes cell effect enable flags, preserving their settings and the master bus. Use set_master_effects for master-bus changes.",
|
|
5570
|
+
inputSchema: {
|
|
5571
|
+
enabled: z.boolean().describe("true enables, false bypasses."),
|
|
5572
|
+
cellId: z.number().int().min(1).max(8).optional().describe("Cell number (1-8). Omit for all cells, including empty cells."),
|
|
5573
|
+
effect: z.string().optional().describe("Cell effect name, e.g. delay, crusher, fdn, filter. Omit for all cell effects."),
|
|
5574
|
+
},
|
|
5575
|
+
}, async ({ enabled, cellId, effect }) => {
|
|
5576
|
+
const check = await requireBridge();
|
|
5577
|
+
if (check !== true)
|
|
5578
|
+
return check;
|
|
5579
|
+
const manifest = await loadManifestForProductGuard();
|
|
5580
|
+
if (!manifest)
|
|
5581
|
+
return error("The selected product manifest is unavailable; effect availability cannot be validated safely.");
|
|
5582
|
+
const available = cellEffectOrder(manifest);
|
|
5583
|
+
if (!available.length)
|
|
5584
|
+
return error("The selected product has no cell effect chain.");
|
|
5585
|
+
if (effect !== undefined && !available.includes(effect))
|
|
5586
|
+
return error(`Unknown cell effect: ${effect}. Available effects: ${available.join(", ")}.`);
|
|
5587
|
+
if (cellId !== undefined) {
|
|
5588
|
+
const cellCheck = validateCellIdForManifest(cellId, manifest);
|
|
5589
|
+
if (cellCheck !== true)
|
|
5590
|
+
return cellCheck;
|
|
5591
|
+
}
|
|
5592
|
+
const cellCount = manifestMaxCellId(manifest);
|
|
5593
|
+
if (cellCount === null)
|
|
5594
|
+
return error("The selected product manifest has no cells.");
|
|
5595
|
+
const cellIds = cellId === undefined ? Array.from({ length: cellCount }, (_, i) => i + 1) : [cellId];
|
|
5596
|
+
const effects = effect === undefined ? available : [effect];
|
|
5597
|
+
const delta = Object.fromEntries(cellIds.flatMap((id) => effects.map((name) => [`cell${id}_${name}Enabled`, enabled ? 1 : 0])));
|
|
5598
|
+
const stateRaw = await bridge.send("getState");
|
|
5599
|
+
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
5600
|
+
const changed = Object.entries(delta).filter(([key, value]) => Number(state[key] ?? 0) !== value).length;
|
|
5601
|
+
await applyKv(delta, EFFECT_CONFIG_KV_OPTIONS);
|
|
5602
|
+
return text(JSON.stringify({ cellIds, effects, enabled, changed }));
|
|
5603
|
+
});
|
|
4962
5604
|
server.registerTool("update_effect_config", {
|
|
4963
5605
|
title: "Update Effect Config",
|
|
4964
|
-
description: "Toggle cell effects on/off (filter, delay, crusher, flanger, chorus, phaser, eq, eq8, tablefilter, fdn, gate, compressor, panner, gain, vocoder, ringmod, vinylbrake, transient, repeat, pitchshift, limiter,
|
|
4965
|
-
"Wavefold and Resonator ship in every product. Exact availability comes from the selected product manifest. " +
|
|
5606
|
+
description: "Toggle cell effects on/off (filter, delay, crusher, flanger, chorus, phaser, freqshift, drive, lpc, fmod, specdelay, squeeze, tape, granular, noisegate, codec, cellmix, eq, eq8, tablefilter, fdn, gate, compressor, panner, gain, vocoder, ringmod, vinylbrake, transient, repeat, pitchshift, limiter, shimmer, wavefold, resonator). " +
|
|
5607
|
+
"granular is the Grain card and noisegate the Noise Gate. Wavefold and Resonator ship in every product. Exact availability comes from the selected product manifest. " +
|
|
4966
5608
|
"Uses the lighter KV path that does not rebuild the audio graph. Pass 1 to enable, 0 to disable.",
|
|
4967
5609
|
inputSchema: {
|
|
4968
5610
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
@@ -4974,7 +5616,7 @@ server.registerTool("update_effect_config", {
|
|
|
4974
5616
|
fdnEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4975
5617
|
gateEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4976
5618
|
compressorEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4977
|
-
// Shared extended effects. These ship on PAM
|
|
5619
|
+
// Shared extended effects. These ship on PAM too, so leaving them out of the
|
|
4978
5620
|
// schema made them unreachable over the bridge even though the handler is
|
|
4979
5621
|
// generic (it just builds `cell{N}_{key}`) and the state vars exist in
|
|
4980
5622
|
// build.js alongside the eight above.
|
|
@@ -4988,9 +5630,19 @@ server.registerTool("update_effect_config", {
|
|
|
4988
5630
|
repeatEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4989
5631
|
pitchshiftEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4990
5632
|
limiterEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4991
|
-
qverbEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4992
5633
|
chorusEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4993
5634
|
phaserEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5635
|
+
freqshiftEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5636
|
+
driveEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5637
|
+
lpcEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5638
|
+
specdelayEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5639
|
+
squeezeEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5640
|
+
tapeEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5641
|
+
granularEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5642
|
+
noisegateEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5643
|
+
codecEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5644
|
+
cellmixEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5645
|
+
fmodEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4994
5646
|
shimmerEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4995
5647
|
tablefilterEnabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
4996
5648
|
// Product-specific extended effects. The handler rejects flags absent from
|
|
@@ -5030,6 +5682,39 @@ server.registerTool("update_effect_config", {
|
|
|
5030
5682
|
.join(", ");
|
|
5031
5683
|
return text(`Updated effect config for cell ${cellId}: ${summary}`);
|
|
5032
5684
|
});
|
|
5685
|
+
// Spectral Delay's curve is the array state var cell{N}_specdelayBands, which set_parameters
|
|
5686
|
+
// (numbers only) cannot write. KVStateSyncManager projects it to the node, so no WebView is needed.
|
|
5687
|
+
const SPECDELAY_BANDS = 32;
|
|
5688
|
+
server.registerTool("set_spectral_delay_curve", {
|
|
5689
|
+
title: "Set Spectral Delay Curve",
|
|
5690
|
+
description: "Set a cell's Spectral Delay curve: 32 log-spaced bands from 20 Hz to 20 kHz, each 0..1. In TIME mode a " +
|
|
5691
|
+
"band's value is its share of Time (0 = the shortest repeat, 1 = the whole Time); in LEVEL mode it is how " +
|
|
5692
|
+
"much of that band is repeated. Depth scales the curve. Pass shape 'rise' (highs arrive last) or 'fall', or " +
|
|
5693
|
+
"32 explicit bands, low to high. Enable the effect with update_effect_config specdelayEnabled.",
|
|
5694
|
+
inputSchema: {
|
|
5695
|
+
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
5696
|
+
shape: z.enum(["rise", "fall"]).optional().describe("A factory curve. Omit when passing bands."),
|
|
5697
|
+
bands: z
|
|
5698
|
+
.array(z.number().min(0).max(1))
|
|
5699
|
+
.length(SPECDELAY_BANDS)
|
|
5700
|
+
.optional()
|
|
5701
|
+
.describe("32 values 0..1, 20 Hz first. Omit when passing shape."),
|
|
5702
|
+
},
|
|
5703
|
+
}, async ({ cellId, shape, bands }) => {
|
|
5704
|
+
const check = await requireBridgeForCells(cellId);
|
|
5705
|
+
if (check !== true)
|
|
5706
|
+
return check;
|
|
5707
|
+
if ((shape === undefined) === (bands === undefined))
|
|
5708
|
+
return error("Pass exactly one of shape or bands.");
|
|
5709
|
+
const manifest = await loadManifestForProductGuard();
|
|
5710
|
+
if (manifest && !cellEffectNames(manifest).includes("specdelay")) {
|
|
5711
|
+
return error(`Spectral Delay is not available in ${manifest.name ?? "the selected product"}.`);
|
|
5712
|
+
}
|
|
5713
|
+
const rise = Array.from({ length: SPECDELAY_BANDS }, (_, b) => b / (SPECDELAY_BANDS - 1));
|
|
5714
|
+
const curve = bands ?? (shape === "fall" ? rise.map((v) => 1 - v) : rise);
|
|
5715
|
+
await applyKv({ [`cell${cellId}_specdelayBands`]: curve }, STATE_UPDATE_KV_OPTIONS);
|
|
5716
|
+
return text(`Set cell ${cellId} Spectral Delay curve (${shape ?? "32 bands"}).`);
|
|
5717
|
+
});
|
|
5033
5718
|
// MFX cable patchbay. `mfx_routing` is a non-numeric state variable, so it was
|
|
5034
5719
|
// unreachable through set_parameters (numbers only) — an agent could shape an MFX
|
|
5035
5720
|
// cell but never patch one to another, which is the whole product.
|
|
@@ -5083,8 +5768,8 @@ function validateMfxEdges(edges, maxCells) {
|
|
|
5083
5768
|
}
|
|
5084
5769
|
server.registerTool("set_mfx_routing", {
|
|
5085
5770
|
title: "Set MFX Cable Routing",
|
|
5086
|
-
description: "Read or replace the MFX cable patch — the graph that decides which cell feeds which. MFX-only;
|
|
5087
|
-
"
|
|
5771
|
+
description: "Read or replace the MFX cable patch — the graph that decides which cell feeds which. MFX-only; other products " +
|
|
5772
|
+
"return a product error.\n\n" +
|
|
5088
5773
|
"Ports: 'input' (the host's audio in) and 'sidechain' are sources; 'output' is the destination; 'cellN' is both. " +
|
|
5089
5774
|
"A self-edge cellN→cellN is a feedback loop, and input→output is a direct dry path. Duplicate cables are dropped.\n\n" +
|
|
5090
5775
|
"SPLIT is a three-band crossover node: cable 'input' (or 'sidechain') to 'split', then patch 'split_lo', " +
|
|
@@ -5323,7 +6008,20 @@ server.registerTool("configure_mfx_combo", {
|
|
|
5323
6008
|
locked: next?.locked === 1 ? 1 : 0,
|
|
5324
6009
|
targets: Array.isArray(next?.targets) ? next.targets : null,
|
|
5325
6010
|
};
|
|
5326
|
-
|
|
6011
|
+
const delta = { mfx_combos: { ...slots, [String(index)]: update } };
|
|
6012
|
+
if ((op === "setTargetStop" || op === "invertTarget") && paramId) {
|
|
6013
|
+
const before = combo.targets.find((target) => target.id === paramId);
|
|
6014
|
+
const after = next?.targets?.find((target) => target.id === paramId);
|
|
6015
|
+
const info = parameterIndex.get(paramId);
|
|
6016
|
+
if (before && after && info) {
|
|
6017
|
+
const current = typeof state[paramId] === "number" ? state[paramId] : info.defaultValue;
|
|
6018
|
+
const comboValue = state[`mfx_combo${index}`];
|
|
6019
|
+
const changed = mfxComboTargetValueAfterRangeEdit(before, after, typeof comboValue === "number" ? comboValue : 0, info, current);
|
|
6020
|
+
if (changed !== current)
|
|
6021
|
+
delta[paramId] = changed;
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
6024
|
+
await applyKv(delta, EFFECT_CONFIG_KV_OPTIONS);
|
|
5327
6025
|
return json({ combo: index, slot: update });
|
|
5328
6026
|
});
|
|
5329
6027
|
server.registerTool("set_effect_order", {
|
|
@@ -5390,8 +6088,9 @@ server.registerTool("update_cell_config", {
|
|
|
5390
6088
|
title: "Update Cell Config",
|
|
5391
6089
|
description: "Update cell-level configuration properties like polyphony, MIDI settings, MIDI output (channel, Send, CC out controllers), varispeed, and modulation latch. " +
|
|
5392
6090
|
"These are KV-only properties that don't appear in the parameter manifest. " +
|
|
5393
|
-
"
|
|
5394
|
-
"
|
|
6091
|
+
"Most keys take a lightweight KV path; the source keys (sidechainFrom, cellmixFrom, cellmixPreFx) rebuild the graph once. " +
|
|
6092
|
+
"cellmixFrom picks the source the cell's Cell Mix card blends in and switches the card on. " +
|
|
6093
|
+
"When bound to MFX, this tool accepts only sidechainFrom, cellmixFrom and cellmixPreFx; on MSQ it accepts the MIDI and output keys only.",
|
|
5395
6094
|
inputSchema: {
|
|
5396
6095
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
5397
6096
|
isPoly: z.union([z.literal(0), z.literal(1)]).optional().describe("Polyphonic mode (0=mono, 1=poly)."),
|
|
@@ -5445,9 +6144,23 @@ server.registerTool("update_cell_config", {
|
|
|
5445
6144
|
.optional()
|
|
5446
6145
|
.describe("Sidechain compressor source. null=no sidechain, 0=external audio input, 1-8=cell output. " +
|
|
5447
6146
|
"Auto-enables the cell's compressor when set to a non-null value."),
|
|
6147
|
+
cellmixFrom: z
|
|
6148
|
+
.number()
|
|
6149
|
+
.int()
|
|
6150
|
+
.min(-1)
|
|
6151
|
+
.max(8)
|
|
6152
|
+
.optional()
|
|
6153
|
+
.describe("Cell Mix source: -1=none, 0=external sidechain input, 1-8=another cell (never this cell). " +
|
|
6154
|
+
"Any value but -1 also switches the cell's Cell Mix card on."),
|
|
6155
|
+
cellmixPreFx: z
|
|
6156
|
+
.boolean()
|
|
6157
|
+
.optional()
|
|
6158
|
+
.describe("Where Cell Mix taps a source cell: true=before its effects, false=after its effects and before its fader."),
|
|
5448
6159
|
midiOutChannel: z.number().int().min(0).max(16).optional().describe("MIDI out channel for the cell's CC out (and MSQ's notes): 0=Auto (the cell number), 1-16 explicit."),
|
|
5449
6160
|
sendToHost: z.union([z.literal(0), z.literal(1)]).optional().describe("0 keeps the cell's CC out (and MSQ's notes) off the MIDI out."),
|
|
5450
6161
|
midiOutPort: z.number().int().min(0).max(3).optional().describe("Standalone only: 0=Main MIDI Output, 1-3=Ports B-D set in Settings."),
|
|
6162
|
+
polyNoteMin: z.number().int().min(0).max(127).optional().describe("Lowest note a poly cell plays (MSQ: sends), at most polyNoteMax."),
|
|
6163
|
+
polyNoteMax: z.number().int().min(0).max(127).optional().describe("Highest note a poly cell plays (MSQ: sends), at least polyNoteMin."),
|
|
5451
6164
|
...Object.fromEntries(CC_OUT_SLOTS.map((slot) => [`ccOutCc${slot}`, ccOutControllerSchema(slot)])),
|
|
5452
6165
|
...Object.fromEntries(CC_LANES.map((lane) => [
|
|
5453
6166
|
ccLaneSlotField(lane),
|
|
@@ -5483,8 +6196,8 @@ server.registerTool("update_cell_config", {
|
|
|
5483
6196
|
if (manifest && isMfxManifest(manifest)) {
|
|
5484
6197
|
const unsupported = providedKeys.filter((key) => !MFX_CELL_CONFIG_KEYS.has(key));
|
|
5485
6198
|
if (unsupported.length > 0) {
|
|
5486
|
-
return error(
|
|
5487
|
-
`Sampler/MIDI/instrument config keys are PAM
|
|
6199
|
+
return error(`update_cell_config in MFX accepts only ${[...MFX_CELL_CONFIG_KEYS].join(", ")}. ` +
|
|
6200
|
+
`Sampler/MIDI/instrument config keys are PAM-only: ${unsupported.join(", ")}.`);
|
|
5488
6201
|
}
|
|
5489
6202
|
}
|
|
5490
6203
|
if (bridge.getConnectedInstance()?.product?.toUpperCase() === "MSQ") {
|
|
@@ -5494,6 +6207,27 @@ server.registerTool("update_cell_config", {
|
|
|
5494
6207
|
`MSQ has no sampler: ${unsupported.join(", ")}.`);
|
|
5495
6208
|
}
|
|
5496
6209
|
}
|
|
6210
|
+
if (config.cellmixFrom === cellId) {
|
|
6211
|
+
return error(`cellmixFrom cannot be cell ${cellId} itself: use -1 for none, 0 for the sidechain input, or another cell.`);
|
|
6212
|
+
}
|
|
6213
|
+
if (config.cellmixFrom !== undefined && config.cellmixFrom >= 1) {
|
|
6214
|
+
const sourceCheck = validateCellIdForManifest(config.cellmixFrom, manifest);
|
|
6215
|
+
if (sourceCheck !== true)
|
|
6216
|
+
return sourceCheck;
|
|
6217
|
+
}
|
|
6218
|
+
// MSQ's router drops every note outside Low-High as stored, so an inverted pair would silence the cell.
|
|
6219
|
+
if (config.polyNoteMin !== undefined || config.polyNoteMax !== undefined) {
|
|
6220
|
+
let current = {};
|
|
6221
|
+
if (config.polyNoteMin === undefined || config.polyNoteMax === undefined) {
|
|
6222
|
+
const state = await bridge.send("getState");
|
|
6223
|
+
const cells = isRecord(state) && isRecord(state.cells) ? state.cells : {};
|
|
6224
|
+
current = isRecord(cells[String(cellId)]) ? cells[String(cellId)] : {};
|
|
6225
|
+
}
|
|
6226
|
+
const low = config.polyNoteMin ?? Number(current.polyNoteMin ?? 0);
|
|
6227
|
+
const high = config.polyNoteMax ?? Number(current.polyNoteMax ?? 127);
|
|
6228
|
+
if (low > high)
|
|
6229
|
+
return error(`polyNoteMin (${low}) must not exceed polyNoteMax (${high}).`);
|
|
6230
|
+
}
|
|
5497
6231
|
const cellConfig = {};
|
|
5498
6232
|
const flatDelta = {};
|
|
5499
6233
|
for (const [key, value] of Object.entries(config)) {
|
|
@@ -5504,10 +6238,18 @@ server.registerTool("update_cell_config", {
|
|
|
5504
6238
|
else
|
|
5505
6239
|
cellConfig[key] = value;
|
|
5506
6240
|
}
|
|
6241
|
+
// An agent's scale is the user's: a Player switched off later must not put its pre-arm scale back.
|
|
6242
|
+
if (cellConfig.scaleName !== undefined || cellConfig.scaleRoot !== undefined) {
|
|
6243
|
+
cellConfig.playerPreScale = NO_SCALE_ARM;
|
|
6244
|
+
cellConfig.playerPreScaleRoot = NO_POLY_ARM;
|
|
6245
|
+
}
|
|
5507
6246
|
// Auto-enable compressor when setting sidechain source
|
|
5508
6247
|
if (cellConfig.sidechainFrom !== undefined && cellConfig.sidechainFrom !== null) {
|
|
5509
6248
|
flatDelta[`cell${cellId}_compressorEnabled`] = 1;
|
|
5510
6249
|
}
|
|
6250
|
+
if (cellConfig.cellmixFrom !== undefined && cellConfig.cellmixFrom !== -1) {
|
|
6251
|
+
flatDelta[`cell${cellId}_cellmixEnabled`] = 1;
|
|
6252
|
+
}
|
|
5511
6253
|
// Mirror interop.ts updateCellConfig: a pitch/stretch engine change OR a
|
|
5512
6254
|
// sliceKeymapMode flip (which changes pitch interpretation) bumps
|
|
5513
6255
|
// modeChangeSeq so the audio thread flushes its Signalsmith analysis state.
|
|
@@ -5521,8 +6263,9 @@ server.registerTool("update_cell_config", {
|
|
|
5521
6263
|
const delta = { ...flatDelta };
|
|
5522
6264
|
if (Object.keys(cellConfig).length > 0)
|
|
5523
6265
|
delta.cells = { [cellId]: cellConfig };
|
|
5524
|
-
//
|
|
5525
|
-
|
|
6266
|
+
// Sidechain and Cell Mix sources are wired at graph build, so they need the dispatch the UI's setters make.
|
|
6267
|
+
const wiresGraph = ["sidechainFrom", "cellmixFrom", "cellmixPreFx"].some((key) => cellConfig[key] !== undefined);
|
|
6268
|
+
await applyKv(delta, wiresGraph ? STATE_UPDATE_KV_OPTIONS : CELL_CONFIG_KV_OPTIONS);
|
|
5526
6269
|
const summary = [
|
|
5527
6270
|
...Object.entries(cellConfig).map(([k, v]) => `${k}=${v}`),
|
|
5528
6271
|
...Object.entries(flatDelta).map(([k, v]) => `${k}=${v}`),
|
|
@@ -5597,11 +6340,13 @@ server.registerTool("set_master_effects", {
|
|
|
5597
6340
|
masterTapeDrive: z.number().min(0).max(1).optional().describe("Tape drive / saturation amount (0-1, default 0.5). Higher = more harmonic grit + HF compression ('push it harder → it gets duller')."),
|
|
5598
6341
|
masterTapeTone: z.number().min(-1).max(1).optional().describe("Tape tone tilt (-1 dark .. 0 flat .. +1 bright, default 0). Go negative for dusty/lo-fi."),
|
|
5599
6342
|
masterTapeMix: z.number().min(0).max(1).optional().describe("Tape dry/wet (0-1, default 1 = fully wet)."),
|
|
5600
|
-
masterTapeMode: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]).optional().describe("Tape character: 0=Clean (gentle tanh), 1=Lo-Fi (most degradation, dusty), 2=
|
|
6343
|
+
masterTapeMode: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]).optional().describe("Tape character: 0=Clean (gentle tanh), 1=Lo-Fi (most degradation, dusty), 2=Broken (drifting wow/flutter, crackle, hard asymmetric drive), 3=Studio (Jiles-Atherton magnetic hysteresis — the deep hi-fi machine, no wow/hiss). Default 0."),
|
|
5601
6344
|
masterTapeWear: z.number().min(0).max(1).optional().describe("Tape wear: wow/flutter pitch drift + age (0-1, default 0.5). Raise for warble/sag."),
|
|
6345
|
+
masterTapeOutTrim: z.number().min(-24).max(24).optional().describe("Tape output trim dB on the wet signal, after Mix (default 0). Unity while Tape is off."),
|
|
5602
6346
|
masterEqEnabled: z.union([z.literal(0), z.literal(1)]).optional().describe("Enable the master EQ (trim/tilt/shelves/bells)."),
|
|
5603
6347
|
masterWidthEnabled: z.union([z.literal(0), z.literal(1)]).optional().describe("Enable the stereo utility (width + elliptical bass-mono)."),
|
|
5604
6348
|
masterInputTrim: z.number().min(-24).max(24).optional().describe("Master input trim in dB (gain staging into the chain; default 0)."),
|
|
6349
|
+
masterEqOutTrim: z.number().min(-24).max(24).optional().describe("Master EQ output trim dB (default 0). Unity while the EQ is off."),
|
|
5605
6350
|
masterEqTilt: z.number().min(-6).max(6).optional().describe("Tilt in dB at the extremes around a 650 Hz pivot (+ = brighter; the coarse tonal-balance lever)."),
|
|
5606
6351
|
masterEqLsFreq: z.number().min(20).max(500).optional().describe("Low-shelf corner Hz (default 90)."),
|
|
5607
6352
|
masterEqLsGain: z.number().min(-12).max(12).optional().describe("Low-shelf gain dB (default 0)."),
|
|
@@ -5620,12 +6365,14 @@ server.registerTool("set_master_effects", {
|
|
|
5620
6365
|
masterDenoiseSplitLow: z.number().min(40).max(2000).optional().describe("Low/mid split in Hz (default 250)."),
|
|
5621
6366
|
masterDenoiseSplitHigh: z.number().min(500).max(12000).optional().describe("Mid/high split in Hz (default 4000)."),
|
|
5622
6367
|
masterDenoiseMix: z.number().min(0).max(1).optional().describe("Master Denoise dry/wet, 0..1 (default 1)."),
|
|
6368
|
+
masterDenoiseOutTrim: z.number().min(-24).max(24).optional().describe("Master Denoise output trim dB on the wet signal, after Mix (default 0). Unity while Denoise is off."),
|
|
5623
6369
|
masterDenoisePrint: z.string().optional().describe("Learned noise print ('pdn1:<sampleRate>:<fftSize>:<base64>'), written by the Denoise panel's Learn button. Pass '' to forget it and return to adaptive tracking; copy a value read from get_state to reuse a print."),
|
|
5624
6370
|
masterIronEnabled: z.union([z.literal(0), z.literal(1)]).optional().describe("Enable the console/transformer iron colour stage (post-Tape, pre-Clipper; zero latency)."),
|
|
5625
6371
|
masterIronDrive: z.number().min(0).max(12).optional().describe("Iron drive dB (default 0). Level-compensated — drive adds colour, not loudness."),
|
|
5626
6372
|
masterIronAmount: z.number().min(0).max(1).optional().describe("Transformer (magnetizing-branch) depth 0..1 — LF-concentrated saturation, the 'iron' signature (default 0.5)."),
|
|
5627
6373
|
masterIronMode: z.union([z.literal(0), z.literal(1)]).optional().describe("0=Console (amp-forward, subtle), 1=Transformer (heavier LF iron)."),
|
|
5628
|
-
|
|
6374
|
+
masterIronOutTrim: z.number().min(-24).max(24).optional().describe("Iron output trim dB on the wet signal (default 0). Unity while Iron is off."),
|
|
6375
|
+
masterSqueezeEnabled: z.union([z.literal(0), z.literal(1)]).optional().describe("Enable Squeeze — a 3-band upward+downward compressor (post-Punch, pre-Tape; zero latency). The character (thresholds, ratios) is baked; Depth is the main lever."),
|
|
5629
6376
|
masterSqueezeDepth: z.number().min(0).max(1).optional().describe("Squeeze depth 0..1 (default 0.3): scales the applied per-band gain toward the full character — a density control, NOT a dry/wet. 0.15-0.3 densifies a master; 0.6+ is the aggressive pumped sound."),
|
|
5630
6377
|
masterSqueezeTime: z.number().min(0.1).max(10).optional().describe("Squeeze time multiplier on all band attack+release (0.1 fast .. 10 slow, default 1)."),
|
|
5631
6378
|
masterSqueezeTone: z.number().min(-1).max(1).optional().describe("Squeeze tone: bipolar low<->high band tilt, ±6 dB at the extremes (default 0)."),
|
|
@@ -5634,6 +6381,8 @@ server.registerTool("set_master_effects", {
|
|
|
5634
6381
|
masterSqueezeUpAmount: z.number().min(0).max(1.5).optional().describe("Squeeze upward-branch trim 0..1.5 (default 1). 0 disables the upward lift (downward-only multiband)."),
|
|
5635
6382
|
masterSqueezeDownAmount: z.number().min(0).max(1.5).optional().describe("Squeeze downward-branch trim 0..1.5 (default 1). 0 disables the downward limiting (pure upward lift)."),
|
|
5636
6383
|
masterSqueezeUpCapDb: z.number().min(0).max(24).optional().describe("Squeeze upward max-boost clamp dB (default 12) — bounds how far quiet material (tails, noise floor) is lifted."),
|
|
6384
|
+
masterSqueezeLoCross: z.number().min(10).max(500).optional().describe("Squeeze low/mid crossover Hz (default 88.3)."),
|
|
6385
|
+
masterSqueezeHiCross: z.number().min(500).max(20000).optional().describe("Squeeze mid/high crossover Hz (default 2500)."),
|
|
5637
6386
|
masterLoudnessAutoReset: z.union([z.literal(0), z.literal(1)]).optional().describe("Loudness meter auto-reset on host transport start (default 1). 0 keeps measurements running across transport stops."),
|
|
5638
6387
|
masterEqBells: z
|
|
5639
6388
|
.array(z.object({
|
|
@@ -5724,6 +6473,14 @@ const DRUM_SYNTH_REGISTRY = (() => {
|
|
|
5724
6473
|
})();
|
|
5725
6474
|
const drumSynth = createDrumSynthCatalog(DRUM_SYNTH_REGISTRY);
|
|
5726
6475
|
const DRUM_SYNTH_ENGINE_NAMES = drumSynth.engineNames;
|
|
6476
|
+
const DRUM_SYNTH_MODE_ROSTER = drumSynth.engines
|
|
6477
|
+
.filter((e) => e.modes.length > 0)
|
|
6478
|
+
.map((e) => `${e.name}=[${e.modes.map((m) => m.name).join(",")}]`)
|
|
6479
|
+
.join("; ");
|
|
6480
|
+
const DRUM_SYNTH_HOLD_ENGINES = drumSynth.engines
|
|
6481
|
+
.filter((e) => e.hold || e.modes.some((m) => m.hold))
|
|
6482
|
+
.map((e) => (e.hold ? e.name : `${e.name} (${e.modes.filter((m) => m.hold).map((m) => m.name).join("/")} mode)`))
|
|
6483
|
+
.join(", ");
|
|
5727
6484
|
/** State carries the bare enum; agents reason about the name. Report both. */
|
|
5728
6485
|
function describeSynthEngine(raw) {
|
|
5729
6486
|
const id = Math.round(Number(raw ?? 0));
|
|
@@ -5741,12 +6498,28 @@ server.registerTool("set_drum_synth", {
|
|
|
5741
6498
|
"(re-mapped through the destination's own range), so Kick Decay 0.41 becomes Stab Decay 0.336 — the same 198 ms. " +
|
|
5742
6499
|
"Untouched slots, and slots whose label or unit has no counterpart, take the destination's default. Per-engine p-labels: " +
|
|
5743
6500
|
drumSynth.engines.map((e) => `${e.name}=[${e.params.map((p) => p.label).join(",")}]`).join("; ") + ". " +
|
|
6501
|
+
`Engine modes are alternative topologies over the same knobs: ${DRUM_SYNTH_MODE_ROSTER}. A mode belongs to ` +
|
|
6502
|
+
"its engine: a change of engine without mode selects the first one, the original render; re-selecting the " +
|
|
6503
|
+
"cell's current engine keeps its mode. hold:true makes each new " +
|
|
6504
|
+
"hit sustain at its post-attack level for as long as its note is held (a sequencer step's gate length or a " +
|
|
6505
|
+
`held MIDI key), then decay at the live Decay rate. Only ${DRUM_SYNTH_HOLD_ENGINES} sustain; on any other ` +
|
|
6506
|
+
"engine or mode the flag is stored but has no effect. The synth-only output stage after the voice sum is four " +
|
|
6507
|
+
"ordinary parameters, set with set_parameters: cell{N}_synthFilterMode (0 Off, 1 LP, 2 BP, 3 HP), " +
|
|
6508
|
+
"cell{N}_synthCutoff (Hz), cell{N}_synthReso (%), cell{N}_synthDrive (%). " +
|
|
5744
6509
|
"NOTE: requires a PAM build with drum synth (manifest.drumSynth.enabled). This headless tool is product-gated, " +
|
|
5745
6510
|
"not gated by the UI-only drumSynthEnabled preference. " +
|
|
5746
6511
|
"On products without it, this tool returns a product error instead of writing inert synth keys.",
|
|
5747
6512
|
inputSchema: {
|
|
5748
6513
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
5749
6514
|
engine: z.enum(DRUM_SYNTH_ENGINE_NAMES).describe("Engine name. 'Off' disables the synth voice (sample-only cell)."),
|
|
6515
|
+
mode: z
|
|
6516
|
+
.string()
|
|
6517
|
+
.optional()
|
|
6518
|
+
.describe(`Engine mode name, case-insensitive: ${DRUM_SYNTH_MODE_ROSTER}. Other engines have none. Omitted: kept on the same engine, the first mode on a change.`),
|
|
6519
|
+
hold: z
|
|
6520
|
+
.boolean()
|
|
6521
|
+
.optional()
|
|
6522
|
+
.describe(`Sustain each hit while its note is held, then decay (cell{N}_synthHold). Sustains only on ${DRUM_SYNTH_HOLD_ENGINES}; a trigger_midi note-on then rings until its noteOn:false.`),
|
|
5750
6523
|
volume: z.number().min(-60).max(12).optional().describe("Synth output level in dB (default 0)."),
|
|
5751
6524
|
p1: z.number().min(0).max(1).optional().describe("Param 1 (engine-specific; usually Tune). Overrides the engine default."),
|
|
5752
6525
|
p2: z.number().min(0).max(1).optional().describe("Param 2 (engine-specific)."),
|
|
@@ -5757,7 +6530,7 @@ server.registerTool("set_drum_synth", {
|
|
|
5757
6530
|
p7: z.number().min(0).max(1).optional().describe("Param 7 (engine-specific)."),
|
|
5758
6531
|
p8: z.number().min(0).max(1).optional().describe("Param 8 (engine-specific)."),
|
|
5759
6532
|
},
|
|
5760
|
-
}, async ({ cellId, engine, volume, ...overrides }) => {
|
|
6533
|
+
}, async ({ cellId, engine, mode, hold, volume, ...overrides }) => {
|
|
5761
6534
|
const check = await requireBridge();
|
|
5762
6535
|
if (check !== true)
|
|
5763
6536
|
return check;
|
|
@@ -5771,10 +6544,17 @@ server.registerTool("set_drum_synth", {
|
|
|
5771
6544
|
const engineId = DRUM_SYNTH_ENGINE_NAMES.indexOf(engine);
|
|
5772
6545
|
if (engineId < 0)
|
|
5773
6546
|
return error(`Unknown drum synth engine: ${engine}`);
|
|
6547
|
+
const desc = drumSynth.getEngine(engineId);
|
|
6548
|
+
const modes = desc?.modes ?? [];
|
|
6549
|
+
const modeId = mode === undefined ? -1 : modes.findIndex((m) => m.name.toLowerCase() === mode.trim().toLowerCase());
|
|
6550
|
+
if (mode !== undefined && modeId < 0) {
|
|
6551
|
+
return error(modes.length > 0
|
|
6552
|
+
? `Unknown ${engine} mode: ${mode}. Valid modes: ${modes.map((m) => m.name).join(", ")}.`
|
|
6553
|
+
: `${engine} has no engine modes. Modes exist on ${DRUM_SYNTH_MODE_ROSTER}.`);
|
|
6554
|
+
}
|
|
5774
6555
|
const stateRaw = await bridge.send("getState");
|
|
5775
6556
|
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
5776
6557
|
const delta = drumSynth.buildEngineSwitchDelta(cellId, engineId, state);
|
|
5777
|
-
const desc = drumSynth.getEngine(engineId);
|
|
5778
6558
|
if (desc) {
|
|
5779
6559
|
for (const [key, value] of Object.entries(overrides)) {
|
|
5780
6560
|
if (value === undefined)
|
|
@@ -5786,10 +6566,26 @@ server.registerTool("set_drum_synth", {
|
|
|
5786
6566
|
}
|
|
5787
6567
|
if (volume !== undefined)
|
|
5788
6568
|
delta[`cell${cellId}_synthVolume`] = volume;
|
|
6569
|
+
if (modeId >= 0)
|
|
6570
|
+
delta[`cell${cellId}_synthMode`] = modeId;
|
|
6571
|
+
if (hold !== undefined)
|
|
6572
|
+
delta[`cell${cellId}_synthHold`] = hold ? 1 : 0;
|
|
5789
6573
|
// synthEngine is a graph-render key → needs a graph dispatch (STATE_UPDATE_KV_OPTIONS).
|
|
5790
6574
|
await applyKv(delta, STATE_UPDATE_KV_OPTIONS);
|
|
5791
6575
|
const labels = desc ? desc.params.map((p) => p.label).join(",") : "—";
|
|
5792
|
-
|
|
6576
|
+
let summary = `Cell ${cellId} drum synth → ${engine}${engineId > 0 ? ` (p1..pN = ${labels})` : ""}.`;
|
|
6577
|
+
if (modeId >= 0)
|
|
6578
|
+
summary += ` Mode ${modes[modeId].name}.`;
|
|
6579
|
+
if (hold === false)
|
|
6580
|
+
summary += " Hold off.";
|
|
6581
|
+
if (hold === true) {
|
|
6582
|
+
const playedMode = Math.round(Number(delta[`cell${cellId}_synthMode`] ?? state[`cell${cellId}_synthMode`] ?? 0));
|
|
6583
|
+
summary += drumSynth.holdsInMode(engineId, playedMode)
|
|
6584
|
+
? " Hold on: each hit sustains while its note is held."
|
|
6585
|
+
: ` Hold on, but ${engine}${modes.length > 0 ? ` in mode ${modes[playedMode]?.name ?? modes[0].name}` : ""} ` +
|
|
6586
|
+
`does not sustain; it takes effect on ${DRUM_SYNTH_HOLD_ENGINES}.`;
|
|
6587
|
+
}
|
|
6588
|
+
return text(summary);
|
|
5793
6589
|
});
|
|
5794
6590
|
server.registerTool("set_drum_synth_wave", {
|
|
5795
6591
|
title: "Set Drum Synth Wave Curve (Wave engine)",
|
|
@@ -5847,7 +6643,7 @@ const playerSlotArg = (n) => z
|
|
|
5847
6643
|
server.registerTool("list_players", {
|
|
5848
6644
|
title: "List Generative Players",
|
|
5849
6645
|
description: "Catalogue of PAM's generative Players — per-cell generators that replace a cell's drawn pattern with a " +
|
|
5850
|
-
"seeded, transport-synced cycle (EUCLID, KIT, TURING, WALK, MARKOV, RATCHET, PHASE, RIFF, MASK). Returns each " +
|
|
6646
|
+
"seeded, transport-synced cycle (EUCLID, KIT, TURING, WALK, MARKOV, RATCHET, PHASE, RIFF, MASK, PERC). Returns each " +
|
|
5851
6647
|
"player's blurb, defaults, P-slot vocabulary (detented positions or 0..1 knobs) and curated preset names, plus " +
|
|
5852
6648
|
"the role spread (which player each cell arms by default). Offline — no bridge needed. Arm one with set_player.",
|
|
5853
6649
|
inputSchema: {
|
|
@@ -5882,13 +6678,29 @@ server.registerTool("list_players", {
|
|
|
5882
6678
|
],
|
|
5883
6679
|
});
|
|
5884
6680
|
});
|
|
6681
|
+
/** The plugin's default-source rule (PlayerListenSection): a cell worth hearing runs a Player or has drawn
|
|
6682
|
+
* notes, and for Harmonic its notes are pitches (`cellNotesArePitches`). */
|
|
6683
|
+
function cellSounds(state, cells, id, mode, midiProduct) {
|
|
6684
|
+
const raw = cells[String(id)];
|
|
6685
|
+
const cell = isRecord(raw) ? raw : {};
|
|
6686
|
+
const drawn = state[`cell${id}_midiSequence`];
|
|
6687
|
+
const sounds = cellPlayerType(cell) > 0 || (Array.isArray(drawn) && drawn.length > 0);
|
|
6688
|
+
if (!sounds || mode !== 3)
|
|
6689
|
+
return sounds;
|
|
6690
|
+
const mono = !midiProduct && Math.round(Number(cell.isPoly ?? 0)) !== 1;
|
|
6691
|
+
const drumPlayer = getPlayerSpec(cellPlayerType(cell))?.tag === "drum";
|
|
6692
|
+
return !mono && (!drumPlayer || Math.round(Number(cell.playerPatternMode ?? 0)) === 1);
|
|
6693
|
+
}
|
|
5885
6694
|
server.registerTool("set_player", {
|
|
5886
6695
|
title: "Set Generative Player",
|
|
5887
6696
|
description: "Arm, edit or switch off a cell's generative Player (PAM-only). Passing `player` arms that generator with the " +
|
|
5888
6697
|
"roster's own voicing (or a curated `preset`), the track genre on genre-aware players, and a reversible poly " +
|
|
5889
6698
|
"arm on melodic ones; omit `player` to edit the running player's values in place. Values land in both the cell " +
|
|
5890
6699
|
"config and the cell{N}_player* automation params, like the plugin UI. 'off' restores the drawn pattern and the " +
|
|
5891
|
-
"cell's pre-arm polyphony.
|
|
6700
|
+
"cell's pre-arm polyphony and scale. A melodic arm in the plugin UI also gives a chromatic cell the house or song " +
|
|
6701
|
+
"scale; here the scale stays update_cell_config's (scaleName/scaleRoot). Pass p1 to choose PERC's drum; without it " +
|
|
6702
|
+
"PERC arms on Conga. A plugin-UI arm also takes the cell's snap as the grid when the cell never chose one; " +
|
|
6703
|
+
"here pass grid, or the cell keeps its own (1/4 when unset). The player sounds only while the transport runs. The UI shows a running player " +
|
|
5892
6704
|
"unprompted; to arm from the UI as well, update_settings { experimentalMode: true, experimentalGenerativePatterns: true }.",
|
|
5893
6705
|
inputSchema: {
|
|
5894
6706
|
cellIds: z
|
|
@@ -5919,18 +6731,32 @@ server.registerTool("set_player", {
|
|
|
5919
6731
|
.enum(PATTERN_MODES)
|
|
5920
6732
|
.optional()
|
|
5921
6733
|
.describe("'mask' (default, shown as Replace in the UI): the player replaces the drawn pattern. 'keep' (shown as Layer): the drawn pattern keeps sounding and the player layers over it."),
|
|
6734
|
+
grid: z
|
|
6735
|
+
.enum(PLAYER_GRIDS)
|
|
6736
|
+
.optional()
|
|
6737
|
+
.describe("The step the player generates on, named as the snap menu names it: 1/N of a beat, so '1/4' is sixteenths (the default), " +
|
|
6738
|
+
"'1/3' eighth-note triplets, '1/2' eighths. A cycle holds at most 128 steps; a longer pattern stretches the step. " +
|
|
6739
|
+
"Swing is off on 1/3, 1/5 and 1/9. Kept per cell; it never follows the snap menu."),
|
|
5922
6740
|
listenMode: z
|
|
5923
6741
|
.enum(LISTEN_MODES)
|
|
5924
6742
|
.optional()
|
|
5925
|
-
.describe("Ensemble listening
|
|
5926
|
-
|
|
5927
|
-
|
|
6743
|
+
.describe("Ensemble listening on the source's last completed cycle: complement gives way where it played and fills its rests, " +
|
|
6744
|
+
"answer replies half a phrase later, harmonic favours the notes it sounds. A soft source note pushes less than the loudest notes the cell hears. " +
|
|
6745
|
+
"Switching listening on without listenSource hears the cell's role partner (the bass hears the kick), as the plugin UI does."),
|
|
6746
|
+
listenSource: z.number().int().min(0).max(8).optional().describe("Listening source: 0 = all other cells, 1-8 = one cell (a cell's own number is skipped)."),
|
|
6747
|
+
listenAmount: z.number().min(0).max(1).optional().describe("Listening strength 0..1: a nudge low down; at 1 the relationship decides which candidates play."),
|
|
6748
|
+
listenPair: z
|
|
6749
|
+
.enum(LISTEN_PAIRS)
|
|
6750
|
+
.optional()
|
|
6751
|
+
.describe("Two cells that complement each other: 'keep' gives this cell the steps they share (its partner gives way), " +
|
|
6752
|
+
"'trade' never settles (they swap those steps every cycle), 'auto' (default) lets the lower cell keep them. " +
|
|
6753
|
+
"Written to both cells of the pair; refused while a cell complements several cells."),
|
|
5928
6754
|
poly: z
|
|
5929
6755
|
.boolean()
|
|
5930
6756
|
.optional()
|
|
5931
6757
|
.describe("Override the automatic poly arm: true forces poly, false keeps the cell mono even for a melodic player."),
|
|
5932
6758
|
},
|
|
5933
|
-
}, async ({ cellIds, player, preset, genre, density, mutate, p1, p2, p3, p4, p5, seed, patternMode, listenMode, listenSource, listenAmount, poly }) => {
|
|
6759
|
+
}, async ({ cellIds, player, preset, genre, density, mutate, p1, p2, p3, p4, p5, seed, patternMode, grid, listenMode, listenSource, listenAmount, listenPair, poly }) => {
|
|
5934
6760
|
const check = await requireBridgeForCells(cellIds);
|
|
5935
6761
|
if (check !== true)
|
|
5936
6762
|
return check;
|
|
@@ -5938,6 +6764,7 @@ server.registerTool("set_player", {
|
|
|
5938
6764
|
const gate = playerProductError(manifest);
|
|
5939
6765
|
if (gate)
|
|
5940
6766
|
return gate;
|
|
6767
|
+
const midiProduct = !!manifest && isMsqManifest(manifest);
|
|
5941
6768
|
const playerRef = typeof player === "string" ? player.trim().toLowerCase() : player;
|
|
5942
6769
|
const wantsOff = playerRef === "off" || playerRef === 0;
|
|
5943
6770
|
const wantsRole = playerRef === "role";
|
|
@@ -5972,11 +6799,20 @@ server.registerTool("set_player", {
|
|
|
5972
6799
|
config.isPoly = prePoly;
|
|
5973
6800
|
if (preRoot !== undefined)
|
|
5974
6801
|
config.polyRootNote = preRoot;
|
|
6802
|
+
// The scale a melodic arm seeded in the plugin UI goes back with the player.
|
|
6803
|
+
const preScale = typeof cell.playerPreScale === "string" && cell.playerPreScale !== NO_SCALE_ARM ? cell.playerPreScale : undefined;
|
|
6804
|
+
if (preScale !== undefined) {
|
|
6805
|
+
config.scaleName = preScale;
|
|
6806
|
+
config.scaleRoot = recorded("playerPreScaleRoot") ?? 0;
|
|
6807
|
+
}
|
|
5975
6808
|
config.playerPrePoly = NO_POLY_ARM;
|
|
5976
6809
|
config.playerPreRoot = NO_POLY_ARM;
|
|
6810
|
+
config.playerPreScale = NO_SCALE_ARM;
|
|
6811
|
+
config.playerPreScaleRoot = NO_POLY_ARM;
|
|
5977
6812
|
cellsDelta[String(cellId)] = config;
|
|
5978
6813
|
lines.push(`Cell ${cellId}: ${getPlayerSpec(currentType)?.name ?? "player"} off — drawn pattern restored` +
|
|
5979
|
-
`${prePoly !== undefined || preRoot !== undefined ? ", pre-arm polyphony restored" : ""}
|
|
6814
|
+
`${prePoly !== undefined || preRoot !== undefined ? ", pre-arm polyphony restored" : ""}` +
|
|
6815
|
+
`${preScale !== undefined ? ", pre-arm scale restored" : ""}.`);
|
|
5980
6816
|
continue;
|
|
5981
6817
|
}
|
|
5982
6818
|
const roleConfig = wantsRole ? rolePlayerConfigFor(cellId) : undefined;
|
|
@@ -6028,12 +6864,23 @@ server.registerTool("set_player", {
|
|
|
6028
6864
|
config.playerSeed = seed === "random" ? randomPlayerSeed() : Math.round(seed);
|
|
6029
6865
|
if (patternMode)
|
|
6030
6866
|
config.playerPatternMode = PATTERN_MODES.indexOf(patternMode);
|
|
6867
|
+
if (grid)
|
|
6868
|
+
config.playerGrid = playerGridValue(grid);
|
|
6031
6869
|
if (listenMode)
|
|
6032
6870
|
config.playerListenMode = LISTEN_MODES.indexOf(listenMode);
|
|
6033
|
-
if (listenSource !== undefined)
|
|
6871
|
+
if (listenSource !== undefined && listenSource !== cellId)
|
|
6034
6872
|
config.playerListenSource = listenSource;
|
|
6873
|
+
// Switching listening on with no source named hears the role partner, never the whole rack by default.
|
|
6874
|
+
const startsListening = (listenSource === undefined || listenSource === cellId) && (config.playerListenMode ?? 0) > 0 &&
|
|
6875
|
+
Math.round(Number(cell.playerListenMode ?? 0)) <= 0 && Math.round(Number(cell.playerListenSource ?? 0)) === 0;
|
|
6876
|
+
if (startsListening) {
|
|
6877
|
+
const mode = config.playerListenMode;
|
|
6878
|
+
config.playerListenSource = listenPartnerFor(cellId, mode, 8, (id) => cellSounds(state, cellsObj, id, mode, midiProduct));
|
|
6879
|
+
}
|
|
6035
6880
|
if (listenAmount !== undefined)
|
|
6036
6881
|
config.playerListenAmount = listenAmount;
|
|
6882
|
+
if (listenPair)
|
|
6883
|
+
config.playerListenPair = LISTEN_PAIRS.indexOf(listenPair);
|
|
6037
6884
|
// Reversible poly arm: record the pre-arm values once, never the armed state.
|
|
6038
6885
|
const autoPoly = arming && poly !== false && (isMelodicPlayer(spec) || roleConfig?.isPoly === 1);
|
|
6039
6886
|
let polyNote = "";
|
|
@@ -6073,10 +6920,43 @@ server.registerTool("set_player", {
|
|
|
6073
6920
|
parts.push(`seed ${config.playerSeed}`);
|
|
6074
6921
|
if (patternMode)
|
|
6075
6922
|
parts.push(`pattern ${patternMode}`);
|
|
6923
|
+
if (grid)
|
|
6924
|
+
parts.push(`grid ${grid}`);
|
|
6076
6925
|
if (listenMode)
|
|
6077
|
-
parts.push(`listen ${listenMode}`);
|
|
6926
|
+
parts.push(`listen ${listenMode}${config.playerListenSource !== undefined ? ` to ${config.playerListenSource > 0 ? `cell ${config.playerListenSource}` : "all"}` : ""}`);
|
|
6927
|
+
if (listenSource === cellId)
|
|
6928
|
+
parts.push(`source ${cellId} skipped (a cell cannot listen to itself)`);
|
|
6929
|
+
if (listenPair)
|
|
6930
|
+
parts.push(`pair ${listenPair}`);
|
|
6078
6931
|
lines.push(`Cell ${cellId}: ${spec.name} ${arming ? "armed" : "updated"} — ${parts.join(", ")}${polyNote}.`);
|
|
6079
6932
|
}
|
|
6933
|
+
// A pair's choice lands on both cells, as the plugin's pair chips write it.
|
|
6934
|
+
if (listenPair) {
|
|
6935
|
+
const cellOf = (id) => {
|
|
6936
|
+
const raw = cellsObj[String(id)];
|
|
6937
|
+
return { ...(isRecord(raw) ? raw : {}), ...(cellsDelta[String(id)] ?? {}) };
|
|
6938
|
+
};
|
|
6939
|
+
const choice = LISTEN_PAIRS.indexOf(listenPair);
|
|
6940
|
+
const partnerChoice = listenPair === "keep" ? LISTEN_PAIRS.indexOf("auto") : choice;
|
|
6941
|
+
for (const cellId of cellIds) {
|
|
6942
|
+
if (!cellsDelta[String(cellId)])
|
|
6943
|
+
continue;
|
|
6944
|
+
const partner = soleComplementPartner(cellOf, cellId, 8);
|
|
6945
|
+
if (partner < 0) {
|
|
6946
|
+
const crowded = crowdedComplementCell(cellOf, cellId, 8);
|
|
6947
|
+
return error(`Cell ${crowded} complements several cells, so one pair choice would settle all of them; give cell ${crowded} one listenSource first.`);
|
|
6948
|
+
}
|
|
6949
|
+
if (partner === 0) {
|
|
6950
|
+
lines.push(`Cell ${cellId}: no cell complements it back yet; its pair choice applies once one does.`);
|
|
6951
|
+
continue;
|
|
6952
|
+
}
|
|
6953
|
+
if (listenPair === "keep" && cellIds.includes(partner))
|
|
6954
|
+
return error(`Cells ${cellId} and ${partner} complement each other: name one of them for listenPair 'keep'.`);
|
|
6955
|
+
cellsDelta[String(partner)] = { ...(cellsDelta[String(partner)] ?? {}), playerListenPair: partnerChoice };
|
|
6956
|
+
if (!cellIds.includes(partner))
|
|
6957
|
+
lines.push(`Cell ${partner}: ${listenPair === "keep" ? `gives cell ${cellId} the steps they share` : `pair ${listenPair}`}.`);
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6080
6960
|
if (Object.keys(cellsDelta).length === 0)
|
|
6081
6961
|
return text(lines.join("\n"));
|
|
6082
6962
|
await applyKv({ ...flatDelta, cells: cellsDelta }, CELL_CONFIG_KV_OPTIONS);
|
|
@@ -6255,11 +7135,12 @@ server.registerTool("add_param_seq", {
|
|
|
6255
7135
|
cellId: z.number().min(1).max(8).describe("Cell ID (1-8) — used for routing."),
|
|
6256
7136
|
sequence: z
|
|
6257
7137
|
.array(z.number().min(0).max(1))
|
|
7138
|
+
.max(128)
|
|
6258
7139
|
.optional()
|
|
6259
|
-
.describe("Step values (0-1). Defaults to 16 steps of 0.5."),
|
|
7140
|
+
.describe("Step values (0-1), up to 128. Defaults to 16 steps of 0.5."),
|
|
6260
7141
|
strength: z.number().min(0).max(1).default(0.5).describe("Modulation depth (0-1)."),
|
|
6261
7142
|
bipolar: z.union([z.literal(0), z.literal(1)]).default(0).describe("0 = unipolar, 1 = bipolar."),
|
|
6262
|
-
length: z.number().min(
|
|
7143
|
+
length: z.number().min(1 / 64).max(16).default(1).describe("Pattern length in bars."),
|
|
6263
7144
|
speed: z.number().min(0.0625).max(16).default(1).describe("Pattern speed multiplier."),
|
|
6264
7145
|
freeMode: z
|
|
6265
7146
|
.union([z.literal(0), z.literal(1)])
|
|
@@ -6280,6 +7161,20 @@ server.registerTool("add_param_seq", {
|
|
|
6280
7161
|
return error(param);
|
|
6281
7162
|
const existing = await bridge.send("getState", "paramSeqs");
|
|
6282
7163
|
const paramSeqs = (Array.isArray(existing) ? existing : []).filter((seq) => seq?.paramId !== paramId);
|
|
7164
|
+
// The target's effect and macro switch on, as the UI's addParamSeq does.
|
|
7165
|
+
const enables = {};
|
|
7166
|
+
if (param.effect && /^cell\d+$/.test(param.cell ?? ""))
|
|
7167
|
+
enables[`${param.cell}_${param.effect}Enabled`] = 1;
|
|
7168
|
+
if (paramId.startsWith("macro_")) {
|
|
7169
|
+
const macrosRaw = await bridge.send("getState", "macros");
|
|
7170
|
+
const macros = Array.isArray(macrosRaw) ? macrosRaw : [];
|
|
7171
|
+
const held = macros.find((m) => m.paramId === paramId);
|
|
7172
|
+
if (held?.enabled !== 1) {
|
|
7173
|
+
enables.macros = held
|
|
7174
|
+
? macros.map((m) => (m === held ? { ...m, enabled: 1 } : m))
|
|
7175
|
+
: [...macros, { paramId, enabled: 1, strength: 0.5, bipolar: 0, mode: 0 }];
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
6283
7178
|
const newSeq = {
|
|
6284
7179
|
paramId,
|
|
6285
7180
|
cellId,
|
|
@@ -6295,7 +7190,7 @@ server.registerTool("add_param_seq", {
|
|
|
6295
7190
|
stepMode,
|
|
6296
7191
|
};
|
|
6297
7192
|
paramSeqs.push(newSeq);
|
|
6298
|
-
await applyKv({ paramSeqs }, STATE_UPDATE_KV_OPTIONS);
|
|
7193
|
+
await applyKv({ paramSeqs, ...enables }, STATE_UPDATE_KV_OPTIONS);
|
|
6299
7194
|
return text(`Added paramSeq for ${paramId} (${newSeq.sequence.length} steps, strength ${strength}).`);
|
|
6300
7195
|
});
|
|
6301
7196
|
server.registerTool("update_param_seq", {
|
|
@@ -6304,9 +7199,9 @@ server.registerTool("update_param_seq", {
|
|
|
6304
7199
|
"Use add_param_seq first if no paramSeq exists for this paramId.",
|
|
6305
7200
|
inputSchema: {
|
|
6306
7201
|
paramId: z.string().describe("Target parameter ID."),
|
|
6307
|
-
sequence: z.array(z.number().min(0).max(1)).optional().describe("New step values (0-1)."),
|
|
7202
|
+
sequence: z.array(z.number().min(0).max(1)).max(128).optional().describe("New step values (0-1), up to 128."),
|
|
6308
7203
|
strength: z.number().min(0).max(1).optional().describe("Modulation depth (0-1)."),
|
|
6309
|
-
length: z.number().min(
|
|
7204
|
+
length: z.number().min(1 / 64).max(16).optional().describe("Pattern length in bars."),
|
|
6310
7205
|
speed: z.number().min(0.0625).max(16).optional().describe("Pattern speed multiplier."),
|
|
6311
7206
|
bipolar: z.union([z.literal(0), z.literal(1)]).optional().describe("0 = unipolar, 1 = bipolar."),
|
|
6312
7207
|
enabled: z.union([z.literal(0), z.literal(1)]).optional().describe("0 = bypass the sequencer, 1 = run it."),
|
|
@@ -6343,39 +7238,44 @@ server.registerTool("update_param_seq", {
|
|
|
6343
7238
|
});
|
|
6344
7239
|
server.registerTool("set_free_sequencer", {
|
|
6345
7240
|
title: "Set Free Sequencer",
|
|
6346
|
-
description: "Write the Free Sequencer's master step contour (top-level `freeSeqSequence`, 2..
|
|
7241
|
+
description: "Write the Free Sequencer's master step contour (top-level `freeSeqSequence`, 2..128 steps of 0..1). Every " +
|
|
6347
7242
|
"parameter sequencer mapped to it (freeMode:1 via add_param_seq / update_param_seq) follows these steps AND its " +
|
|
6348
7243
|
"clock live, each with its own strength, polarity and smoothing; cell-locked and Gate-mode sequencers keep " +
|
|
6349
7244
|
"their own steps. An empty contour restores the legacy per-sequencer behaviour.",
|
|
6350
7245
|
inputSchema: {
|
|
6351
7246
|
sequence: z
|
|
6352
7247
|
.array(z.number().min(0).max(1))
|
|
6353
|
-
.max(
|
|
6354
|
-
.describe("Master step values, 0..1 each: 2..
|
|
7248
|
+
.max(128)
|
|
7249
|
+
.describe("Master step values, 0..1 each: 2..128 steps, or [] to clear the contour."),
|
|
6355
7250
|
},
|
|
6356
7251
|
}, async ({ sequence }) => {
|
|
6357
7252
|
const check = await requireBridge();
|
|
6358
7253
|
if (check !== true)
|
|
6359
7254
|
return check;
|
|
6360
7255
|
if (sequence.length === 1)
|
|
6361
|
-
return error("A contour needs 2..
|
|
7256
|
+
return error("A contour needs 2..128 steps; pass [] to clear it.");
|
|
6362
7257
|
await bridge.send("applyFreeSeqSequenceUpdate", { sequence });
|
|
6363
7258
|
if (sequence.length === 0)
|
|
6364
7259
|
return text("Free Sequencer contour cleared; mapped sequencers use their own steps.");
|
|
6365
7260
|
return text(`Free Sequencer: ${sequence.length} steps written. Map a paramSeq with freeMode:1 to follow it.`);
|
|
6366
7261
|
});
|
|
7262
|
+
// getMidiExportEvents' cap (MCPBridge::handleGetMidiExportEvents); check-parity compares the two.
|
|
7263
|
+
const MAX_MIDI_EXPORT_BEATS = 1024;
|
|
6367
7264
|
server.registerTool("export_midi", {
|
|
6368
7265
|
title: "Export MIDI",
|
|
6369
|
-
description: "Write the cells' note patterns to a Standard MIDI File (type 1, 480 PPQ, tempo from the transport): " +
|
|
6370
|
-
"one track per cell
|
|
7266
|
+
description: "Write the cells' note patterns to a Standard MIDI File (type 1, 480 PPQ, tempo and meter from the transport): " +
|
|
7267
|
+
"one track per cell, across `bars`. MSQ writes each cell on its output channel (Auto = cell number), leaves " +
|
|
7268
|
+
"out a cell with Send off and notes outside a poly cell's Low-High; PAM writes notes on the cell number and " +
|
|
7269
|
+
"CC on the cell's MIDI out channel. Each sending CC out slot's value opens the track at tick 0, its CC lane's " +
|
|
7270
|
+
"locks and releases follow on the lane's own clock, and a pedal still down at the end is lifted there. A Player cell exports the cycles it " +
|
|
6371
7271
|
"generates (density-gated, PATTERN Keep layered), rendered by PAM itself; Listen responses are left out. " +
|
|
6372
7272
|
"Grid timing from base values: swing, probability, CTRL macros and modulators are not applied. Nothing " +
|
|
6373
7273
|
"changes in PAM. Default location " +
|
|
6374
7274
|
"is PAM's Exports folder.",
|
|
6375
7275
|
inputSchema: {
|
|
6376
|
-
filename: z.string().optional().describe("Base name without extension (default
|
|
6377
|
-
cellIds: z.array(z.number().int().min(1).max(8)).optional().describe("Cells to include (default: every cell with notes)."),
|
|
6378
|
-
bars: z.number().min(1).max(64).optional().describe("Length in
|
|
7276
|
+
filename: z.string().optional().describe("Base name without extension (default <product>_midi_<epoch>)."),
|
|
7277
|
+
cellIds: z.array(z.number().int().min(1).max(8)).optional().describe("Cells to include (default: every cell with notes or CC out)."),
|
|
7278
|
+
bars: z.number().min(1).max(64).optional().describe("Length in bars of the transport's meter (default: the longest included pattern cycle, at least 1)."),
|
|
6379
7279
|
mode: z.enum(["combined", "stems"]).optional().describe("'combined' (default): one multi-track file. 'stems': one file per cell."),
|
|
6380
7280
|
outputDir: z.string().optional().describe("Directory to write into (default: PAM's Exports folder)."),
|
|
6381
7281
|
},
|
|
@@ -6392,7 +7292,15 @@ server.registerTool("export_midi", {
|
|
|
6392
7292
|
exportsFolder(),
|
|
6393
7293
|
]);
|
|
6394
7294
|
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
6395
|
-
const
|
|
7295
|
+
const transport = isRecord(transportRaw) ? transportRaw : {};
|
|
7296
|
+
const bpm = Number(transport.bpm) || Number(state.bpm) || 120;
|
|
7297
|
+
// A build whose getTransport predates the meter writes 4/4, as it always did.
|
|
7298
|
+
const numerator = Math.round(Number(transport.timeSigNumerator));
|
|
7299
|
+
const denominatorPower = Math.round(Math.log2(Number(transport.timeSigDenominator)));
|
|
7300
|
+
const meter = numerator >= 1 && numerator <= 32 && denominatorPower >= 0 && denominatorPower <= 5
|
|
7301
|
+
? { numerator, denominator: 2 ** denominatorPower }
|
|
7302
|
+
: { numerator: 4, denominator: 4 };
|
|
7303
|
+
const barBeats = beatsPerBar(meter);
|
|
6396
7304
|
const dir = outputDir ? expandHome(outputDir) : exportsDir;
|
|
6397
7305
|
if (!dir)
|
|
6398
7306
|
return error("Could not resolve PAM's Exports folder from get_default_locations — pass outputDir.");
|
|
@@ -6406,6 +7314,10 @@ server.registerTool("export_midi", {
|
|
|
6406
7314
|
const playerIds = ids.filter((id) => Number(cellConfig(id).playerType) > 0);
|
|
6407
7315
|
// A GRAB follower plays its source's notes, so an empty own grid does not rule it out.
|
|
6408
7316
|
const followerIds = ids.filter((id) => Number(cellConfig(id).seqFrom) > 0);
|
|
7317
|
+
const ccIds = ids.filter((id) => CC_OUT_CONFIG_KEYS.some((key) => {
|
|
7318
|
+
const controller = cellConfig(id)[key];
|
|
7319
|
+
return typeof controller === "number" && Number.isInteger(controller) && controller >= 0 && controller <= 119;
|
|
7320
|
+
}));
|
|
6409
7321
|
const cells = ids
|
|
6410
7322
|
.map((id) => ({
|
|
6411
7323
|
id,
|
|
@@ -6413,17 +7325,31 @@ server.registerTool("export_midi", {
|
|
|
6413
7325
|
patternLength: state[`cell${id}_patternLength`],
|
|
6414
7326
|
speed: state[`cell${id}_patternSpeed`],
|
|
6415
7327
|
}))
|
|
6416
|
-
.filter((cell) => (Array.isArray(cell.sequence) && cell.sequence.length > 0) ||
|
|
7328
|
+
.filter((cell) => (Array.isArray(cell.sequence) && cell.sequence.length > 0) ||
|
|
7329
|
+
playerIds.includes(cell.id) ||
|
|
7330
|
+
followerIds.includes(cell.id) ||
|
|
7331
|
+
ccIds.includes(cell.id));
|
|
6417
7332
|
if (cells.length === 0)
|
|
6418
|
-
return error("
|
|
6419
|
-
const
|
|
6420
|
-
const
|
|
6421
|
-
const speedIndex = Math.round(Number(
|
|
7333
|
+
return error("Nothing to export — the selected cells carry no notes, run no Player and send no CC out.");
|
|
7334
|
+
const cycleOf = (length, speed) => {
|
|
7335
|
+
const bars = Number(length);
|
|
7336
|
+
const speedIndex = Math.round(Number(speed));
|
|
6422
7337
|
const mul = Number.isFinite(speedIndex) ? patternSpeedMultiplier(speedIndex) : 1;
|
|
6423
|
-
return (Number.isFinite(
|
|
6424
|
-
}
|
|
6425
|
-
|
|
6426
|
-
const
|
|
7338
|
+
return (Number.isFinite(bars) && bars > 0 ? bars : 4) / mul / barBeats;
|
|
7339
|
+
};
|
|
7340
|
+
// A CC lane loops on its own clock, so its cycle counts toward the default length too.
|
|
7341
|
+
const cycleBars = cells.flatMap((cell) => [
|
|
7342
|
+
cycleOf(cell.patternLength, cell.speed),
|
|
7343
|
+
...CC_LANES.map(ccLaneIndex)
|
|
7344
|
+
.filter((lane) => {
|
|
7345
|
+
const sequence = state[`cell${cell.id}_ccOut${lane}Sequence`];
|
|
7346
|
+
return Array.isArray(sequence) && sequence.length > 0;
|
|
7347
|
+
})
|
|
7348
|
+
.map((lane) => cycleOf(state[`cell${cell.id}_patternLengthCc${lane}`], state[`cell${cell.id}_patternSpeedCc${lane}`])),
|
|
7349
|
+
]);
|
|
7350
|
+
const maxBars = Math.max(1, Math.floor(MAX_MIDI_EXPORT_BEATS / barBeats));
|
|
7351
|
+
const totalBars = Math.min(maxBars, bars ?? Math.max(1, Math.ceil(Math.max(...cycleBars))));
|
|
7352
|
+
const totalBeats = totalBars * barBeats;
|
|
6427
7353
|
// PAM renders the notes itself (Player cycles included); a build without the route
|
|
6428
7354
|
// falls back to tiling the drawn grids here. Any other failure is reported, not tiled.
|
|
6429
7355
|
let native;
|
|
@@ -6438,48 +7364,61 @@ server.registerTool("export_midi", {
|
|
|
6438
7364
|
const nativeCells = isRecord(native) && Array.isArray(native.cells) ? native.cells.filter(isRecord) : undefined;
|
|
6439
7365
|
const staleBuildNote = !nativeCells && playerIds.length > 0
|
|
6440
7366
|
? `This PAM build predates Player-aware export: cell ${playerIds.join(", ")} exported its drawn grid, not the Player. Update PAM.`
|
|
6441
|
-
:
|
|
7367
|
+
: !nativeCells && ccIds.length > 0
|
|
7368
|
+
? `This PAM build predates CC export: cell ${ccIds.join(", ")} left its CC out behind. Update PAM.`
|
|
7369
|
+
: undefined;
|
|
6442
7370
|
const tracks = cells
|
|
6443
7371
|
.map((cell) => {
|
|
6444
7372
|
const rendered = nativeCells?.find((entry) => Number(entry.cellId) === cell.id);
|
|
6445
|
-
|
|
7373
|
+
// Only a build without the route lands here without channels; it predates MSQ's output channel.
|
|
7374
|
+
const fallbackChannel = Math.min(16, cell.id);
|
|
6446
7375
|
const events = rendered && Array.isArray(rendered.events)
|
|
6447
7376
|
? rendered.events.filter(isRecord).map((e) => ({
|
|
6448
|
-
channel,
|
|
7377
|
+
channel: Number(e.channel) || fallbackChannel,
|
|
6449
7378
|
note: Number(e.note),
|
|
6450
7379
|
velocity: Number(e.velocity),
|
|
6451
7380
|
startBeats: Number(e.startBeats),
|
|
6452
7381
|
durationBeats: Number(e.durationBeats),
|
|
6453
7382
|
}))
|
|
6454
|
-
: tilePatternEvents(cell.sequence, cell.patternLength, cell.speed, totalBeats,
|
|
6455
|
-
|
|
7383
|
+
: tilePatternEvents(cell.sequence, cell.patternLength, cell.speed, totalBeats, fallbackChannel);
|
|
7384
|
+
const cc = rendered && Array.isArray(rendered.cc)
|
|
7385
|
+
? rendered.cc.filter(isRecord).map((e) => ({
|
|
7386
|
+
channel: Number(e.channel) || fallbackChannel,
|
|
7387
|
+
controller: Number(e.controller),
|
|
7388
|
+
value: Number(e.value),
|
|
7389
|
+
beats: Number(e.beats),
|
|
7390
|
+
}))
|
|
7391
|
+
: [];
|
|
7392
|
+
return { name: `Cell ${cell.id}`, events, cc };
|
|
6456
7393
|
})
|
|
6457
|
-
.filter((track) => track.events.length > 0);
|
|
7394
|
+
.filter((track) => track.events.length > 0 || track.cc.length > 0);
|
|
6458
7395
|
if (tracks.length === 0)
|
|
6459
|
-
return error(staleBuildNote ?? "
|
|
7396
|
+
return error(staleBuildNote ?? "Nothing to export — the selected cells rendered no notes or CC over that length.");
|
|
6460
7397
|
const notes = [];
|
|
6461
7398
|
if (staleBuildNote)
|
|
6462
7399
|
notes.push(staleBuildNote);
|
|
6463
7400
|
const listening = playerIds.filter((id) => Number(cellConfig(id).playerListenMode) > 0 && cells.some((cell) => cell.id === id));
|
|
6464
7401
|
if (nativeCells && listening.length > 0)
|
|
6465
7402
|
notes.push(`Cell ${listening.join(", ")} listens to other cells; the file carries its cycles without that response.`);
|
|
6466
|
-
const
|
|
7403
|
+
const product = manifest && isMsqManifest(manifest) ? "msq" : "pam";
|
|
7404
|
+
const base = (filename?.trim() || `${product}_midi_${Date.now()}`).replace(/\.mid$/i, "").replace(/[\\/:]/g, "_");
|
|
6467
7405
|
await mkdir(dir, { recursive: true });
|
|
6468
7406
|
const written = [];
|
|
6469
7407
|
if (mode === "stems") {
|
|
6470
7408
|
for (const track of tracks) {
|
|
6471
7409
|
const path = join(dir, `${base}_cell${track.name.slice(5)}.mid`);
|
|
6472
|
-
await writeFile(path, buildMidiFile(bpm, [track], track.name));
|
|
7410
|
+
await writeFile(path, buildMidiFile(bpm, [track], track.name, meter, totalBeats));
|
|
6473
7411
|
written.push(path);
|
|
6474
7412
|
}
|
|
6475
7413
|
}
|
|
6476
7414
|
else {
|
|
6477
7415
|
const path = join(dir, `${base}.mid`);
|
|
6478
|
-
await writeFile(path, buildMidiFile(bpm, tracks, base));
|
|
7416
|
+
await writeFile(path, buildMidiFile(bpm, tracks, base, meter, totalBeats));
|
|
6479
7417
|
written.push(path);
|
|
6480
7418
|
}
|
|
6481
7419
|
const noteCount = tracks.reduce((sum, track) => sum + track.events.length, 0);
|
|
6482
|
-
|
|
7420
|
+
const ccCount = tracks.reduce((sum, track) => sum + track.cc.length, 0);
|
|
7421
|
+
return text(`Exported ${noteCount} notes${ccCount > 0 ? ` and ${ccCount} CC messages` : ""} from ${tracks.length} cell${tracks.length === 1 ? "" : "s"} over ${totalBars} bar${totalBars === 1 ? "" : "s"}${meter.numerator === 4 && meter.denominator === 4 ? "" : ` of ${meter.numerator}/${meter.denominator}`} at ${bpm} BPM.\n` +
|
|
6483
7422
|
[...written.map((path) => `File: ${path}`), ...notes].join("\n"));
|
|
6484
7423
|
});
|
|
6485
7424
|
server.registerTool("remove_param_seq", {
|
|
@@ -6596,6 +7535,7 @@ async function pollExportStatus(maxWaitMs = 180_000, pollMs = 500) {
|
|
|
6596
7535
|
const status = (await bridge.send("getExportStatus"));
|
|
6597
7536
|
if (status.complete) {
|
|
6598
7537
|
lastExportLoudness = status.loudness ?? null;
|
|
7538
|
+
lastExportError = typeof status.error === "string" ? status.error : null;
|
|
6599
7539
|
return status.success ? "success" : "failed";
|
|
6600
7540
|
}
|
|
6601
7541
|
}
|
|
@@ -6603,6 +7543,11 @@ async function pollExportStatus(maxWaitMs = 180_000, pollMs = 500) {
|
|
|
6603
7543
|
}
|
|
6604
7544
|
// Loudness report of the last completed bounce (set by pollExportStatus).
|
|
6605
7545
|
let lastExportLoudness = null;
|
|
7546
|
+
// Native reason for the last failed bounce, when the plugin reports one.
|
|
7547
|
+
let lastExportError = null;
|
|
7548
|
+
function exportFailureDetail() {
|
|
7549
|
+
return lastExportError ? ` ${lastExportError}` : "";
|
|
7550
|
+
}
|
|
6606
7551
|
function loudnessReportLine() {
|
|
6607
7552
|
const l = lastExportLoudness;
|
|
6608
7553
|
if (!l || typeof l.i !== "number")
|
|
@@ -6620,7 +7565,8 @@ function loudnessReportLine() {
|
|
|
6620
7565
|
}
|
|
6621
7566
|
server.registerTool("export_audio", {
|
|
6622
7567
|
title: "Export Audio",
|
|
6623
|
-
description: "Render and export audio from the current preset to a WAV file in PAM's Exports folder. " +
|
|
7568
|
+
description: "Render and export audio from the current preset in a running PAM instance to a WAV file in PAM's Exports folder. " +
|
|
7569
|
+
"For a saved preset with no PAM open, use the paid `preset render` command through `pam mcp`'s `run` tool; it supports master, stems, variations and songs. " +
|
|
6624
7570
|
"Waits for completion. Default mode renders the looping preset for `bars` bars at the current BPM. " +
|
|
6625
7571
|
"Set `mode` to bounce stems or the whole Song Mode arrangement.\n\n" +
|
|
6626
7572
|
"Modes: 'main' (full mix, default) · 'individual' (one cell solo, needs cellId) · " +
|
|
@@ -6678,7 +7624,7 @@ server.registerTool("export_audio", {
|
|
|
6678
7624
|
`Duration: ${durationSecs.toFixed(1)}s (${bars} bars at ${bpm} BPM, ${bitDepth}-bit)` +
|
|
6679
7625
|
loudnessReportLine());
|
|
6680
7626
|
if (outcome === "failed")
|
|
6681
|
-
return error(
|
|
7627
|
+
return error(`Audio export failed.${exportFailureDetail()}`);
|
|
6682
7628
|
return text(`Export timed out. The render may still be running.\nExpected output: ${outputPath}`);
|
|
6683
7629
|
}
|
|
6684
7630
|
// Stem / song-mode modes: build full pass-through options (filename + mode +
|
|
@@ -6709,7 +7655,7 @@ server.registerTool("export_audio", {
|
|
|
6709
7655
|
`File(s) written to ${SERVER_PRODUCT.name}'s Exports folder, base name: ${filename}.wav` +
|
|
6710
7656
|
loudnessReportLine());
|
|
6711
7657
|
if (outcome === "failed")
|
|
6712
|
-
return error(`Export failed (mode '${effectiveMode}')
|
|
7658
|
+
return error(`Export failed (mode '${effectiveMode}').${exportFailureDetail()}`);
|
|
6713
7659
|
return text(`Export timed out (mode '${effectiveMode}'). The render may still be running.`);
|
|
6714
7660
|
});
|
|
6715
7661
|
// ============================================================
|
|
@@ -6761,7 +7707,9 @@ server.registerResource("cell-parameters", new ResourceTemplate(`${SERVER_PRODUC
|
|
|
6761
7707
|
});
|
|
6762
7708
|
server.registerResource("global-parameters", `${SERVER_PRODUCT.resourceScheme}://parameters/global`, {
|
|
6763
7709
|
title: "Global Parameters",
|
|
6764
|
-
description:
|
|
7710
|
+
description: IS_MSQ_SERVER
|
|
7711
|
+
? "Non-cell-specific parameters (LFOs, macros, XY, pattern selectors)."
|
|
7712
|
+
: "Non-cell-specific parameters (volume, crossfade, looper, etc.).",
|
|
6765
7713
|
mimeType: "application/json",
|
|
6766
7714
|
}, async (uri) => {
|
|
6767
7715
|
const params = await getGlobalParameters();
|
|
@@ -6790,7 +7738,7 @@ MFX has four effects cells. Audio reaches a cell only when the cable patch route
|
|
|
6790
7738
|
}],
|
|
6791
7739
|
}));
|
|
6792
7740
|
}
|
|
6793
|
-
else {
|
|
7741
|
+
else if (!IS_MSQ_SERVER) {
|
|
6794
7742
|
server.registerResource("beat-making-guide", "pam://guide/beat-making", {
|
|
6795
7743
|
title: "PAM Beat-Making Guide",
|
|
6796
7744
|
description: "Comprehensive guide for creating beats with PAM. Covers cell roles, slicer for loops, " +
|
|
@@ -7186,12 +8134,37 @@ if (IS_MFX_SERVER) {
|
|
|
7186
8134
|
description: "Diagnose why live tools cannot reach MFX.",
|
|
7187
8135
|
argsSchema: {},
|
|
7188
8136
|
}, () => promptText(`Diagnose the MFX MCP connection:\n\n` +
|
|
7189
|
-
`1. Run connection_status. If no instance appears, start MFX and enable Settings →
|
|
8137
|
+
`1. Run connection_status. If no instance appears, start MFX and enable Settings → Integrations → MCP Bridge.\n` +
|
|
7190
8138
|
`2. MFX starts at the configured port and automatically takes the next available port when needed; do not assign a second fixed product port.\n` +
|
|
7191
8139
|
`3. If an instance appears but cannot connect, it may have closed or be busy with a render; retry once.\n` +
|
|
7192
8140
|
`4. If a live tool is unavailable, update the MFX plugin as well as the mfx-mcp package.`));
|
|
7193
8141
|
}
|
|
7194
|
-
if (
|
|
8142
|
+
if (IS_MSQ_SERVER) {
|
|
8143
|
+
server.registerPrompt("sequence-a-part", {
|
|
8144
|
+
title: "Sequence a Part",
|
|
8145
|
+
description: "Turn a musical brief into cells that drive the host's instruments, then save it.",
|
|
8146
|
+
argsSchema: {
|
|
8147
|
+
brief: z.string().describe("Musical brief, e.g. 'rolling acid bass on channel 2 with a filter sweep'."),
|
|
8148
|
+
},
|
|
8149
|
+
}, ({ brief }) => promptText(`Sequence this in MSQ: "${brief}"\n\n` +
|
|
8150
|
+
`1. connection_status, then get_design_overview for what each cell sends now.\n` +
|
|
8151
|
+
`2. set_bpm, and configure_transport for the time signature if the brief needs one.\n` +
|
|
8152
|
+
`3. Wire each cell with update_cell_config: output channel (0 = Auto, the cell number) matching the instrument that should play it, trigger note, poly and scale, Send on.\n` +
|
|
8153
|
+
`4. Notes: update_pattern (type 'note'), or set_player for a generated part (list_players first); randomize target:'sequences' gives a starting point.\n` +
|
|
8154
|
+
`5. CC: update_cell_config ccOutCc1-16 picks controllers, set_parameters cell{N}_ccOut{K} sets their values, update_pattern with type 'ccOut1'-'ccOut4' draws CC lanes. add_modulation adds movement.\n` +
|
|
8155
|
+
`6. start_transport: notes leave only while it rolls.\n` +
|
|
8156
|
+
`7. save_preset and update_preset_metadata; export_midi writes the parts to a MIDI file.`));
|
|
8157
|
+
server.registerPrompt("troubleshoot-bridge", {
|
|
8158
|
+
title: "Troubleshoot the MSQ Bridge",
|
|
8159
|
+
description: "Diagnose why live tools cannot reach MSQ, or why its instruments stay silent.",
|
|
8160
|
+
argsSchema: {},
|
|
8161
|
+
}, () => promptText(`Diagnose the MSQ MCP connection:\n\n` +
|
|
8162
|
+
`1. Run connection_status. If no instance appears, start MSQ and enable Settings → Integrations → MCP Bridge.\n` +
|
|
8163
|
+
`2. MSQ records its actual port on startup. If an instance appears but cannot connect, it may have closed; retry once.\n` +
|
|
8164
|
+
`3. MSQ makes no sound itself. If nothing plays, check each cell's Send and channel with get_design_overview, that the host routes MSQ's MIDI out to an instrument listening on that channel, and that the transport is rolling.\n` +
|
|
8165
|
+
`4. If a live tool is unavailable, update the MSQ plugin as well as the msq-mcp package.`));
|
|
8166
|
+
}
|
|
8167
|
+
if (SERVER_PRODUCT.id === "pam")
|
|
7195
8168
|
server.registerPrompt("build-a-beat", {
|
|
7196
8169
|
title: "Build a Beat",
|
|
7197
8170
|
description: "Guided kit-building + pattern workflow: coherent samples, genre patterns, transport, refinement, save.",
|
|
@@ -7206,7 +8179,7 @@ if (!IS_MFX_SERVER)
|
|
|
7206
8179
|
`4. start_transport — sequencer output needs the transport rolling.\n` +
|
|
7207
8180
|
`5. Refine: update_pattern for individual lanes, set_parameters for levels/filters (ranges via design_context), update_effect_config to switch FX on, add_modulation for movement. For a groove that evolves, set_player (player 'role' across the kit, or KIT / EUCLID / RIFF per cell — list_players first${genre ? `, genre '${genre}'` : ""}).\n` +
|
|
7208
8181
|
`6. save_preset once it works, and update_preset_metadata with genre tags and BPM.`));
|
|
7209
|
-
if (
|
|
8182
|
+
if (SERVER_PRODUCT.id === "pam")
|
|
7210
8183
|
server.registerPrompt("make-a-song", {
|
|
7211
8184
|
title: "Make a Song",
|
|
7212
8185
|
description: "Arrange the current preset into sections with variations + Song Mode, then bounce a full track.",
|
|
@@ -7219,7 +8192,7 @@ if (!IS_MFX_SERVER)
|
|
|
7219
8192
|
`3. Audition: start_transport with Song Mode driving the timeline; queue_variation switches sections live.\n` +
|
|
7220
8193
|
`4. Bounce with export_song — strategy 'live' captures the ground-truth master mix (required for Magenta/Notochord/automation), strategy 'offline' is the deterministic multi-section bounce with no bar cap. includeVideo:true films the VX scene (window must be open).\n` +
|
|
7221
8194
|
`5. The rendered file lands in PAM's Exports/Resamples locations — get_default_locations resolves them.`));
|
|
7222
|
-
if (
|
|
8195
|
+
if (SERVER_PRODUCT.id === "pam")
|
|
7223
8196
|
server.registerPrompt("design-a-preset", {
|
|
7224
8197
|
title: "Design a Preset",
|
|
7225
8198
|
description: "Sound-design workflow from a creative brief to a saved preset.",
|
|
@@ -7233,7 +8206,7 @@ if (!IS_MFX_SERVER)
|
|
|
7233
8206
|
`4. Add movement: add_modulation (LFO / envelope / vary), add_param_seq for stepped motion.\n` +
|
|
7234
8207
|
`5. Write patterns with update_pattern, then start_transport to audition.\n` +
|
|
7235
8208
|
`6. save_preset with a fitting name and update_preset_metadata (category, tags, BPM).`));
|
|
7236
|
-
if (
|
|
8209
|
+
if (SERVER_PRODUCT.id === "pam")
|
|
7237
8210
|
server.registerPrompt("mix-and-master", {
|
|
7238
8211
|
title: "Mix and Master",
|
|
7239
8212
|
description: "Balance the cells and shape the master bus using the analysis + suggestion tools.",
|
|
@@ -7244,13 +8217,13 @@ if (!IS_MFX_SERVER)
|
|
|
7244
8217
|
`3. Shape the master bus with set_master_effects (compressor, tape, limiter — design_context lists the keys).\n` +
|
|
7245
8218
|
`4. Re-check get_master_analysis and iterate toward the loudness target.\n` +
|
|
7246
8219
|
`5. Render with export_audio (mode 'main'; 'all-cells' for stems).`));
|
|
7247
|
-
if (
|
|
8220
|
+
if (SERVER_PRODUCT.id === "pam")
|
|
7248
8221
|
server.registerPrompt("troubleshoot-bridge", {
|
|
7249
8222
|
title: "Troubleshoot the PAM Bridge",
|
|
7250
8223
|
description: "Diagnose why live tools can't reach the plugin.",
|
|
7251
8224
|
argsSchema: {},
|
|
7252
8225
|
}, () => promptText(`Diagnose the PAM MCP connection:\n\n` +
|
|
7253
|
-
`1. connection_status. No instances → PAM isn't running or the bridge is off (PAM Settings →
|
|
8226
|
+
`1. connection_status. No instances → PAM isn't running or the bridge is off (PAM Settings → Integrations → MCP Bridge, default port 9847). Multiple instances → select_instance picks which product the live tools drive.\n` +
|
|
7254
8227
|
`2. Instances found but not connected → the plugin may have shut down, or its message thread is busy (heavy render); retry once.\n` +
|
|
7255
8228
|
`3. Timeouts on WebView-routed tools (video tools, capture_ui, set_drum_synth_wave, randomize target 'synth') → the plugin window must be OPEN.\n` +
|
|
7256
8229
|
`4. "PAM build does not support 'X'" errors → the plugin is older than this server; the user must update PAM (updating the npm package alone is not enough).\n` +
|
|
@@ -7661,6 +8634,18 @@ server.registerTool("looper_variation_sync", {
|
|
|
7661
8634
|
// ============================================================
|
|
7662
8635
|
// TOOLS — Bar-synced variation scheduling + variation hold
|
|
7663
8636
|
// ============================================================
|
|
8637
|
+
// Shared by queue_variation and the song_mode transition schema below.
|
|
8638
|
+
const swapSpecSchema = z.object({
|
|
8639
|
+
order: z
|
|
8640
|
+
.array(z.number().int().min(1).max(16))
|
|
8641
|
+
.refine((order) => new Set(order).size === order.length, { message: "swap.order must not repeat a cell" })
|
|
8642
|
+
.optional()
|
|
8643
|
+
.describe("Cell Swap hand-over order as 1-based cell numbers (leftmost swaps first); unlisted cells follow in ascending order. Omit or [] for left to right."),
|
|
8644
|
+
timing: z
|
|
8645
|
+
.enum(["immediate", "step", "bar"])
|
|
8646
|
+
.optional()
|
|
8647
|
+
.describe("When a cell whose threshold the curve crossed hands over: 'immediate' at once (default), 'step' at the next 16th-note step, 'bar' at the next bar line, both counted back from the transition's anchor bar."),
|
|
8648
|
+
});
|
|
7664
8649
|
server.registerTool("queue_variation", {
|
|
7665
8650
|
title: "Queue Variation Switch",
|
|
7666
8651
|
description: "Schedule a global variation switch on the next bar boundary. " +
|
|
@@ -7682,9 +8667,9 @@ server.registerTool("queue_variation", {
|
|
|
7682
8667
|
.optional()
|
|
7683
8668
|
.describe("Pre-anchor morph window in bars for this flip. Omit all transition options to inherit native settings."),
|
|
7684
8669
|
morphMode: z
|
|
7685
|
-
.enum(["auto", "param", "mix", "seq", "all"])
|
|
8670
|
+
.enum(["auto", "param", "mix", "seq", "all", "swap"])
|
|
7686
8671
|
.optional()
|
|
7687
|
-
.describe("Morph style: 'param' glides continuous params (default); 'mix' equal-power blends the two sections; 'seq' adds a staged pattern dissolve to the glide (old notes lose trigger probability as new notes gain it); 'all' runs glide + dissolve + blend together; 'auto' lets the engine pick — mix when the sections differ in graph topology (samples, synth engines, routing) or when nothing can glide, param otherwise."),
|
|
8672
|
+
.describe("Morph style: 'param' glides continuous params (default); 'mix' equal-power blends the two sections; 'seq' adds a staged pattern dissolve to the glide (old notes lose trigger probability as new notes gain it); 'all' runs glide + dissolve + blend together; 'swap' hands whole cells over one by one across the window (PAM only; order via transition.swap.order); 'auto' lets the engine pick — mix when the sections differ in graph topology (samples, synth engines, routing) or when nothing can glide, param otherwise."),
|
|
7688
8673
|
morphCurve: z
|
|
7689
8674
|
.enum(["linear", "ease", "exp", "log"])
|
|
7690
8675
|
.optional()
|
|
@@ -7695,11 +8680,14 @@ server.registerTool("queue_variation", {
|
|
|
7695
8680
|
mixLaw: z.enum(["equalPower", "equalGain", "correlation"]).optional(),
|
|
7696
8681
|
tail: z.enum(["cut", "fade", "ring"]).optional(),
|
|
7697
8682
|
tailMaxBars: z.number().min(0.25).max(8).optional(),
|
|
8683
|
+
swap: swapSpecSchema
|
|
8684
|
+
.optional()
|
|
8685
|
+
.describe("Cell Swap settings for a swap morph: order (1-based cells, leftmost first; unlisted cells follow ascending) and timing."),
|
|
7698
8686
|
})
|
|
7699
8687
|
.optional()
|
|
7700
8688
|
.describe("Advanced transition settings for this flip: pattern policy for a " +
|
|
7701
|
-
"seq/all morph, blend gain law for a mix/all morph, and the outgoing section's " +
|
|
7702
|
-
"tail. Omitted fields keep the defaults (dissolve, equalPower, fade)."),
|
|
8689
|
+
"seq/all morph, blend gain law for a mix/all morph, the Cell Swap order for a swap morph, and the outgoing section's " +
|
|
8690
|
+
"tail. Omitted fields keep the defaults (dissolve, equalPower, left to right, fade)."),
|
|
7703
8691
|
},
|
|
7704
8692
|
}, async ({ index, intervalBars, fadeMs, resetPhase, morphBars, morphMode, morphCurve, transition }) => {
|
|
7705
8693
|
const check = await requireBridge();
|
|
@@ -7937,7 +8925,7 @@ server.registerTool("update_modulation", {
|
|
|
7937
8925
|
// ============================================================
|
|
7938
8926
|
const morphCurveEnum = z.enum(["linear", "ease", "exp", "log"]);
|
|
7939
8927
|
const transitionSpecSchema = z.object({
|
|
7940
|
-
mode: z.enum(["auto", "param", "mix", "seq", "all"]).optional(),
|
|
8928
|
+
mode: z.enum(["auto", "param", "mix", "seq", "all", "swap"]).optional(),
|
|
7941
8929
|
window: z
|
|
7942
8930
|
.object({
|
|
7943
8931
|
bars: z.number().min(0).max(8).optional(),
|
|
@@ -7959,6 +8947,7 @@ const transitionSpecSchema = z.object({
|
|
|
7959
8947
|
})
|
|
7960
8948
|
.optional(),
|
|
7961
8949
|
phase: z.enum(["inherit", "reset", "continue"]).optional(),
|
|
8950
|
+
swap: swapSpecSchema.optional().describe("Cell Swap settings (mode 'swap'; PAM only)."),
|
|
7962
8951
|
});
|
|
7963
8952
|
const TRANSITION_SPEC_HONESTY = "Advanced transition spec: pattern.policy " +
|
|
7964
8953
|
"('dissolve' prob-morphs events, 'wipe' replaces them at a moving frontier, 'cells' hands " +
|
|
@@ -7966,7 +8955,7 @@ const TRANSITION_SPEC_HONESTY = "Advanced transition spec: pattern.policy " +
|
|
|
7966
8955
|
"correlated sections, 'correlation' auto-adapts), audio.tail ('fade' default, 'cut', 'ring' " +
|
|
7967
8956
|
"lets voices/FX decay up to tailMaxBars), phase ('reset'/'continue' overrides the song's " +
|
|
7968
8957
|
"Reset Phase per cable), window.patternCurve/mixCurve (per-domain easing; patternCurve " +
|
|
7969
|
-
"defaults linear). window.staggerBy (off/cell/category), staggerSpread (0..0.9), staggerOrder (forward/reverse), and relations (off/stepped) shape the parameter glide. Runs on variation sections + queued flips, and on preset destinations " +
|
|
8958
|
+
"defaults linear). window.staggerBy (off/cell/category), staggerSpread (0..0.9), staggerOrder (forward/reverse), and relations (off/stepped) shape the parameter glide. swap.order (1-based cells) sets the Cell Swap hand-over order and swap.timing (immediate/step/bar) when a crossed cell hands over, for mode 'swap', which swaps whole cells one by one along the curve (PAM only). Runs on variation sections + queued flips, and on preset destinations " +
|
|
7970
8959
|
"when the lanePresetSections setting is on (the dual-lane preset section " +
|
|
7971
8960
|
"pipeline; with it off, preset destinations coerce to a param glide). Offline bounces " +
|
|
7972
8961
|
"coerce ring→fade, blend→hard switch. 'get' reads back each cable's resolvedTransition " +
|
|
@@ -8033,9 +9022,9 @@ server.registerTool("song_mode", {
|
|
|
8033
9022
|
.optional()
|
|
8034
9023
|
.describe("Morph glide easing; default ease. Ignored without morphBars."),
|
|
8035
9024
|
morphMode: z
|
|
8036
|
-
.enum(["auto", "param", "mix", "seq", "all"])
|
|
9025
|
+
.enum(["auto", "param", "mix", "seq", "all", "swap"])
|
|
8037
9026
|
.optional()
|
|
8038
|
-
.describe("Morph realization; default auto — the engine picks mix when the sections differ in graph topology (samples, synth engines, routing) or when nothing can glide, param otherwise. 'param' glides continuous params; 'mix' DJ-blends stems; 'seq' adds a staged pattern dissolve to the glide; 'all' runs glide + dissolve + blend. Ignored without morphBars."),
|
|
9027
|
+
.describe("Morph realization; default auto — the engine picks mix when the sections differ in graph topology (samples, synth engines, routing) or when nothing can glide, param otherwise. 'param' glides continuous params; 'mix' DJ-blends stems; 'seq' adds a staged pattern dissolve to the glide; 'all' runs glide + dissolve + blend; 'swap' hands whole cells over one by one (PAM only). Ignored without morphBars."),
|
|
8039
9028
|
transition: transitionSpecSchema.optional().describe(TRANSITION_SPEC_HONESTY),
|
|
8040
9029
|
transitionPreset: z
|
|
8041
9030
|
.string()
|
|
@@ -8079,7 +9068,7 @@ server.registerTool("song_mode", {
|
|
|
8079
9068
|
.optional()
|
|
8080
9069
|
.describe("Per-cable morph easing; overrides the destination section's morphCurve."),
|
|
8081
9070
|
morphMode: z
|
|
8082
|
-
.enum(["auto", "param", "mix", "seq", "all"])
|
|
9071
|
+
.enum(["auto", "param", "mix", "seq", "all", "swap"])
|
|
8083
9072
|
.optional()
|
|
8084
9073
|
.describe("Per-cable morph realization; overrides the destination section's morphMode. Same modes as the section field, incl. 'seq' (pattern dissolve) and 'all' (glide + dissolve + blend)."),
|
|
8085
9074
|
transition: transitionSpecSchema.optional().describe("Per-cable " + TRANSITION_SPEC_HONESTY),
|
|
@@ -8259,9 +9248,118 @@ server.registerTool("song_mode", {
|
|
|
8259
9248
|
}
|
|
8260
9249
|
return text("No-op");
|
|
8261
9250
|
});
|
|
9251
|
+
// TOOLS — DJ set (docs/plans/DJ_SINGLE_TIMELINE.md D23)
|
|
9252
|
+
const djBarsSchema = z.union([z.literal(1), z.literal(2), z.literal(4), z.literal(8), z.literal(16)]);
|
|
9253
|
+
// shared/dj/set.schema.json's longest window and ring tail, past song_mode's 8 (check-parity compares them).
|
|
9254
|
+
const DJ_TRANSITION_BARS_MAX = 32;
|
|
9255
|
+
// shared/dj/set.schema.json closes every object, so song_mode's spec is closed here section by section.
|
|
9256
|
+
const djTransitionSpecSchema = transitionSpecSchema
|
|
9257
|
+
.extend({
|
|
9258
|
+
window: transitionSpecSchema.shape.window.unwrap()
|
|
9259
|
+
.extend({ bars: z.number().min(0).max(DJ_TRANSITION_BARS_MAX).optional() })
|
|
9260
|
+
.strict()
|
|
9261
|
+
.optional(),
|
|
9262
|
+
pattern: transitionSpecSchema.shape.pattern.unwrap().strict().optional(),
|
|
9263
|
+
audio: transitionSpecSchema.shape.audio.unwrap()
|
|
9264
|
+
.extend({ tailMaxBars: z.number().min(0.25).max(DJ_TRANSITION_BARS_MAX).optional() })
|
|
9265
|
+
.strict()
|
|
9266
|
+
.optional(),
|
|
9267
|
+
swap: swapSpecSchema.strict().optional().describe("Cell Swap settings (mode 'swap')."),
|
|
9268
|
+
})
|
|
9269
|
+
.strict();
|
|
9270
|
+
const djTransitionSchema = z
|
|
9271
|
+
.object({
|
|
9272
|
+
preset: z.string().min(1).max(64).optional().describe("Transition preset id (drop, rise, break, blend, sweep, fill, or a saved one), stamped first."),
|
|
9273
|
+
spec: djTransitionSpecSchema.optional().describe("Advanced transition spec, merged over the preset one level deep in each section."),
|
|
9274
|
+
bars: z.number().min(0).max(DJ_TRANSITION_BARS_MAX).optional().describe(`Window length in bars, 0 to ${DJ_TRANSITION_BARS_MAX}; 0 is a cut.`),
|
|
9275
|
+
mode: z.enum(["auto", "param", "mix", "seq", "all", "swap"]).optional().describe("AUTO, GLIDE (param), BLEND (mix), STEPS (seq), FULL (all) or SWAP."),
|
|
9276
|
+
curve: morphCurveEnum.optional(),
|
|
9277
|
+
})
|
|
9278
|
+
.strict();
|
|
9279
|
+
const djSetDocumentSchema = z
|
|
9280
|
+
.object({
|
|
9281
|
+
queue: z
|
|
9282
|
+
.array(z
|
|
9283
|
+
.object({
|
|
9284
|
+
presetPath: z.string().min(1).max(4096),
|
|
9285
|
+
variationIndex: z.number().int().min(0).max(7).optional().describe("Variation slot; absent plays the preset's first populated one."),
|
|
9286
|
+
bars: z.number().int().min(1).max(64).optional().describe("The item's own length; absent follows stepBars."),
|
|
9287
|
+
transition: djTransitionSchema.optional().describe("The boundary into this item; on queue[0], used only when it queues behind another NOW."),
|
|
9288
|
+
})
|
|
9289
|
+
.strict())
|
|
9290
|
+
.min(1)
|
|
9291
|
+
.max(65)
|
|
9292
|
+
.describe("queue[0] is NOW; the rest play in order."),
|
|
9293
|
+
stepBars: djBarsSchema.optional().describe("Bars each item plays under Auto (default 4)."),
|
|
9294
|
+
launchBars: djBarsSchema.optional().describe("The grid boundaries land on (default 1)."),
|
|
9295
|
+
auto: z.boolean().optional().describe("Step to the next item every stepBars (default true); off holds NOW until a Go."),
|
|
9296
|
+
autoMix: z.boolean().optional().describe("Auto mix: after its dwell, fill NEXT from the library and go (default false)."),
|
|
9297
|
+
defaultTransition: djTransitionSchema.optional().describe("Every boundary without its own (default AUTO, 8 bars, ease); bars, mode or curve alone edit that default."),
|
|
9298
|
+
tempo: z
|
|
9299
|
+
.object({
|
|
9300
|
+
source: z.enum(["master", "followNext"]).optional().describe("The master tempo, or glide to each NEXT's preset tempo."),
|
|
9301
|
+
glideBars: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(4), z.literal(8)]).optional(),
|
|
9302
|
+
bpm: z.number().min(20).max(400).optional().describe("Master tempo to glide to; refused while a DAW or MIDI clock owns it."),
|
|
9303
|
+
})
|
|
9304
|
+
.strict()
|
|
9305
|
+
.optional(),
|
|
9306
|
+
})
|
|
9307
|
+
.strict();
|
|
9308
|
+
const DJ_SET_BOUNDARY = z.object({}).passthrough();
|
|
9309
|
+
server.registerTool("dj_set", {
|
|
9310
|
+
title: "DJ Set",
|
|
9311
|
+
description: "Load a whole DJ set into PAM's DJ view in one call, or read it back (op 'get'). PAM only; the window must be open " +
|
|
9312
|
+
"and the experimental DJ view on (update_settings {experimentalMode:true, experimentalDjView:true}). queue[0] is " +
|
|
9313
|
+
"NOW. Stopped, the set starts on queue[0] (loading it); playing, nothing is cut: a queue[0] equal to NOW keeps " +
|
|
9314
|
+
"playing and the rest replace the queue, else every item queues behind NOW (nowMatched:false), behind any boundary " +
|
|
9315
|
+
"already under way (kept). With an open set, an omitted top-level field keeps its running value. play:true starts " +
|
|
9316
|
+
"the transport, so with auto (the default) the set runs. Pass `set` or the `presets` shorthand. The reply's `set` " +
|
|
9317
|
+
"is canonical: sending it back changes nothing. Failures are {ok:false, code, error}.",
|
|
9318
|
+
inputSchema: {
|
|
9319
|
+
op: z.enum(["set", "get"]).optional().describe("'set' (default) loads; 'get' reads the open set and takes no other field."),
|
|
9320
|
+
set: djSetDocumentSchema.optional(),
|
|
9321
|
+
presets: z.array(z.string().min(1).max(4096)).min(1).max(65).optional().describe("Shorthand: these presets in order, each at its first populated slot."),
|
|
9322
|
+
play: z.boolean().optional().describe("Start the transport after loading."),
|
|
9323
|
+
},
|
|
9324
|
+
outputSchema: {
|
|
9325
|
+
ok: z.boolean(),
|
|
9326
|
+
open: z.boolean().optional(),
|
|
9327
|
+
playing: z.boolean().optional(),
|
|
9328
|
+
bar: z.number().optional(),
|
|
9329
|
+
bpm: z.number().optional(),
|
|
9330
|
+
timeSig: z.array(z.number()).optional(),
|
|
9331
|
+
set: z.object({}).passthrough().nullable().optional(),
|
|
9332
|
+
now: z.object({ startBar: z.number() }).nullable().optional(),
|
|
9333
|
+
boundaries: z.array(DJ_SET_BOUNDARY).optional(),
|
|
9334
|
+
status: z.string().nullable().optional(),
|
|
9335
|
+
nowMatched: z.boolean().optional(),
|
|
9336
|
+
kept: z.number().optional(),
|
|
9337
|
+
code: z.string().optional(),
|
|
9338
|
+
error: z.string().optional(),
|
|
9339
|
+
},
|
|
9340
|
+
}, async ({ op, set, presets, play }) => {
|
|
9341
|
+
if ((op ?? "set") === "get" ? Boolean(set || presets) || play !== undefined : Boolean(set) === Boolean(presets))
|
|
9342
|
+
return packFailure("dj_invalid_set", op === "get" ? "op 'get' takes no other field" : "Pass exactly one of set and presets.");
|
|
9343
|
+
const check = await requireBridge();
|
|
9344
|
+
if (check !== true)
|
|
9345
|
+
return check;
|
|
9346
|
+
const product = await connectedProduct();
|
|
9347
|
+
if (product === "MFX" || product === "GATE" || product === "MSQ")
|
|
9348
|
+
return packFailure("dj_unsupported_product", `${product} has no DJ view; dj_set is a PAM feature.`);
|
|
9349
|
+
const reply = parseBridgeValue(await bridge.send("djSet", {
|
|
9350
|
+
...(op ? { op } : {}),
|
|
9351
|
+
...(set ? { set } : {}),
|
|
9352
|
+
...(presets ? { presets } : {}),
|
|
9353
|
+
...(play !== undefined ? { play } : {}),
|
|
9354
|
+
}));
|
|
9355
|
+
if (isPackFailure(reply))
|
|
9356
|
+
return packFailure(reply.code, reply.error);
|
|
9357
|
+
return structured(reply);
|
|
9358
|
+
});
|
|
8262
9359
|
server.registerTool("export_song", {
|
|
8263
9360
|
title: "Export Song (Full Track)",
|
|
8264
|
-
description: "Bounce the full Song Mode arrangement — the 'render my whole track' button. " +
|
|
9361
|
+
description: "Bounce the full Song Mode arrangement in a running PAM instance — the 'render my whole track' button. " +
|
|
9362
|
+
"For a saved preset song with no PAM open, use the paid `preset render` command with song:true through `pam mcp`'s `run` tool. " +
|
|
8265
9363
|
"Set up the arrangement first with the `song_mode` tool (prefer op setArrangement), then call this.\n\n" +
|
|
8266
9364
|
"Strategies:\n" +
|
|
8267
9365
|
"• 'live' + includeVideo:true — full A/V render: plays the arrangement in real time, films the VX " +
|
|
@@ -9008,7 +10106,7 @@ server.registerTool("update_settings", {
|
|
|
9008
10106
|
description: "Merge a partial settings delta into PAM's plugin settings (the Settings panel state) and apply it live. " +
|
|
9009
10107
|
"Returns the full merged settings object so you can discover available keys. This is the only way to flip " +
|
|
9010
10108
|
"experimental-feature gates from an agent. Common boolean keys: experimentalMode, " +
|
|
9011
|
-
"experimentalPluginHost,
|
|
10109
|
+
"experimentalPluginHost, mcpBridgeEnabled (false closes this bridge after it replies). To enable the plugin host, set BOTH " +
|
|
9012
10110
|
"experimentalMode:true AND the specific sub-flag (e.g. experimentalPluginHost:true) in one call. " +
|
|
9013
10111
|
"Set drumSynthEnabled:false to hide its UI without muting loaded synths. Other keys " +
|
|
9014
10112
|
"include UI/library preferences — call get_state or inspect the returned object for the current set.",
|
|
@@ -9199,18 +10297,19 @@ server.registerTool("set_midi_mapping", {
|
|
|
9199
10297
|
title: "Set MIDI Action Mapping",
|
|
9200
10298
|
description: "Configure the global MIDI-controller mappings that trigger PAM actions: variation switch, preset change, " +
|
|
9201
10299
|
"looper record, live resample, VX video record toggle, and looper-variation switch. Set the enable flag, " +
|
|
9202
|
-
"MIDI channel, CC number,
|
|
10300
|
+
"MIDI channel, CC number, source (Program Change or CC), or bar-sync for each target. One field per call.",
|
|
9203
10301
|
inputSchema: {
|
|
9204
10302
|
target: z
|
|
9205
10303
|
.enum(["variation", "preset", "looper", "resample", "vxRecord", "looperVariation"])
|
|
9206
10304
|
.describe("Which action's mapping to configure. 'vxRecord' toggles VX video recording — requires the PAM window open (VX renders in the WebView)."),
|
|
9207
10305
|
field: z
|
|
9208
10306
|
.enum(["enabled", "channel", "ccNumber", "source", "barSync"])
|
|
9209
|
-
.describe("Field to set. 'source'
|
|
9210
|
-
"
|
|
10307
|
+
.describe("Field to set. 'source' applies to preset and variation (a variation switch on Program Change takes " +
|
|
10308
|
+
"every program on its channel, so presets no longer load from it there); 'barSync' is " +
|
|
10309
|
+
"variation/looperVariation-only; looper/resample/vxRecord have no source/barSync."),
|
|
9211
10310
|
value: z
|
|
9212
10311
|
.union([z.number(), z.boolean()])
|
|
9213
|
-
.describe("boolean for enabled/barSync; number for channel (1-16), ccNumber (0-127), or source."),
|
|
10312
|
+
.describe("boolean for enabled/barSync; number for channel (1-16), ccNumber (0-127), or source (0 = Program Change, 1 = CC)."),
|
|
9214
10313
|
},
|
|
9215
10314
|
}, async ({ target, field, value }) => {
|
|
9216
10315
|
const check = await requireBridge();
|
|
@@ -9387,16 +10486,16 @@ server.registerTool("remove_plugin_from_blocklist", {
|
|
|
9387
10486
|
// TOOL — ask_assistant (drive the FunctionGemma assistant end-to-end)
|
|
9388
10487
|
// ============================================================
|
|
9389
10488
|
// The in-server twin of the ASK bar: renders the SAME FunctionGemma prompt
|
|
9390
|
-
// (shared
|
|
10489
|
+
// (shared functiongemma-format module + live [ctx] line), generates against a local
|
|
9391
10490
|
// OpenAI-compatible endpoint (llama-server serving the fine-tune), parses the
|
|
9392
10491
|
// call blocks, and executes them through this server's own handlers. Lets an
|
|
9393
10492
|
// MCP client (Claude Code) test the assistant model directly against a live
|
|
9394
10493
|
// PAM. Stateless per call — thread follow-ups by passing back `modelTurn`
|
|
9395
10494
|
// from the previous trace as `history`.
|
|
9396
10495
|
//
|
|
9397
|
-
// Dev-tree only: registered only when the
|
|
10496
|
+
// Dev-tree only: registered only when the FunctionGemma toolpack resolves (the
|
|
9398
10497
|
// published npm package doesn't carry it — the tool would be dead weight).
|
|
9399
|
-
const ASSISTANT_TOOLPACK_PATH = resolve(PACKAGE_ROOT, "../
|
|
10498
|
+
const ASSISTANT_TOOLPACK_PATH = resolve(PACKAGE_ROOT, "../ml-research/out/toolpack.json");
|
|
9400
10499
|
// The assistant's view vocabulary → MainViewTypes (stores/Interface.ts).
|
|
9401
10500
|
// Pinned to the in-plugin executor's VIEW_MAP (assistantExecutor.ts) — one
|
|
9402
10501
|
// vocabulary across tool arg, [ctx] view= field, and this dev twin. null =
|
|
@@ -9421,6 +10520,9 @@ const SET_VIEW_MAIN_VIEW = {
|
|
|
9421
10520
|
export: "Export",
|
|
9422
10521
|
presets: "PresetBrowser",
|
|
9423
10522
|
browser: "PresetBrowser",
|
|
10523
|
+
// The preset browser's pack Store tab, PAM only.
|
|
10524
|
+
store: "PresetBrowser",
|
|
10525
|
+
packs: "PresetBrowser",
|
|
9424
10526
|
// MFX-only surfaces. The handler rejects them for other products.
|
|
9425
10527
|
patch: "MfxRouting",
|
|
9426
10528
|
patchbay: "MfxRouting",
|
|
@@ -9444,11 +10546,19 @@ if (existsSync(ASSISTANT_TOOLPACK_PATH))
|
|
|
9444
10546
|
.union([z.string(), z.number()])
|
|
9445
10547
|
.optional()
|
|
9446
10548
|
.describe("Main view: cells, mixer, ctrl, looper, song, lfo, envelope, vary, paramseq, video, export, presets. " +
|
|
9447
|
-
"MFX also takes patch (the cable patchbay)."),
|
|
10549
|
+
"PAM also takes store (the preset browser's pack Store tab); MFX also takes patch (the cable patchbay)."),
|
|
9448
10550
|
cellId: z.number().int().optional().describe("Cell to select/focus (1-8)."),
|
|
9449
10551
|
gridSize: z.number().optional().describe("How many pads the grid shows: 1, 4, or 8."),
|
|
10552
|
+
panel: z
|
|
10553
|
+
.enum(["sampler", "synth", "pattern", "effects"])
|
|
10554
|
+
.optional()
|
|
10555
|
+
.describe("Cell grid tab to show, on cellId or else every cell: sampler, synth, pattern, effects."),
|
|
10556
|
+
fullscreen: z
|
|
10557
|
+
.enum(["sequencer", "effects", "players", "none"])
|
|
10558
|
+
.optional()
|
|
10559
|
+
.describe("CTRL page rack to show alone (sequencer, effects, players), or none."),
|
|
9450
10560
|
},
|
|
9451
|
-
}, async ({ view: viewArg, cellId, gridSize }) => {
|
|
10561
|
+
}, async ({ view: viewArg, cellId, gridSize, panel, fullscreen }) => {
|
|
9452
10562
|
const check = await requireBridgeForCells(cellId);
|
|
9453
10563
|
if (check !== true)
|
|
9454
10564
|
return check;
|
|
@@ -9466,11 +10576,13 @@ if (existsSync(ASSISTANT_TOOLPACK_PATH))
|
|
|
9466
10576
|
token = view.trim().toLowerCase();
|
|
9467
10577
|
if (!(token in SET_VIEW_MAIN_VIEW))
|
|
9468
10578
|
return error(`Unknown view: ${view}. Views: cells, mixer, ctrl, looper, song, lfo, envelope, vary, paramseq, video, export, presets, patch.`);
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
10579
|
+
const product = token === "patch" || token === "patchbay" || token === "store" || token === "packs"
|
|
10580
|
+
? await connectedProduct()
|
|
10581
|
+
: undefined;
|
|
10582
|
+
if ((token === "patch" || token === "patchbay") && product && product !== "MFX")
|
|
10583
|
+
return error(`${token} is available in MFX only.`);
|
|
10584
|
+
if ((token === "store" || token === "packs") && (product === "MFX" || product === "MSQ"))
|
|
10585
|
+
return error(`${token} is available in PAM only.`);
|
|
9474
10586
|
applied.view = token;
|
|
9475
10587
|
}
|
|
9476
10588
|
if (gridSize !== undefined) {
|
|
@@ -9483,13 +10595,19 @@ if (existsSync(ASSISTANT_TOOLPACK_PATH))
|
|
|
9483
10595
|
return error("cellId must be 1-8.");
|
|
9484
10596
|
applied.cellId = cellId;
|
|
9485
10597
|
}
|
|
10598
|
+
if (panel !== undefined)
|
|
10599
|
+
applied.panel = panel;
|
|
10600
|
+
if (fullscreen !== undefined)
|
|
10601
|
+
applied.fullscreen = fullscreen;
|
|
9486
10602
|
if (!Object.keys(applied).length)
|
|
9487
|
-
return error("Provide view, cellId, and/or
|
|
10603
|
+
return error("Provide view, cellId, gridSize, panel and/or fullscreen.");
|
|
9488
10604
|
try {
|
|
9489
10605
|
await bridge.send("setView", {
|
|
9490
10606
|
...(token !== undefined ? { view: token } : {}),
|
|
9491
10607
|
...(cellId !== undefined ? { cellId } : {}),
|
|
9492
10608
|
...(gridSize !== undefined ? { gridSize } : {}),
|
|
10609
|
+
...(panel !== undefined ? { panel } : {}),
|
|
10610
|
+
...(fullscreen !== undefined ? { fullscreen } : {}),
|
|
9493
10611
|
});
|
|
9494
10612
|
return json(applied);
|
|
9495
10613
|
}
|
|
@@ -9514,6 +10632,8 @@ if (existsSync(ASSISTANT_TOOLPACK_PATH))
|
|
|
9514
10632
|
return error(`set_view failed: ${e2.message}`);
|
|
9515
10633
|
}
|
|
9516
10634
|
delete applied.cellId; // not applied on the fallback path — don't over-claim
|
|
10635
|
+
delete applied.panel;
|
|
10636
|
+
delete applied.fullscreen;
|
|
9517
10637
|
return json(applied);
|
|
9518
10638
|
}
|
|
9519
10639
|
});
|
|
@@ -9524,7 +10644,8 @@ if (existsSync(ASSISTANT_TOOLPACK_PATH))
|
|
|
9524
10644
|
"FunctionGemma prompt (with the live [ctx] state line), generate via a local llama-server, " +
|
|
9525
10645
|
"parse the tool calls, execute them against PAM, and return the full trace. Requires " +
|
|
9526
10646
|
"llama-server running with the fine-tuned GGUF (--special). For multi-turn tests, pass the " +
|
|
9527
|
-
"previous trace's modelTurn back via history."
|
|
10647
|
+
"previous trace's modelTurn back via history. The prompt keeps the v34 [ctx] line and has no " +
|
|
10648
|
+
"[params] line, so for toolpack 36+ models it differs from the plugin's prompt.",
|
|
9528
10649
|
inputSchema: {
|
|
9529
10650
|
text: z.string().describe("The user command, e.g. 'mute the kick' or '174 bpm dnb kit'."),
|
|
9530
10651
|
endpoint: z
|
|
@@ -9765,6 +10886,7 @@ const shutdown = async () => {
|
|
|
9765
10886
|
return;
|
|
9766
10887
|
shuttingDown = true;
|
|
9767
10888
|
bridge.disconnect();
|
|
10889
|
+
await daemon?.close();
|
|
9768
10890
|
try {
|
|
9769
10891
|
await server.close();
|
|
9770
10892
|
}
|