@map-audio/pam-mcp-server 1.22.0 → 1.25.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 +32 -6
- package/dist/bridge.d.ts +4 -0
- package/dist/bridge.js +46 -152
- package/dist/bridge.js.map +1 -1
- package/dist/generated/variationSnapshot.d.ts +8 -4
- package/dist/generated/variationSnapshot.js +6 -4
- package/dist/generated/variationSnapshot.js.map +1 -1
- package/dist/generativePlayerPresets.json +70 -0
- package/dist/generativePlayers.json +134 -0
- package/dist/manifest-cell1.json +2 -2
- package/dist/manifest-mfx.json +171 -20
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +12 -9
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.json +1915 -240
- package/dist/midiFile.d.ts +21 -0
- package/dist/midiFile.js +108 -0
- package/dist/midiFile.js.map +1 -0
- package/dist/players.d.ts +89 -0
- package/dist/players.js +235 -0
- package/dist/players.js.map +1 -0
- package/dist/presetSave.d.ts +5 -0
- package/dist/presetSave.js +22 -0
- package/dist/presetSave.js.map +1 -1
- package/dist/sampleSlicing.d.ts +12 -0
- package/dist/sampleSlicing.js +78 -0
- package/dist/sampleSlicing.js.map +1 -0
- package/dist/server.js +1052 -269
- package/dist/server.js.map +1 -1
- package/dist/variationSessionKeys.json +34 -1
- package/package.json +5 -4
package/dist/server.js
CHANGED
|
@@ -8,8 +8,9 @@ import { CORPUS_META, CORPUS_RANGES } from "./corpusRanges.js";
|
|
|
8
8
|
import { CORPUS_FINGERPRINTS, CORPUS_NOVELTY_BASELINE, CORPUS_VARIATION_BASELINE } from "./corpusFingerprints.js";
|
|
9
9
|
import { buildFingerprint, scoreNovelty, similarity as fingerprintSimilarity } from "./fingerprint.js";
|
|
10
10
|
import { analyzeMotion, decodeWav } from "./audioAnalysis.js";
|
|
11
|
-
import { readFile } from "node:fs/promises";
|
|
12
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
13
14
|
import { inflateSync } from "node:zlib";
|
|
14
15
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -17,10 +18,13 @@ import { createDrumSynthCatalog, } from "./drumSynth.js";
|
|
|
17
18
|
import { loadManifest, cellEffectNames, cellEffectOrder, getParameterIndex, getParametersByCell, getGlobalParameters, findPresetDirs, resolvePresetPath, setActiveProductHint, walkPresets, } from "./manifest.js";
|
|
18
19
|
import { PluginBridge } from "./bridge.js";
|
|
19
20
|
import { buildMixMeasurementFromState, currentFlatFromState, parseMixAnalysisPayload, suggestMix, } from "./mixAssist.js";
|
|
20
|
-
import { normalizePresetLookupPath, selectSaveVariationSourcePath, } from "./presetSave.js";
|
|
21
|
+
import { normalizePresetLookupPath, parsePresetFile, selectSaveVariationSourcePath, } from "./presetSave.js";
|
|
21
22
|
import { STOP_SEQUENCES, buildGenerationPrompt, parseFunctionCalls, renderContextLine, renderFunctionCall, renderFunctionResponse, truncateAtFabricatedResponse, withContext, } from "./fgemma-format.js";
|
|
22
23
|
import { mcpProductProfile } from "./productProfile.js";
|
|
23
24
|
import { MFX_COMBO_COUNT, MFX_COMBO_MANUAL_STRENGTH, buildMfxComboTarget, isMfxComboTargetParameter, mfxComboTargetValueFromCurrent, parseMfxComboSlots, reanchorMfxComboTarget, resolveMfxCombos, setMfxComboTargetStop, } from "./mfxCombos.js";
|
|
25
|
+
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";
|
|
26
|
+
import { buildMidiFile, patternSpeedMultiplier, tilePatternEvents } from "./midiFile.js";
|
|
27
|
+
import { MAX_SLICES, detectOnsetBoundaries, equalBoundaries, normalizeBoundaries } from "./sampleSlicing.js";
|
|
24
28
|
// --- State ---
|
|
25
29
|
// Variation slot count. Must stay in sync with PRESET_VARIATION_SLOTS in
|
|
26
30
|
// interface/src/stores/Preset.ts. Defined locally because the MCP package
|
|
@@ -196,22 +200,6 @@ async function requireBridgeForCells(...cellIds) {
|
|
|
196
200
|
}
|
|
197
201
|
return true;
|
|
198
202
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Parse the PAM binary preset format: <metadataSize as text int><metadata JSON><data JSON>
|
|
201
|
-
*/
|
|
202
|
-
function parsePresetFile(raw) {
|
|
203
|
-
const sizeMatch = raw.match(/^(\d+)/);
|
|
204
|
-
if (!sizeMatch)
|
|
205
|
-
throw new Error("invalid format (no metadata size prefix)");
|
|
206
|
-
const prefixLen = sizeMatch[1].length;
|
|
207
|
-
const metadataSize = parseInt(sizeMatch[1], 10);
|
|
208
|
-
if (metadataSize <= 0 || prefixLen + metadataSize > raw.length) {
|
|
209
|
-
throw new Error(`invalid format (metadata size ${metadataSize} out of bounds for file length ${raw.length})`);
|
|
210
|
-
}
|
|
211
|
-
const metadataStr = raw.slice(prefixLen, prefixLen + metadataSize);
|
|
212
|
-
const dataStr = raw.slice(prefixLen + metadataSize);
|
|
213
|
-
return { metadata: JSON.parse(metadataStr), data: JSON.parse(dataStr) };
|
|
214
|
-
}
|
|
215
203
|
// PAM ≥ 1.4.61 stores variations ONLY in the data block, zlib+base64-compressed with a
|
|
216
204
|
// "PAMZ1:" marker (see helpers/VariationCodec.h). Decompress one entry to plain JSON;
|
|
217
205
|
// plain (older-format) entries pass through unchanged.
|
|
@@ -241,20 +229,17 @@ function variationsFromParsedPreset(parsed) {
|
|
|
241
229
|
return Array(PRESET_VARIATION_SLOTS).fill(null);
|
|
242
230
|
return src.map(decompressVariationEntry);
|
|
243
231
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
* Returns Array(8) of plain-JSON strings or nulls.
|
|
247
|
-
*/
|
|
248
|
-
async function readExistingVariations(presetPath) {
|
|
232
|
+
async function readPresetForSave(presetPath, allowMissing = false) {
|
|
233
|
+
let fullPath;
|
|
249
234
|
try {
|
|
250
|
-
|
|
251
|
-
const raw = await readFile(fullPath, "utf-8");
|
|
252
|
-
return variationsFromParsedPreset(parsePresetFile(raw));
|
|
235
|
+
fullPath = await resolvePresetPath(presetPath);
|
|
253
236
|
}
|
|
254
|
-
catch {
|
|
255
|
-
|
|
256
|
-
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (allowMissing && error.code === "ENOENT")
|
|
239
|
+
return null;
|
|
240
|
+
throw error;
|
|
257
241
|
}
|
|
242
|
+
return parsePresetFile(await readFile(fullPath));
|
|
258
243
|
}
|
|
259
244
|
const VARIATION_CONTRACT = (() => {
|
|
260
245
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -294,6 +279,7 @@ const VARIATION_EXCLUDED_PARAMS = [
|
|
|
294
279
|
function buildVariationString(state) {
|
|
295
280
|
return JSON.stringify(buildVariationSnapshotBase(state, {
|
|
296
281
|
sessionKeys: VARIATION_EXCLUDED_PARAMS,
|
|
282
|
+
transientKeys: VARIATION_CONTRACT.transientDispatchKeys ?? [],
|
|
297
283
|
visualParamIds: VARIATION_CONTRACT.visualParamIds ?? [],
|
|
298
284
|
lfoGeneratorCount: VARIATION_CONTRACT.lfoGeneratorCount ?? 6,
|
|
299
285
|
}));
|
|
@@ -583,13 +569,9 @@ const server = new McpServer({
|
|
|
583
569
|
version: SERVER_VERSION,
|
|
584
570
|
}, {
|
|
585
571
|
instructions: IS_MFX_SERVER
|
|
586
|
-
? "MFX is a four-cell modular FX router
|
|
587
|
-
"
|
|
588
|
-
|
|
589
|
-
"Treat tool annotations as authoritative and use select_instance when several products are running."
|
|
590
|
-
: "PAM is an eight-cell sampler. Start read and design work with design_context and " +
|
|
591
|
-
"get_design_overview; filter get_state with section, keys, or cellIds. For samples, call " +
|
|
592
|
-
"get_default_locations before list_samples and load only returned paths; never guess library paths. " +
|
|
572
|
+
? "MFX is a four-cell modular FX router: cells are patched with cables and each cell runs its own effect chain. " +
|
|
573
|
+
"Use cells 1-4. Treat tool annotations as authoritative and use select_instance when several products are running."
|
|
574
|
+
: "PAM is an eight-cell sampler. " +
|
|
593
575
|
"Sequencer output requires a running transport. UI, video, and visual tools require the plugin " +
|
|
594
576
|
"window open; experimental plugin-host and AI tools require experimentalMode plus the matching " +
|
|
595
577
|
"feature flag. Treat tool annotations as authoritative and use select_instance when several " +
|
|
@@ -624,13 +606,14 @@ const TOOL_ANNOTATIONS = {
|
|
|
624
606
|
get_design_overview: RO,
|
|
625
607
|
get_transport: RO,
|
|
626
608
|
get_master_analysis: RO,
|
|
627
|
-
suggest_mix:
|
|
609
|
+
suggest_mix: W,
|
|
628
610
|
connection_status: RO,
|
|
611
|
+
situation: RO,
|
|
629
612
|
list_samples: RO,
|
|
630
613
|
get_default_locations: RO,
|
|
631
614
|
get_audio_info: RO,
|
|
632
615
|
get_video_state: RO,
|
|
633
|
-
capture_ui:
|
|
616
|
+
capture_ui: WI,
|
|
634
617
|
get_sample_peaks: RO,
|
|
635
618
|
get_audio_device_setup: RO,
|
|
636
619
|
get_memory_report: RO,
|
|
@@ -663,6 +646,11 @@ const TOOL_ANNOTATIONS = {
|
|
|
663
646
|
remove_param_seq: DI,
|
|
664
647
|
set_drum_synth: WI,
|
|
665
648
|
set_drum_synth_wave: WI,
|
|
649
|
+
list_players: RO,
|
|
650
|
+
set_player: WI,
|
|
651
|
+
set_slices: WI,
|
|
652
|
+
set_free_sequencer: WI,
|
|
653
|
+
export_midi: W,
|
|
666
654
|
stop_cell: WI,
|
|
667
655
|
reset_cell: DI,
|
|
668
656
|
copy_cell: DI, // overwrites the destination cell
|
|
@@ -682,6 +670,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
682
670
|
midi_recording: W,
|
|
683
671
|
// Presets / variations
|
|
684
672
|
load_preset: DI, // replaces the working state (unsaved edits are lost)
|
|
673
|
+
get_preset_load_status: RO,
|
|
685
674
|
save_preset: WI,
|
|
686
675
|
new_preset: DI,
|
|
687
676
|
import_preset: WI,
|
|
@@ -772,7 +761,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
772
761
|
set_video_fx: WI,
|
|
773
762
|
remove_video_fx: DI,
|
|
774
763
|
bind_video_param: WI,
|
|
775
|
-
set_video_settings:
|
|
764
|
+
set_video_settings: D, // resetWorkspace:true clears the scene
|
|
776
765
|
vx_variation: W,
|
|
777
766
|
// Connection management
|
|
778
767
|
select_instance: WI,
|
|
@@ -797,6 +786,7 @@ const MFX_TOOL_NAMES = new Set([
|
|
|
797
786
|
"get_design_overview",
|
|
798
787
|
"get_transport",
|
|
799
788
|
"connection_status",
|
|
789
|
+
"situation",
|
|
800
790
|
"get_default_locations",
|
|
801
791
|
"capture_ui",
|
|
802
792
|
"get_memory_report",
|
|
@@ -811,6 +801,7 @@ const MFX_TOOL_NAMES = new Set([
|
|
|
811
801
|
"add_param_seq",
|
|
812
802
|
"update_param_seq",
|
|
813
803
|
"remove_param_seq",
|
|
804
|
+
"set_free_sequencer",
|
|
814
805
|
"stop_cell",
|
|
815
806
|
"reset_cell",
|
|
816
807
|
"copy_cell",
|
|
@@ -821,6 +812,7 @@ const MFX_TOOL_NAMES = new Set([
|
|
|
821
812
|
"set_metronome",
|
|
822
813
|
"configure_transport",
|
|
823
814
|
"load_preset",
|
|
815
|
+
"get_preset_load_status",
|
|
824
816
|
"save_preset",
|
|
825
817
|
"new_preset",
|
|
826
818
|
"import_preset",
|
|
@@ -836,6 +828,7 @@ const MFX_TOOL_NAMES = new Set([
|
|
|
836
828
|
"variation_hold",
|
|
837
829
|
"export_audio",
|
|
838
830
|
"update_settings",
|
|
831
|
+
"get_audio_device_setup",
|
|
839
832
|
"set_audio_device",
|
|
840
833
|
"set_mfx_routing",
|
|
841
834
|
"set_mfx_edge_gain",
|
|
@@ -847,27 +840,138 @@ const MFX_TOOL_NAMES = new Set([
|
|
|
847
840
|
]);
|
|
848
841
|
function mfxSurfaceText(value) {
|
|
849
842
|
return value
|
|
843
|
+
// MFX has its own version line, so a PAM version gate names a build MFX never reaches.
|
|
844
|
+
.replace(/PAM ?(?:≥|>=) ?\d+(?:\.\d+)*/g, "a current MFX build")
|
|
845
|
+
.replaceAll("a PAM build", "an MFX build")
|
|
846
|
+
.replaceAll(" saved via set_visual", "")
|
|
847
|
+
.replaceAll(" Check corr in get_master_analysis after widening.", "")
|
|
848
|
+
.replaceAll(" (same constraint as the video tools)", "")
|
|
849
|
+
.replaceAll("so the preset_novelty scale does", "so a whole-preset novelty scale does")
|
|
850
850
|
.replaceAll("PAM/Cell1-only", "sampler-product-only")
|
|
851
851
|
.replaceAll("PAM/Cell1", "sampler products")
|
|
852
852
|
.replaceAll("PAM-only", "sampler-product-only")
|
|
853
853
|
.replaceAll("PAM's", "MFX's")
|
|
854
854
|
.replaceAll("PAM", "MFX");
|
|
855
855
|
}
|
|
856
|
+
const MFX_MAX_CELL = 4;
|
|
857
|
+
// Zod wrappers are immutable, so narrowing a leaf means rebuilding the
|
|
858
|
+
// optional/default/array/union chain around it and re-attaching the description.
|
|
859
|
+
function rebuildSchema(schema, narrowLeaf) {
|
|
860
|
+
const def = schema._zod?.def;
|
|
861
|
+
const keepText = (next) => (schema.description ? next.describe(schema.description) : next);
|
|
862
|
+
const inner = () => rebuildSchema(def.innerType, narrowLeaf);
|
|
863
|
+
switch (def?.type) {
|
|
864
|
+
case "optional":
|
|
865
|
+
return keepText(inner().optional());
|
|
866
|
+
case "nullable":
|
|
867
|
+
return keepText(inner().nullable());
|
|
868
|
+
case "default":
|
|
869
|
+
return keepText(inner().default(def.defaultValue));
|
|
870
|
+
case "array":
|
|
871
|
+
return keepText(z.array(rebuildSchema(def.element, narrowLeaf)));
|
|
872
|
+
case "union":
|
|
873
|
+
return keepText(z.union(def.options.map((option) => rebuildSchema(option, narrowLeaf))));
|
|
874
|
+
default:
|
|
875
|
+
return keepText(narrowLeaf(schema));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function schemaKind(schema) {
|
|
879
|
+
return schema._zod?.def?.type;
|
|
880
|
+
}
|
|
881
|
+
const capCellId = (leaf) => schemaKind(leaf) === "number" ? leaf.max(MFX_MAX_CELL) : leaf;
|
|
882
|
+
const dropEnumMembers = (drop) => (leaf) => {
|
|
883
|
+
if (schemaKind(leaf) !== "enum")
|
|
884
|
+
return leaf;
|
|
885
|
+
const entries = leaf._zod.def.entries;
|
|
886
|
+
const kept = Object.values(entries).filter((value) => !drop.includes(value));
|
|
887
|
+
return z.enum(kept);
|
|
888
|
+
};
|
|
889
|
+
// MFX ships four cells and the shared schemas advertise PAM's eight. The runtime
|
|
890
|
+
// guard is manifest-driven; this is the advertised half of the same contract.
|
|
891
|
+
const MFX_CELL_FIELDS = new Set([
|
|
892
|
+
"cellId",
|
|
893
|
+
"sourceCellId",
|
|
894
|
+
"targetCellId",
|
|
895
|
+
"cellIds",
|
|
896
|
+
"selectedCellIds",
|
|
897
|
+
"sidechainFrom",
|
|
898
|
+
]);
|
|
899
|
+
// Enum members MFX has no surface for: Vary is removed from its modulation stack,
|
|
900
|
+
// and neither song_mode nor a looper tool is registered on this launcher.
|
|
901
|
+
const MFX_ENUM_DROPS = {
|
|
902
|
+
add_modulation: { type: ["vary"] },
|
|
903
|
+
get_state: { section: ["vary", "loopers", "songMode"] },
|
|
904
|
+
export_audio: { mode: ["song-mode"] },
|
|
905
|
+
};
|
|
906
|
+
const MFX_KEPT_FIELDS = {
|
|
907
|
+
update_cell_config: ["cellId", "sidechainFrom"],
|
|
908
|
+
};
|
|
909
|
+
const MFX_DROPPED_FIELDS = {
|
|
910
|
+
select_instance: ["product"],
|
|
911
|
+
// MFX cells are effect slots — there is no synth/sampler tab to switch.
|
|
912
|
+
capture_ui: ["instrumentView"],
|
|
913
|
+
};
|
|
914
|
+
const MFX_TOOL_DESCRIPTIONS = {
|
|
915
|
+
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.",
|
|
916
|
+
update_cell_config: "Set one MFX cell's sidechain source. A non-null source also enables that cell's compressor.",
|
|
917
|
+
capture_ui: "Render MFX's plugin UI (#app_root) to a PNG via html2canvas and return the saved file path (read it to " +
|
|
918
|
+
"view the image). Optionally compose the view first: gridSize (1 or 4), mainView (CellGrid|MfxRouting|" +
|
|
919
|
+
"CtrlAll|Envelope|LFO|ParamSeq|Mixer|PresetBrowser; omit/null = cell grid). REQUIRES MFX's window OPEN — " +
|
|
920
|
+
"html2canvas needs the live DOM, there is no headless renderer. Beats screen-capture: deterministic, no " +
|
|
921
|
+
"cursor/screen control, exact UI bitmap. View arguments switch the live editor and the previous view is " +
|
|
922
|
+
"not restored.",
|
|
923
|
+
};
|
|
924
|
+
const MFX_FIELD_DESCRIPTIONS = {
|
|
925
|
+
capture_ui: { gridSize: "Cell grid size to set before capture: 1 or 4." },
|
|
926
|
+
export_audio: { bars: "Bars to render." },
|
|
927
|
+
add_modulation: {
|
|
928
|
+
cellId: "Required for envelope: source cell driving the follower (1-4). This is not the target cell; target is inferred from paramId.",
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
// Text whose subject is a tool this launcher does not register.
|
|
932
|
+
function mfxDescriptionEdits(name, value) {
|
|
933
|
+
if (name === "export_audio")
|
|
934
|
+
return value
|
|
935
|
+
.replace("Set `mode` to bounce stems or the whole Song Mode arrangement.", "Set `mode` to bounce per-cell or per-bus stems.")
|
|
936
|
+
.replace(" · 'song-mode' (the Song Mode timeline)", "")
|
|
937
|
+
.replace(/\n\nNOTE on 'song-mode':[\s\S]*$/, "");
|
|
938
|
+
if (name === "analyze_motion")
|
|
939
|
+
return value.replace("Fills the gap left by get_master_analysis (loudness) and get_mix_analysis (aggregate spectrum + onset " +
|
|
940
|
+
"density), neither of which can tell a modulated preset from a static one.", "No other tool on this surface can tell a modulated preset from a static one.");
|
|
941
|
+
if (name === "set_master_effects")
|
|
942
|
+
return value.replace("Check results with get_master_analysis (keep tp <= -1 dBTP, PSR >= 8 in the loudest section). ", "Aim for tp <= -1 dBTP and PSR >= 8 in the loudest section; no loudness analysis tool is registered on this surface. ");
|
|
943
|
+
if (name === "set_parameters")
|
|
944
|
+
return (value +
|
|
945
|
+
" Writing mfx_combo1, mfx_combo2 or mfx_combo3 also fans the value out to that combo's resolved target " +
|
|
946
|
+
"parameters, unless one of those targets is written in the same call.");
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
856
949
|
function mfxToolConfig(name, config) {
|
|
857
950
|
const source = config;
|
|
858
|
-
const
|
|
859
|
-
|
|
951
|
+
const kept = MFX_KEPT_FIELDS[name];
|
|
952
|
+
const droppedFields = MFX_DROPPED_FIELDS[name] ?? [];
|
|
953
|
+
const enumDrops = MFX_ENUM_DROPS[name] ?? {};
|
|
954
|
+
const fieldText = MFX_FIELD_DESCRIPTIONS[name] ?? {};
|
|
860
955
|
const inputSchema = source.inputSchema
|
|
861
|
-
? Object.fromEntries(
|
|
862
|
-
key
|
|
863
|
-
|
|
864
|
-
|
|
956
|
+
? Object.fromEntries(Object.entries(source.inputSchema)
|
|
957
|
+
.filter(([key]) => (!kept || kept.includes(key)) && !droppedFields.includes(key))
|
|
958
|
+
.map(([key, schema]) => {
|
|
959
|
+
let narrowed = schema;
|
|
960
|
+
if (enumDrops[key])
|
|
961
|
+
narrowed = rebuildSchema(narrowed, dropEnumMembers(enumDrops[key]));
|
|
962
|
+
if (MFX_CELL_FIELDS.has(key))
|
|
963
|
+
narrowed = rebuildSchema(narrowed, capCellId);
|
|
964
|
+
const described = fieldText[key] ??
|
|
965
|
+
(narrowed.description
|
|
966
|
+
? mfxSurfaceText(MFX_CELL_FIELDS.has(key)
|
|
967
|
+
? narrowed.description.replaceAll("1-8", "1-4").replaceAll("1..8", "1..4")
|
|
968
|
+
: narrowed.description)
|
|
969
|
+
: undefined);
|
|
970
|
+
return [key, described ? narrowed.describe(described) : narrowed];
|
|
971
|
+
}))
|
|
865
972
|
: undefined;
|
|
866
|
-
const description = name
|
|
867
|
-
|
|
868
|
-
: name === "update_cell_config"
|
|
869
|
-
? "Set one MFX cell's sidechain source. A non-null source also enables that cell's compressor."
|
|
870
|
-
: source.description ? mfxSurfaceText(source.description) : undefined;
|
|
973
|
+
const description = MFX_TOOL_DESCRIPTIONS[name] ??
|
|
974
|
+
(source.description ? mfxSurfaceText(mfxDescriptionEdits(name, source.description)) : undefined);
|
|
871
975
|
return {
|
|
872
976
|
...source,
|
|
873
977
|
...(description ? { description } : {}),
|
|
@@ -996,7 +1100,7 @@ server.registerTool("get_parameter_info", {
|
|
|
996
1100
|
});
|
|
997
1101
|
server.registerTool("list_presets", {
|
|
998
1102
|
title: "List Presets",
|
|
999
|
-
description: "List available PAM presets (factory and user).",
|
|
1103
|
+
description: "List available PAM presets (factory and user) as an array of {name, path}. Above 400 matches it returns {presets, total, truncated, hint} instead — narrow with `search` rather than paging.",
|
|
1000
1104
|
inputSchema: {
|
|
1001
1105
|
search: z.string().optional().describe("Filter presets by name."),
|
|
1002
1106
|
},
|
|
@@ -1127,9 +1231,8 @@ server.registerTool("design_context", {
|
|
|
1127
1231
|
groups: z
|
|
1128
1232
|
.array(z.string())
|
|
1129
1233
|
.optional()
|
|
1130
|
-
.describe("Narrow keyPerCellParams to these groups (e.g. ['delay','vocoder'])
|
|
1131
|
-
"Omit
|
|
1132
|
-
"subset is what makes presets resemble each other."),
|
|
1234
|
+
.describe("Narrow keyPerCellParams to these groups (e.g. ['delay','vocoder']). " +
|
|
1235
|
+
"Omit it to get the full per-cell vocabulary."),
|
|
1133
1236
|
},
|
|
1134
1237
|
annotations: { readOnlyHint: true },
|
|
1135
1238
|
}, async ({ cellId, groups: groupFilter }) => {
|
|
@@ -1278,7 +1381,7 @@ server.registerTool("design_context", {
|
|
|
1278
1381
|
compressor: group("compressorThreshold", "compressorRatio", "compressorAttack", "compressorRelease"),
|
|
1279
1382
|
panner: group("pannerBalance", "pannerWidth"),
|
|
1280
1383
|
clip: group("gainClip", "gainClipMode"),
|
|
1281
|
-
drumSynth: group("synthEngine", "synthVolume", "synthPitch", "synthAttack", "synth_p1", "synth_p2", "synth_p3"),
|
|
1384
|
+
drumSynth: group("synthEngine", "synthVolume", "synthPitch", "synthAttack", "synthWaveFm", "synth_p1", "synth_p2", "synth_p3"),
|
|
1282
1385
|
// Everything below is a real per-cell engine that no shipped factory
|
|
1283
1386
|
// preset has ever touched. They are the least-explored territory in the
|
|
1284
1387
|
// instrument, which makes them the first place to look when the brief
|
|
@@ -1360,6 +1463,9 @@ server.registerTool("design_context", {
|
|
|
1360
1463
|
return [`cell${id}`, 47 + id];
|
|
1361
1464
|
})),
|
|
1362
1465
|
variationSlots: "API is 0-based (0..7); PAM UI labels slots 1..8. Save/load variation index 0 = UI slot 1.",
|
|
1466
|
+
generativePlayers: "set_player turns a cell into a live generator (list_players for the roster: EUCLID, KIT, TURING, WALK, MARKOV, " +
|
|
1467
|
+
"RATCHET, PHASE, RIFF, MASK). cell{N}_playerDensity/Mutate/P1..P5 are modulatable params with macro_player* twins; " +
|
|
1468
|
+
"write them through set_player, never set_parameters alone, so the cell config stays in step.",
|
|
1363
1469
|
modulationGuidance: "Primary movement should target per-cell params for distinct audible changes. Use macro_* for performance-level fan-out across cells. " +
|
|
1364
1470
|
"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.",
|
|
1365
1471
|
});
|
|
@@ -1454,8 +1560,9 @@ server.registerTool("analyze_motion", {
|
|
|
1454
1560
|
"a bar — the last one is what catches a bar-synced gate, tremolo or sidechain pump, whose bars are all " +
|
|
1455
1561
|
"identical to each other.\n\n" +
|
|
1456
1562
|
"Pass wavPath to analyze an existing render or live_resample capture. Otherwise the tool runs an offline " +
|
|
1457
|
-
"export first
|
|
1458
|
-
"offline export is known to produce
|
|
1563
|
+
"export first and leaves that render in PAM's Exports folder as pam_motion_<epoch>.wav. Digital silence is " +
|
|
1564
|
+
"reported as a RENDER FAILURE with the workaround, not as 'no motion' — offline export is known to produce " +
|
|
1565
|
+
"silence in the standalone build.",
|
|
1459
1566
|
inputSchema: {
|
|
1460
1567
|
wavPath: z
|
|
1461
1568
|
.string()
|
|
@@ -1840,12 +1947,14 @@ server.registerTool("get_design_overview", {
|
|
|
1840
1947
|
]));
|
|
1841
1948
|
return {
|
|
1842
1949
|
cellId: id,
|
|
1950
|
+
muted: state[`${prefix}mute`] === 1 || state[`${prefix}mute`] === true,
|
|
1843
1951
|
sample: {
|
|
1844
1952
|
name: stemName(samplePath),
|
|
1845
1953
|
path: typeof samplePath === "string" ? samplePath : undefined,
|
|
1846
1954
|
},
|
|
1847
1955
|
// Both halves: the enum an agent writes back, and the name it reasons about.
|
|
1848
1956
|
synthEngine: describeSynthEngine(state[`${prefix}synthEngine`]),
|
|
1957
|
+
player: describeCellPlayer(id, cellState, state),
|
|
1849
1958
|
enabledEffects: effectNames.filter((effectName) => state[`${prefix}${effectName}Enabled`] === 1 || state[`${prefix}${effectName}Enabled`] === true),
|
|
1850
1959
|
patternEvents: Object.fromEntries(Object.entries(PATTERN_LANES).map(([lane, suffix]) => [
|
|
1851
1960
|
lane,
|
|
@@ -1870,6 +1979,7 @@ server.registerTool("get_design_overview", {
|
|
|
1870
1979
|
},
|
|
1871
1980
|
totals: {
|
|
1872
1981
|
cellsWithSamples: cells.filter((c) => c.sample.path).length,
|
|
1982
|
+
playersRunning: cells.filter((c) => c.player).length,
|
|
1873
1983
|
activePatternEvents: cells.reduce((sum, c) => sum + Object.values(c.patternEvents).reduce((a, b) => a + b, 0), 0),
|
|
1874
1984
|
modulationLinks: cells.reduce((sum, c) => sum + Object.values(c.modulation).reduce((a, b) => a + b, 0), 0),
|
|
1875
1985
|
},
|
|
@@ -1880,7 +1990,8 @@ server.registerTool("set_parameters", {
|
|
|
1880
1990
|
title: "Set Parameters",
|
|
1881
1991
|
description: "Set one or more PAM parameters. Values are validated against the manifest. " +
|
|
1882
1992
|
"Effect enable flags such as cell1_filterEnabled are settable here too, so params + enables can land in one call " +
|
|
1883
|
-
"when a graph dispatch is acceptable. Use get_parameter_info or design_context to discover valid IDs and ranges."
|
|
1993
|
+
"when a graph dispatch is acceptable. Use get_parameter_info or design_context to discover valid IDs and ranges. " +
|
|
1994
|
+
"Writing a macro_* id also enables that macro, matching the UI.",
|
|
1884
1995
|
inputSchema: {
|
|
1885
1996
|
parameters: z
|
|
1886
1997
|
.record(z.string(), z.number())
|
|
@@ -1925,7 +2036,7 @@ server.registerTool("set_parameters", {
|
|
|
1925
2036
|
delta.macros = macros;
|
|
1926
2037
|
}
|
|
1927
2038
|
let comboTargetCount = 0;
|
|
1928
|
-
if (
|
|
2039
|
+
if (isMfxManifest(await loadManifest())) {
|
|
1929
2040
|
const comboValues = new Map();
|
|
1930
2041
|
for (let comboIndex = 1; comboIndex <= MFX_COMBO_COUNT; comboIndex++) {
|
|
1931
2042
|
const value = parameters[`mfx_combo${comboIndex}`];
|
|
@@ -1970,7 +2081,7 @@ server.registerTool("set_parameters", {
|
|
|
1970
2081
|
});
|
|
1971
2082
|
server.registerTool("load_sample", {
|
|
1972
2083
|
title: "Load Sample",
|
|
1973
|
-
description: "Load an audio file (wav, mp3, aif, flac) into a PAM cell.",
|
|
2084
|
+
description: "Load an audio file (wav, mp3, aif, flac) into a PAM cell, replacing whatever the cell currently holds. Take filePath from list_samples or a get_default_locations root.",
|
|
1974
2085
|
inputSchema: {
|
|
1975
2086
|
filePath: z.string().describe("Path to the audio file (absolute, or relative as returned by list_samples)."),
|
|
1976
2087
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
@@ -2149,6 +2260,7 @@ server.registerTool("generate_kit", {
|
|
|
2149
2260
|
// curve / prompt / seed / sigma), sidecar renders, and the take shelf. Requires
|
|
2150
2261
|
// experimentalMode + experimentalAIGen (update_settings) AND the SA3 Python
|
|
2151
2262
|
// sidecar running on 127.0.0.1:8093 (packages/sa3-workbench-sidecar/README.md).
|
|
2263
|
+
const WORKBENCH_PREREQ = "Requires experimentalMode + experimentalAIGen (update_settings) and the SA3 sidecar on 127.0.0.1:8093; check workbench_sidecar_status first. ";
|
|
2152
2264
|
const WORKBENCH_MASK_REGION = z.object({
|
|
2153
2265
|
startSec: z.number().min(0).describe("Region start in seconds (source time)."),
|
|
2154
2266
|
endSec: z.number().min(0).describe("Region end in seconds (must be > startSec)."),
|
|
@@ -2184,7 +2296,8 @@ const WORKBENCH_STATE_FIELDS = {
|
|
|
2184
2296
|
};
|
|
2185
2297
|
server.registerTool("workbench_get_state", {
|
|
2186
2298
|
title: "Workbench: Get State",
|
|
2187
|
-
description:
|
|
2299
|
+
description: WORKBENCH_PREREQ +
|
|
2300
|
+
"Read the AI Audio Workbench canvas state AND the take shelf: { state: { sourceCellId, prompt, seed, seedLocked, sigma, steps, maskRegions, strengthCurve, activeSourcePath, sketchPath, sketchGain }, takes: [...] }.",
|
|
2188
2301
|
inputSchema: {},
|
|
2189
2302
|
}, async () => {
|
|
2190
2303
|
const check = await requireBridge();
|
|
@@ -2194,7 +2307,8 @@ server.registerTool("workbench_get_state", {
|
|
|
2194
2307
|
});
|
|
2195
2308
|
server.registerTool("workbench_set_state", {
|
|
2196
2309
|
title: "Workbench: Set State",
|
|
2197
|
-
description:
|
|
2310
|
+
description: WORKBENCH_PREREQ +
|
|
2311
|
+
"Partial-merge the Workbench canvas state (only the keys you pass change). Also pushes the merged state into an open Workbench UI. Returns the full merged state.",
|
|
2198
2312
|
inputSchema: WORKBENCH_STATE_FIELDS,
|
|
2199
2313
|
}, async (args) => {
|
|
2200
2314
|
const check = await requireBridge();
|
|
@@ -2204,7 +2318,8 @@ server.registerTool("workbench_set_state", {
|
|
|
2204
2318
|
});
|
|
2205
2319
|
server.registerTool("workbench_render", {
|
|
2206
2320
|
title: "Workbench: Render",
|
|
2207
|
-
description:
|
|
2321
|
+
description: WORKBENCH_PREREQ +
|
|
2322
|
+
"Render the Workbench transform via the SA3 sidecar and append the result to the take shelf. Renders the CURRENT canvas state merged with any overrides passed here (overrides do not persist). Blocks until done (up to ~3 min: pipeline rebuilds + multi-region inpaints render sequentially). Returns the take { id, wavPath, prompt, seed, sigma, maskRegions, createdAt, sourceCellId, durationSeconds, error? }. Source resolution: sourcePath override > activeSourcePath (chain head) > state.sourceCellId's sample file. A successful render ADVANCES the chain (activeSourcePath = new take's wavPath) — step back with workbench_undo or workbench_reset_chain.",
|
|
2208
2323
|
inputSchema: {
|
|
2209
2324
|
...WORKBENCH_STATE_FIELDS,
|
|
2210
2325
|
sourcePath: z.string().optional()
|
|
@@ -2223,7 +2338,7 @@ server.registerTool("workbench_render", {
|
|
|
2223
2338
|
});
|
|
2224
2339
|
server.registerTool("workbench_list_takes", {
|
|
2225
2340
|
title: "Workbench: List Takes",
|
|
2226
|
-
description: "List all Workbench takes (renders) with full provenance, oldest first.",
|
|
2341
|
+
description: WORKBENCH_PREREQ + "List all Workbench takes (renders) with full provenance, oldest first.",
|
|
2227
2342
|
inputSchema: {},
|
|
2228
2343
|
}, async () => {
|
|
2229
2344
|
const check = await requireBridge();
|
|
@@ -2233,7 +2348,7 @@ server.registerTool("workbench_list_takes", {
|
|
|
2233
2348
|
});
|
|
2234
2349
|
server.registerTool("workbench_load_take", {
|
|
2235
2350
|
title: "Workbench: Load Take",
|
|
2236
|
-
description: "Load a rendered take's WAV into a cell (same native path as load_sample).",
|
|
2351
|
+
description: WORKBENCH_PREREQ + "Load a rendered take's WAV into a cell (same native path as load_sample).",
|
|
2237
2352
|
inputSchema: {
|
|
2238
2353
|
takeId: z.string().describe("Take id from workbench_render / workbench_list_takes."),
|
|
2239
2354
|
cellId: z.number().int().min(1).max(8).describe("Target cell."),
|
|
@@ -2251,7 +2366,8 @@ server.registerTool("workbench_load_take", {
|
|
|
2251
2366
|
});
|
|
2252
2367
|
server.registerTool("workbench_undo", {
|
|
2253
2368
|
title: "Workbench: Undo (pop chain)",
|
|
2254
|
-
description:
|
|
2369
|
+
description: WORKBENCH_PREREQ +
|
|
2370
|
+
"Pop the Workbench render chain one step: restores the previous chain head (activeSourcePath) so the next render re-transforms the prior source. No-op at the chain root (cell sample). Takes are kept. Returns the full { state, takes }.",
|
|
2255
2371
|
inputSchema: {},
|
|
2256
2372
|
}, async () => {
|
|
2257
2373
|
const check = await requireBridge();
|
|
@@ -2261,7 +2377,8 @@ server.registerTool("workbench_undo", {
|
|
|
2261
2377
|
});
|
|
2262
2378
|
server.registerTool("workbench_reset_chain", {
|
|
2263
2379
|
title: "Workbench: Reset Chain",
|
|
2264
|
-
description:
|
|
2380
|
+
description: WORKBENCH_PREREQ +
|
|
2381
|
+
"Clear the Workbench render chain: empties the undo stack and resets the source back to the cell sample (activeSourcePath = ''). Takes are kept. Returns the full { state, takes }.",
|
|
2265
2382
|
inputSchema: {},
|
|
2266
2383
|
}, async () => {
|
|
2267
2384
|
const check = await requireBridge();
|
|
@@ -2322,17 +2439,31 @@ server.registerTool("stop_cell", {
|
|
|
2322
2439
|
});
|
|
2323
2440
|
server.registerTool("load_preset", {
|
|
2324
2441
|
title: "Load Preset",
|
|
2325
|
-
description: "
|
|
2442
|
+
description: "Queue a preset load through PAM's native load/morph lifecycle. Returns requestId and status:'queued'. " +
|
|
2443
|
+
"Poll get_preset_load_status until status:'succeeded' before editing, randomizing, or saving; failure, cancellation, and supersession are explicit. " +
|
|
2444
|
+
"Replaces working state and the variation bank; save_preset first if unsaved edits matter. Requires a plugin build with tracked preset loading.",
|
|
2326
2445
|
inputSchema: {
|
|
2327
2446
|
path: z.string().describe("Preset file path (absolute or relative to preset dirs)."),
|
|
2447
|
+
immediate: z.boolean().optional().describe("true bypasses the preset morph; false/omitted follows the native preset morph settings."),
|
|
2328
2448
|
},
|
|
2329
|
-
}, async ({ path: presetPath }) => {
|
|
2449
|
+
}, async ({ path: presetPath, immediate }) => {
|
|
2330
2450
|
const check = await requireBridge();
|
|
2331
2451
|
if (check !== true)
|
|
2332
2452
|
return check;
|
|
2333
2453
|
const fullPath = await resolvePresetPath(presetPath);
|
|
2334
|
-
await bridge.send("
|
|
2335
|
-
|
|
2454
|
+
return json(await bridge.send("loadPresetTracked", fullPath, immediate === true));
|
|
2455
|
+
});
|
|
2456
|
+
server.registerTool("get_preset_load_status", {
|
|
2457
|
+
title: "Get Preset Load Status",
|
|
2458
|
+
description: "Read the outcome of a load_preset request without requiring the plugin window. " +
|
|
2459
|
+
"Statuses: queued, morphing, succeeded, failed, cancelled, superseded. Only succeeded confirms the world committed, including same-path reloads. " +
|
|
2460
|
+
"PAM retains the latest 64 requests per instance; unknown/expired IDs return an error. Keep the same selected instance while polling.",
|
|
2461
|
+
inputSchema: { requestId: z.string().min(1).describe("requestId returned by load_preset.") },
|
|
2462
|
+
}, async ({ requestId }) => {
|
|
2463
|
+
const check = await requireBridge();
|
|
2464
|
+
if (check !== true)
|
|
2465
|
+
return check;
|
|
2466
|
+
return json(await bridge.send("getPresetLoadStatus", requestId));
|
|
2336
2467
|
});
|
|
2337
2468
|
server.registerTool("add_modulation", {
|
|
2338
2469
|
title: "Add Modulation",
|
|
@@ -2476,7 +2607,10 @@ server.registerTool("randomize", {
|
|
|
2476
2607
|
description: "Trigger PAM's randomization engine to generate random sounds and patterns. " +
|
|
2477
2608
|
"target:'synth' runs the musical, engine-aware drum-synth randomizer (picks an engine + " +
|
|
2478
2609
|
"shapes its params per cell) — it is routed through the WebView, so it requires PAM's window " +
|
|
2479
|
-
"to be OPEN (samples/sequences/full run natively and do not).
|
|
2610
|
+
"to be OPEN (samples/sequences/full run natively and do not). 'lfos'/'mods' are global and require an open window. " +
|
|
2611
|
+
"Returns queued, not completed: poll get_state for the requested changes before another randomize, edit, or save. " +
|
|
2612
|
+
"Concurrent requests can coalesce; a preset/variation load can discard pending work. " +
|
|
2613
|
+
"Use target:'full' to chain sample selection and pattern generation in the native engine.",
|
|
2480
2614
|
inputSchema: {
|
|
2481
2615
|
// 'lfos'/'mods' added for the assistant surface (1.15.0) — WebView-routed
|
|
2482
2616
|
// like 'synth'. The describe() text is pinned (exported toolpack bytes).
|
|
@@ -2487,7 +2621,7 @@ server.registerTool("randomize", {
|
|
|
2487
2621
|
cellIds: z
|
|
2488
2622
|
.array(z.number().min(1).max(8))
|
|
2489
2623
|
.optional()
|
|
2490
|
-
.describe("Specific cells to randomize. Omit for all."),
|
|
2624
|
+
.describe("Specific cells to randomize. Omit for all. Not supported for global 'lfos'/'mods'."),
|
|
2491
2625
|
genreHint: z
|
|
2492
2626
|
.string()
|
|
2493
2627
|
.optional()
|
|
@@ -2518,6 +2652,8 @@ server.registerTool("randomize", {
|
|
|
2518
2652
|
.describe("Rhythmic placement for 'sequences'/'full' (-1 = strong beats, 0 = neutral, 1 = weaker offbeats). Omit for the genre and role default."),
|
|
2519
2653
|
},
|
|
2520
2654
|
}, async ({ target, cellIds, genreHint, bpmHint, mutationAmount, density, syncopation }) => {
|
|
2655
|
+
if ((target === "lfos" || target === "mods") && cellIds !== undefined)
|
|
2656
|
+
return error(`target:'${target}' randomizes global modulation; omit cellIds or edit individual links with update_modulation.`);
|
|
2521
2657
|
const check = await requireBridgeForCells(cellIds);
|
|
2522
2658
|
if (check !== true)
|
|
2523
2659
|
return check;
|
|
@@ -2525,23 +2661,21 @@ server.registerTool("randomize", {
|
|
|
2525
2661
|
// Routed to the canonical WebView randomizer (single source of truth) — needs the
|
|
2526
2662
|
// window open. Empty array → all non-excluded cells.
|
|
2527
2663
|
await bridge.send("randomizeDrumSynth", cellIds ?? []);
|
|
2528
|
-
return
|
|
2529
|
-
`(Drum-synth randomize runs in the WebView — if nothing changed, ensure PAM's window is open.)`);
|
|
2664
|
+
return json({ status: "queued", target, cellIds, note: "Open PAM's window and poll get_design_overview for synth changes before editing or saving." });
|
|
2530
2665
|
}
|
|
2531
2666
|
if (target === "lfos" || target === "mods") {
|
|
2532
|
-
// The data-driven modulation randomizer lives in WebView JS
|
|
2533
|
-
// (interop.ts randomizeLfos/randomizeEnvelopes/randomizeVary) — routed
|
|
2534
|
-
// like 'synth'. Needs PAM ≥ 1.15.0 and the window open.
|
|
2535
2667
|
await bridge.send("randomizeModulations", target, mutationAmount ?? -1);
|
|
2536
|
-
return
|
|
2537
|
-
`(Runs in the WebView — if nothing changed, ensure PAM's window is open.)`);
|
|
2668
|
+
return json({ status: "queued", target, note: "Open PAM's window and poll get_state for modulation changes before editing or saving." });
|
|
2538
2669
|
}
|
|
2539
2670
|
await bridge.send("randomize", target, cellIds ?? [], genreHint ?? "", bpmHint ?? -1, mutationAmount ?? -1, density ?? null, syncopation ?? null);
|
|
2540
|
-
return
|
|
2671
|
+
return json({ status: "queued", target, cellIds, note: "Poll get_state for changed sample paths and/or sequences before another randomize, edit, or save. An acknowledgment does not prove the request applied." });
|
|
2541
2672
|
});
|
|
2542
2673
|
server.registerTool("randomize_samples", {
|
|
2543
2674
|
title: "Randomize Samples",
|
|
2544
|
-
description: "Use PAM's native sample randomization engine to pick coherent samples based on genre, BPM, and kit role.
|
|
2675
|
+
description: "Use PAM's native sample randomization engine to pick coherent samples based on genre, BPM, and kit role. Supports folder filtering. " +
|
|
2676
|
+
"Returns queued: poll get_state section:'cells' for sample changes before generating patterns, editing, or saving. " +
|
|
2677
|
+
"Serialize requests because concurrent requests can coalesce; a preset/variation load can discard pending work. " +
|
|
2678
|
+
"For samples plus patterns, prefer randomize target:'full'.",
|
|
2545
2679
|
inputSchema: {
|
|
2546
2680
|
cellIds: z
|
|
2547
2681
|
.array(z.number().min(1).max(8))
|
|
@@ -2573,8 +2707,8 @@ server.registerTool("randomize_samples", {
|
|
|
2573
2707
|
.describe("Anti-centroid mode (0 = default behaviour, 1 = maximum spread). The native engine scores FOLDER " +
|
|
2574
2708
|
"COHERENCE, which is correct for a kit-from-one-pack and is also why repeated randomizes keep landing " +
|
|
2575
2709
|
"on the same few well-stocked folders. Above 0 this builds a cross-folder pool first and restricts the " +
|
|
2576
|
-
"engine to it, keeping role assignment while removing its ability to converge.
|
|
2577
|
-
"
|
|
2710
|
+
"engine to it, keeping role assignment while removing its ability to converge. Ignored when " +
|
|
2711
|
+
"allowedSamplePaths is given."),
|
|
2578
2712
|
seed: z
|
|
2579
2713
|
.number()
|
|
2580
2714
|
.int()
|
|
@@ -2609,11 +2743,14 @@ server.registerTool("randomize_samples", {
|
|
|
2609
2743
|
}
|
|
2610
2744
|
}
|
|
2611
2745
|
await bridge.send("randomizeSamples", cellIds ?? [], genreHint ?? "", bpmHint ?? -1, excludedFolders ?? [], pool ?? []);
|
|
2612
|
-
return
|
|
2746
|
+
return json({ status: "queued", target: "samples", cellIds, genreHint,
|
|
2747
|
+
note: "Poll get_state section:'cells' for changed sample paths before another randomize, patterns, edit, or save." + diversifyNote });
|
|
2613
2748
|
});
|
|
2614
2749
|
server.registerTool("randomize_patterns", {
|
|
2615
2750
|
title: "Randomize Patterns",
|
|
2616
|
-
description: "Use PAM's native pattern generation engine to create
|
|
2751
|
+
description: "Use PAM's native pattern generation engine to create note patterns based on loaded sample roles, genre, and BPM. " +
|
|
2752
|
+
"Identical to randomize target:'sequences'. Returns queued: poll get_state for changed cellN_midiSequence values " +
|
|
2753
|
+
"before another randomize, edit, or save. Wait for sample loading/randomization first; concurrent requests can coalesce.",
|
|
2617
2754
|
inputSchema: {
|
|
2618
2755
|
cellIds: z
|
|
2619
2756
|
.array(z.number().min(1).max(8))
|
|
@@ -2636,8 +2773,8 @@ server.registerTool("randomize_patterns", {
|
|
|
2636
2773
|
.optional()
|
|
2637
2774
|
.describe("How much to vary from typical patterns (0 = conventional, 1 = experimental). Default ~0.5. " +
|
|
2638
2775
|
"This is the pattern-side divergence lever: the generator otherwise produces the genre's most typical " +
|
|
2639
|
-
"rhythm, which is exactly what makes a batch of presets feel the same
|
|
2640
|
-
"
|
|
2776
|
+
"rhythm, which is exactly what makes a batch of presets feel the same; 0.7-1.0 buys rhythmic " +
|
|
2777
|
+
"distinctness, low values keep a stock groove."),
|
|
2641
2778
|
density: z
|
|
2642
2779
|
.number()
|
|
2643
2780
|
.min(-1)
|
|
@@ -2656,11 +2793,12 @@ server.registerTool("randomize_patterns", {
|
|
|
2656
2793
|
if (check !== true)
|
|
2657
2794
|
return check;
|
|
2658
2795
|
await bridge.send("randomize", "sequences", cellIds ?? [], genreHint ?? "", bpmHint ?? -1, mutationAmount ?? -1, density ?? null, syncopation ?? null);
|
|
2659
|
-
return
|
|
2796
|
+
return json({ status: "queued", target: "sequences", cellIds, genreHint,
|
|
2797
|
+
note: "Poll get_state for changed cellN_midiSequence values before another randomize, edit, or save." });
|
|
2660
2798
|
});
|
|
2661
2799
|
server.registerTool("load_variation", {
|
|
2662
2800
|
title: "Load Variation",
|
|
2663
|
-
description: "
|
|
2801
|
+
description: "Request a switch to a saved variation slot (0-7), including parameters, effects, patterns, and modulation. Empty slots are rejected. Verify currentVariation and a slot-specific value with get_state before editing or saving.",
|
|
2664
2802
|
inputSchema: {
|
|
2665
2803
|
index: z
|
|
2666
2804
|
.number()
|
|
@@ -2674,7 +2812,7 @@ server.registerTool("load_variation", {
|
|
|
2674
2812
|
if (check !== true)
|
|
2675
2813
|
return check;
|
|
2676
2814
|
await bridge.send("loadVariation", index);
|
|
2677
|
-
return text(`
|
|
2815
|
+
return text(`Requested variation ${index} (UI slot ${index + 1}); verify currentVariation and the intended state with get_state before editing or saving.`);
|
|
2678
2816
|
});
|
|
2679
2817
|
server.registerTool("full_stop_transport", {
|
|
2680
2818
|
title: "Full Stop Transport",
|
|
@@ -2728,7 +2866,7 @@ server.registerTool("get_mix_analysis", {
|
|
|
2728
2866
|
"true/sample peak (tp/sp), PSR/PLR crest, stereo correlation, onset density, and a 25-band Bark " +
|
|
2729
2867
|
"spectrum per cell, plus the full-mix master block. Pass refresh:true to run a new analysis " +
|
|
2730
2868
|
"(renders each sounding cell soloed from bar 0 — takes a few seconds; audio does NOT need to be " +
|
|
2731
|
-
"playing). Cached result otherwise. Requires PAM
|
|
2869
|
+
"playing). Cached result otherwise. Requires a PAM build with Mix Assist support.",
|
|
2732
2870
|
inputSchema: {
|
|
2733
2871
|
refresh: z.boolean().optional().describe("Run a fresh analysis instead of returning the cached one."),
|
|
2734
2872
|
durationSeconds: z.number().min(5).max(30).optional().describe("Analysis render length (default 8 bars at the current BPM, clamped 5-30 s)."),
|
|
@@ -2783,7 +2921,7 @@ server.registerTool("suggest_mix", {
|
|
|
2783
2921
|
"partial-loudness fader balance, pan spread, bounded masking EQ cuts, and optional compressor " +
|
|
2784
2922
|
"settings. Returns absolute moves with per-move rationale and guard notes; apply:true lands them " +
|
|
2785
2923
|
"as one state update. Run get_mix_analysis with refresh:true first if the mix changed. " +
|
|
2786
|
-
"Requires PAM
|
|
2924
|
+
"Requires a PAM build with Mix Assist support.",
|
|
2787
2925
|
inputSchema: {
|
|
2788
2926
|
scopes: z
|
|
2789
2927
|
.array(z.enum(["balance", "pan", "eq", "dynamics"]))
|
|
@@ -2870,7 +3008,8 @@ server.registerTool("new_preset", {
|
|
|
2870
3008
|
});
|
|
2871
3009
|
server.registerTool("save_preset", {
|
|
2872
3010
|
title: "Save Preset",
|
|
2873
|
-
description: "Save the current PAM state as a
|
|
3011
|
+
description: "Save the current PAM state as a preset file with metadata. Omit `name` to re-save the active user preset in place " +
|
|
3012
|
+
"(factory presets are refused). Every save also snapshots the current state into variation slot 0 if the bank is empty, otherwise into the active slot. " +
|
|
2874
3013
|
"Writes to the user preset folder by default; pass `path` for a specific location. " +
|
|
2875
3014
|
"Use `update_preset_metadata` to edit an existing preset without re-saving plugin state.",
|
|
2876
3015
|
inputSchema: {
|
|
@@ -2921,19 +3060,12 @@ server.registerTool("save_preset", {
|
|
|
2921
3060
|
return error(`Could not derive a name from the active preset path (${activePath}) — pass name explicitly.`);
|
|
2922
3061
|
if (path === undefined)
|
|
2923
3062
|
path = activePath;
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
continue;
|
|
2931
|
-
if (extras[k] === undefined)
|
|
2932
|
-
extras[k] = v;
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
catch {
|
|
2936
|
-
// unreadable/new — save fresh with whatever the caller passed
|
|
3063
|
+
const parsed = await readPresetForSave(readPath);
|
|
3064
|
+
for (const [k, v] of Object.entries(parsed.metadata)) {
|
|
3065
|
+
if (["name", "factory", "path", "variations", "currentVariation"].includes(k))
|
|
3066
|
+
continue;
|
|
3067
|
+
if (extras[k] === undefined)
|
|
3068
|
+
extras[k] = v;
|
|
2937
3069
|
}
|
|
2938
3070
|
}
|
|
2939
3071
|
const metadata = { name, factory: false };
|
|
@@ -2952,7 +3084,11 @@ server.registerTool("save_preset", {
|
|
|
2952
3084
|
// the new destination starts from eight empty slots because that file does
|
|
2953
3085
|
// not exist yet, which used to collapse every named save to one slot.
|
|
2954
3086
|
const variationSourcePath = selectSaveVariationSourcePath(state.activePresetPath, presetRelPath);
|
|
2955
|
-
const
|
|
3087
|
+
const hasActivePreset = typeof state.activePresetPath === "string"
|
|
3088
|
+
&& state.activePresetPath.length > 0 && state.activePresetPath !== "Init";
|
|
3089
|
+
const source = await readPresetForSave(variationSourcePath, !hasActivePreset);
|
|
3090
|
+
const existingVariations = source
|
|
3091
|
+
? variationsFromParsedPreset(source) : Array(PRESET_VARIATION_SLOTS).fill(null);
|
|
2956
3092
|
const hasVariations = existingVariations.some((v) => v !== null);
|
|
2957
3093
|
if (!hasVariations) {
|
|
2958
3094
|
existingVariations[0] = variationString;
|
|
@@ -3002,49 +3138,25 @@ server.registerTool("save_variation", {
|
|
|
3002
3138
|
const check = await requireBridge();
|
|
3003
3139
|
if (check !== true)
|
|
3004
3140
|
return check;
|
|
3005
|
-
// 1. Canonical save snapshot (engine + hosted-plugin state), then shape it
|
|
3006
3141
|
const { state, canonical } = await fetchSaveSnapshot();
|
|
3007
3142
|
const variationString = buildVariationString(state);
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
let diskPath;
|
|
3011
|
-
const activePath = state.activePresetPath;
|
|
3012
|
-
if (!presetName && activePath) {
|
|
3013
|
-
const filename = activePath.split("/").pop() || "";
|
|
3014
|
-
presetName = filename.replace(/\.preset$/, "");
|
|
3015
|
-
// Use active path for disk lookup (preserves subfolder structure).
|
|
3016
|
-
// Strip "Presets/" prefix if present — resolvePresetPath already prepends
|
|
3017
|
-
// the presets base directory, so "Presets/User/X" would double up to
|
|
3018
|
-
// ".../Presets/Presets/User/X" and silently fail to read back variations.
|
|
3019
|
-
diskPath = normalizePresetLookupPath(activePath);
|
|
3020
|
-
}
|
|
3021
|
-
if (!presetName && lastSavedPresetName) {
|
|
3022
|
-
presetName = lastSavedPresetName;
|
|
3023
|
-
diskPath = lastSavedPresetPath;
|
|
3143
|
+
if (!name && state.activePresetPath === "Init") {
|
|
3144
|
+
return error("No active preset. Call save_preset with a name first, or pass a name to save_variation.");
|
|
3024
3145
|
}
|
|
3146
|
+
const activePath = typeof state.activePresetPath === "string" && state.activePresetPath !== "Init"
|
|
3147
|
+
? normalizePresetLookupPath(state.activePresetPath) : "";
|
|
3148
|
+
const presetName = name || (activePath.split("/").pop() || "").replace(/\.preset$/, "") || lastSavedPresetName;
|
|
3025
3149
|
if (!presetName) {
|
|
3026
|
-
return error("No preset name
|
|
3027
|
-
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
try {
|
|
3036
|
-
const fullPath = await resolvePresetPath(diskPath);
|
|
3037
|
-
const raw = await readFile(fullPath, "utf-8");
|
|
3038
|
-
const parsed = parsePresetFile(raw);
|
|
3039
|
-
existingMeta = parsed.metadata;
|
|
3040
|
-
variations = variationsFromParsedPreset(parsed);
|
|
3041
|
-
}
|
|
3042
|
-
catch {
|
|
3043
|
-
// New preset or unreadable — start fresh
|
|
3044
|
-
}
|
|
3045
|
-
// 4. Update the target slot
|
|
3150
|
+
return error("No active preset. Call save_preset with a name first, or pass a name to save_variation.");
|
|
3151
|
+
}
|
|
3152
|
+
const diskPath = name ? `User/${presetName}.preset`
|
|
3153
|
+
: activePath || lastSavedPresetPath || `User/${presetName}.preset`;
|
|
3154
|
+
const sourcePath = selectSaveVariationSourcePath(activePath, diskPath);
|
|
3155
|
+
const parsed = await readPresetForSave(sourcePath, !!name && !activePath);
|
|
3156
|
+
const existingMeta = parsed?.metadata ?? {};
|
|
3157
|
+
const variations = parsed
|
|
3158
|
+
? variationsFromParsedPreset(parsed) : Array(PRESET_VARIATION_SLOTS).fill(null);
|
|
3046
3159
|
variations[index] = variationString;
|
|
3047
|
-
// 5. Build metadata: merge existing (preserves author/tags/etc.) then override with caller args
|
|
3048
3160
|
const metadata = {
|
|
3049
3161
|
...existingMeta,
|
|
3050
3162
|
name: presetName,
|
|
@@ -3053,12 +3165,13 @@ server.registerTool("save_variation", {
|
|
|
3053
3165
|
currentVariation: index,
|
|
3054
3166
|
...Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined)),
|
|
3055
3167
|
};
|
|
3056
|
-
|
|
3168
|
+
delete metadata.path;
|
|
3057
3169
|
let resultSuffix = "";
|
|
3058
|
-
|
|
3059
|
-
|
|
3170
|
+
const destinationPath = normalizePresetLookupPath(diskPath);
|
|
3171
|
+
if (!name && !destinationPath.startsWith("Factory/")) {
|
|
3172
|
+
metadata.path = destinationPath;
|
|
3060
3173
|
}
|
|
3061
|
-
else if (
|
|
3174
|
+
else if (sourcePath.startsWith("Factory/")) {
|
|
3062
3175
|
// Cannot write back to factory location; savePreset will create User/{name}.preset
|
|
3063
3176
|
resultSuffix = "\n(Note: active preset is a factory preset — saved as new user preset)";
|
|
3064
3177
|
}
|
|
@@ -3159,7 +3272,7 @@ server.registerTool("set_bpm", {
|
|
|
3159
3272
|
});
|
|
3160
3273
|
server.registerTool("trim_sample", {
|
|
3161
3274
|
title: "Trim Sample",
|
|
3162
|
-
description: "Trim a cell's sample to its start/end markers
|
|
3275
|
+
description: "Trim a cell's sample to its start/end markers: writes a new WAV into Samples/Resamples/Trimmed/ and points the cell at it. The original file is untouched, but a preset saved afterwards references the trimmed copy, and calling it again crops the trimmed file further.",
|
|
3163
3276
|
inputSchema: {
|
|
3164
3277
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
3165
3278
|
},
|
|
@@ -3172,7 +3285,7 @@ server.registerTool("trim_sample", {
|
|
|
3172
3285
|
});
|
|
3173
3286
|
server.registerTool("reverse_sample", {
|
|
3174
3287
|
title: "Reverse Sample",
|
|
3175
|
-
description: "Reverse a cell's loaded sample.",
|
|
3288
|
+
description: "Reverse a cell's loaded sample. Writes <name>_reversed.<ext> next to the source file and points the cell at it; calling it again on an already-reversed sample restores the original instead of reversing twice.",
|
|
3176
3289
|
inputSchema: {
|
|
3177
3290
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
3178
3291
|
},
|
|
@@ -3224,11 +3337,117 @@ server.registerTool("connection_status", {
|
|
|
3224
3337
|
connectedTo: bridge.getConnectedInstance(),
|
|
3225
3338
|
note: connected
|
|
3226
3339
|
? instances.length > 1
|
|
3227
|
-
? `Connected. Multiple ${SERVER_PRODUCT.name} instances are running;
|
|
3228
|
-
: `Connected to ${SERVER_PRODUCT.name}.
|
|
3340
|
+
? `Connected. Multiple ${SERVER_PRODUCT.name} instances are running; use select_instance to choose; reconnects stay pinned to that instance.`
|
|
3341
|
+
: `Connected to ${SERVER_PRODUCT.name}. Tool availability also depends on this plugin build, open windows, and feature settings.`
|
|
3229
3342
|
: "Found instances but could not connect. The plugin may have shut down.",
|
|
3230
3343
|
});
|
|
3231
3344
|
});
|
|
3345
|
+
server.registerTool("situation", {
|
|
3346
|
+
title: "Situation",
|
|
3347
|
+
description: "One-call orientation snapshot: discovered instances, the connected instance's product/pid/executable " +
|
|
3348
|
+
"(plus build mtime and a dev-build heuristic when resolvable), server and manifest identity, active preset, " +
|
|
3349
|
+
"transport, and per-cell occupancy counts. Start a session here — it collapses the connection/product/build " +
|
|
3350
|
+
"preflight and the first design reads into one round trip. Deeper reads: get_design_overview, design_context.",
|
|
3351
|
+
inputSchema: {},
|
|
3352
|
+
annotations: { readOnlyHint: true },
|
|
3353
|
+
}, async () => {
|
|
3354
|
+
const instances = await bridge.discover().catch(() => []);
|
|
3355
|
+
const connected = bridge.isConnected() || (await bridge.connect().catch(() => false)) === true;
|
|
3356
|
+
if (connected)
|
|
3357
|
+
syncProductHint();
|
|
3358
|
+
const instance = connected ? bridge.getConnectedInstance() : null;
|
|
3359
|
+
let build;
|
|
3360
|
+
if (instance?.pid) {
|
|
3361
|
+
try {
|
|
3362
|
+
const executable = execFileSync("ps", ["-o", "comm=", "-p", String(instance.pid)], {
|
|
3363
|
+
encoding: "utf8",
|
|
3364
|
+
}).trim();
|
|
3365
|
+
if (executable) {
|
|
3366
|
+
build = {
|
|
3367
|
+
executable,
|
|
3368
|
+
looksLikeDevBuild: executable.includes("/build/") && !executable.startsWith("/Applications/"),
|
|
3369
|
+
};
|
|
3370
|
+
try {
|
|
3371
|
+
build.executableMtime = statSync(executable).mtime.toISOString();
|
|
3372
|
+
}
|
|
3373
|
+
catch {
|
|
3374
|
+
/* executable path unreadable from this process */
|
|
3375
|
+
}
|
|
3376
|
+
const projectRoot = process.env.PAM_PROJECT_ROOT;
|
|
3377
|
+
if (projectRoot && typeof build.executableMtime === "string") {
|
|
3378
|
+
try {
|
|
3379
|
+
const headIso = execFileSync("git", ["-C", resolve(projectRoot), "log", "-1", "--format=%cI"], { encoding: "utf8" }).trim();
|
|
3380
|
+
build.repoHeadCommitTime = headIso;
|
|
3381
|
+
build.binaryOlderThanHeadCommit =
|
|
3382
|
+
new Date(build.executableMtime).getTime() < new Date(headIso).getTime();
|
|
3383
|
+
}
|
|
3384
|
+
catch {
|
|
3385
|
+
/* PAM_PROJECT_ROOT is not a git checkout */
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
catch {
|
|
3391
|
+
/* ps unavailable (Windows) or pid already gone */
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
let live;
|
|
3395
|
+
let manifestName;
|
|
3396
|
+
if (connected) {
|
|
3397
|
+
const [stateRaw, transportRaw, manifest] = await Promise.all([
|
|
3398
|
+
bridge.send("getState").catch(() => undefined),
|
|
3399
|
+
bridge.send("getTransport").catch(() => undefined),
|
|
3400
|
+
loadManifest().catch(() => null),
|
|
3401
|
+
]);
|
|
3402
|
+
manifestName = manifest?.name;
|
|
3403
|
+
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
3404
|
+
const transport = isRecord(transportRaw) ? transportRaw : {};
|
|
3405
|
+
if (manifest) {
|
|
3406
|
+
const effectNames = cellEffectNames(manifest);
|
|
3407
|
+
const ids = [...new Set(manifest.parameters
|
|
3408
|
+
.map((p) => p.cell)
|
|
3409
|
+
.filter((cell) => typeof cell === "string" && /^cell\d+$/.test(cell))
|
|
3410
|
+
.map((cell) => Number(cell.replace("cell", ""))))].sort((a, b) => a - b);
|
|
3411
|
+
const cellsObj = isRecord(state.cells) ? state.cells : {};
|
|
3412
|
+
const cells = ids.map((id) => {
|
|
3413
|
+
const prefix = `cell${id}_`;
|
|
3414
|
+
const rawCellState = cellsObj[String(id)];
|
|
3415
|
+
const cellState = isRecord(rawCellState) ? rawCellState : {};
|
|
3416
|
+
const samplePath = cellState.filePath ??
|
|
3417
|
+
cellState.samplePath ??
|
|
3418
|
+
state[`${prefix}filePath`] ??
|
|
3419
|
+
state[`${prefix}samplePath`];
|
|
3420
|
+
const synth = IS_MFX_SERVER ? undefined : describeSynthEngine(state[`${prefix}synthEngine`]);
|
|
3421
|
+
return {
|
|
3422
|
+
cellId: id,
|
|
3423
|
+
sample: stemName(samplePath),
|
|
3424
|
+
synthEngine: synth && synth.id > 0 ? synth.name : undefined,
|
|
3425
|
+
muted: state[`${prefix}mute`] === 1 || state[`${prefix}mute`] === true,
|
|
3426
|
+
enabledEffects: effectNames.filter((n) => state[`${prefix}${n}Enabled`] === 1 || state[`${prefix}${n}Enabled`] === true).length,
|
|
3427
|
+
};
|
|
3428
|
+
});
|
|
3429
|
+
live = {
|
|
3430
|
+
activePreset: state.activePresetPath,
|
|
3431
|
+
bpm: state.bpm ?? transport.bpm,
|
|
3432
|
+
playing: transport.playing,
|
|
3433
|
+
cells,
|
|
3434
|
+
...(IS_MFX_SERVER ? { mix: state.mfx_mix, routing: state.mfx_routing } : {}),
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
return json({
|
|
3439
|
+
server: { name: SERVER_PRODUCT.name, version: SERVER_VERSION },
|
|
3440
|
+
instances,
|
|
3441
|
+
connected,
|
|
3442
|
+
instance,
|
|
3443
|
+
manifest: manifestName,
|
|
3444
|
+
build,
|
|
3445
|
+
live,
|
|
3446
|
+
note: connected
|
|
3447
|
+
? undefined
|
|
3448
|
+
: "Not connected. connection_status has enable instructions; offline tools still work.",
|
|
3449
|
+
});
|
|
3450
|
+
});
|
|
3232
3451
|
server.registerTool("select_instance", {
|
|
3233
3452
|
title: "Select Plugin Instance",
|
|
3234
3453
|
description: "Choose which running plugin instance (PAM / CELL1 / MFX) the live tools drive when more than " +
|
|
@@ -3393,14 +3612,12 @@ function fitListing(base, itemsKey, items, moreKey, total) {
|
|
|
3393
3612
|
}
|
|
3394
3613
|
server.registerTool("list_samples", {
|
|
3395
3614
|
title: "List Samples",
|
|
3396
|
-
description: "Browse sample library directories.
|
|
3397
|
-
"get_default_locations
|
|
3398
|
-
"paths here. Paths vary per user and platform.",
|
|
3615
|
+
description: "Browse sample library directories. Library paths vary per user and platform, so take them from " +
|
|
3616
|
+
"get_default_locations rather than guessing.",
|
|
3399
3617
|
inputSchema: {
|
|
3400
3618
|
path: z
|
|
3401
3619
|
.string()
|
|
3402
|
-
.describe("Directory path to list
|
|
3403
|
-
"or a subdirectory of one. Do not guess paths like ~/Music/PAM."),
|
|
3620
|
+
.describe("Directory path to list: a path from get_default_locations, or a subdirectory of one."),
|
|
3404
3621
|
audioOnly: z
|
|
3405
3622
|
.boolean()
|
|
3406
3623
|
.default(true)
|
|
@@ -3489,8 +3706,9 @@ server.registerTool("get_audio_info", {
|
|
|
3489
3706
|
});
|
|
3490
3707
|
server.registerTool("export_preset", {
|
|
3491
3708
|
title: "Export Preset",
|
|
3492
|
-
description: "Export a preset as a zip file bundled with all referenced samples
|
|
3493
|
-
"
|
|
3709
|
+
description: "Export a preset as a zip file bundled with all referenced samples, written to PAM's Exports folder as " +
|
|
3710
|
+
"<outputFolderName>.zip (get_default_locations resolves that folder). An existing folder or zip of that " +
|
|
3711
|
+
"name is replaced, and presets belonging to a preset pack are refused.",
|
|
3494
3712
|
inputSchema: {
|
|
3495
3713
|
presetPath: z
|
|
3496
3714
|
.string()
|
|
@@ -3508,7 +3726,10 @@ server.registerTool("export_preset", {
|
|
|
3508
3726
|
});
|
|
3509
3727
|
server.registerTool("import_preset", {
|
|
3510
3728
|
title: "Import Preset",
|
|
3511
|
-
description: "Import a preset from a zip file or folder into PAM's preset library."
|
|
3729
|
+
description: "Import a preset from a zip file or folder into PAM's preset library. Returns the relative path of the " +
|
|
3730
|
+
"first imported preset, or an empty string on failure. Referenced samples are copied into the user sample " +
|
|
3731
|
+
"folder and the preset's paths are rewritten. A name already in the library is never overwritten: the " +
|
|
3732
|
+
"import lands as <name>_1, <name>_2 and so on.",
|
|
3512
3733
|
inputSchema: {
|
|
3513
3734
|
path: z.string().describe("Absolute path to the preset zip file or folder to import."),
|
|
3514
3735
|
},
|
|
@@ -3718,7 +3939,7 @@ server.registerTool("capture_ui", {
|
|
|
3718
3939
|
"PresetBrowser; omit/null = cell grid), instrumentView ('synth'|'sampler', applied to " +
|
|
3719
3940
|
"all cells). REQUIRES PAM's window OPEN — html2canvas needs the live DOM, there is no " +
|
|
3720
3941
|
"headless renderer (same constraint as the video tools). Beats screen-capture: " +
|
|
3721
|
-
"deterministic, no cursor/screen control, exact UI bitmap.",
|
|
3942
|
+
"deterministic, no cursor/screen control, exact UI bitmap. View arguments switch the live editor and the previous view is not restored.",
|
|
3722
3943
|
inputSchema: {
|
|
3723
3944
|
gridSize: z.number().optional().describe("Cell grid size to set before capture: 1, 4, or 8."),
|
|
3724
3945
|
mainView: z
|
|
@@ -3732,7 +3953,7 @@ server.registerTool("capture_ui", {
|
|
|
3732
3953
|
settleMs: z.number().optional().describe("Delay (ms) after view changes before rasterizing. Default 350."),
|
|
3733
3954
|
timeoutMs: z.number().optional().default(8000).describe("Max ms to wait for the WebView to return the image."),
|
|
3734
3955
|
},
|
|
3735
|
-
annotations: { readOnlyHint: true },
|
|
3956
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
3736
3957
|
}, async ({ gridSize, mainView, instrumentView, settleMs, timeoutMs }) => {
|
|
3737
3958
|
const check = await requireBridge();
|
|
3738
3959
|
if (check !== true)
|
|
@@ -4112,13 +4333,23 @@ server.registerTool("set_video_settings", {
|
|
|
4112
4333
|
"loads on the next bar boundary while playing). resetWorkspace clears the " +
|
|
4113
4334
|
"scene (preserves the master layer).",
|
|
4114
4335
|
inputSchema: {
|
|
4115
|
-
syncMode: z
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4336
|
+
syncMode: z
|
|
4337
|
+
.enum(["off", "beat-sync", "beat-match"])
|
|
4338
|
+
.optional()
|
|
4339
|
+
.describe("Clock source: 'off' free-runs, 'beat-sync' locks playback to the bar grid, 'beat-match' retimes clips to the transport."),
|
|
4340
|
+
loopBars: z.number().min(0).optional().describe("Loop length in bars for the beat-sync and beat-match modes."),
|
|
4341
|
+
aspectRatio: z
|
|
4342
|
+
.enum(["auto", "16:9", "4:3", "1:1", "stretch"])
|
|
4343
|
+
.optional()
|
|
4344
|
+
.describe("How a layer is fitted to the canvas. 'auto' keeps the source ratio, 'stretch' fills it."),
|
|
4345
|
+
renderQuality: z.enum(["low", "medium", "high"]).optional().describe("VX render resolution/effort tier."),
|
|
4346
|
+
designAspect: z
|
|
4347
|
+
.number()
|
|
4348
|
+
.nullable()
|
|
4349
|
+
.optional()
|
|
4350
|
+
.describe("Canvas design ratio as width/height (e.g. 1.7777 for 16:9). null clears it and follows the window."),
|
|
4351
|
+
letterboxColor: z.string().describe("#hex fill for the bars outside the design aspect.").optional(),
|
|
4352
|
+
canvasColor: z.string().describe("#hex background behind every VX layer.").optional(),
|
|
4122
4353
|
modEnabled: z.boolean().optional().describe("Mod-matrix master enable."),
|
|
4123
4354
|
vxVariationFollowAudio: z
|
|
4124
4355
|
.boolean()
|
|
@@ -4240,8 +4471,9 @@ const PATTERN_SUFFIXES = [
|
|
|
4240
4471
|
];
|
|
4241
4472
|
server.registerTool("reset_cell", {
|
|
4242
4473
|
title: "Reset Cell",
|
|
4243
|
-
description: "Reset
|
|
4244
|
-
"
|
|
4474
|
+
description: "Reset a single cell's manifest parameters to their defaults and remove the envelopes whose SOURCE is this cell. " +
|
|
4475
|
+
"Untouched: envelopes targeting this cell from another source (clear_modulations), pattern lanes (reset_patterns), " +
|
|
4476
|
+
"the cell config block (update_cell_config) and the loaded sample. Does not touch other cells.",
|
|
4245
4477
|
inputSchema: {
|
|
4246
4478
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
4247
4479
|
},
|
|
@@ -4274,8 +4506,9 @@ server.registerTool("reset_cell", {
|
|
|
4274
4506
|
});
|
|
4275
4507
|
server.registerTool("copy_cell", {
|
|
4276
4508
|
title: "Copy Cell",
|
|
4277
|
-
description: "Copy
|
|
4278
|
-
"
|
|
4509
|
+
description: "Copy a cell's manifest parameters and remap its LFO, envelope and vary links onto another cell, overwriting the target's. " +
|
|
4510
|
+
"Not copied: the loaded sample, parameter sequencers, and the cell config block (polyphony, MIDI note/channel, scale, " +
|
|
4511
|
+
"sidechain source, pitch/stretch engine, slice keymap) — re-apply those with load_sample and update_cell_config.",
|
|
4279
4512
|
inputSchema: {
|
|
4280
4513
|
sourceCellId: z.number().min(1).max(8).describe("Source cell (1-8)."),
|
|
4281
4514
|
targetCellId: z.number().min(1).max(8).describe("Target cell (1-8)."),
|
|
@@ -4385,7 +4618,7 @@ server.registerTool("copy_cell", {
|
|
|
4385
4618
|
server.registerTool("clear_modulations", {
|
|
4386
4619
|
title: "Clear Modulations",
|
|
4387
4620
|
description: "Remove all LFO, envelope, and vary modulation links targeting a cell's parameters " +
|
|
4388
|
-
"(or originating from that cell, for envelopes/vary).
|
|
4621
|
+
"(or originating from that cell, for envelopes/vary). Parameter sequencers survive — use remove_param_seq for those.",
|
|
4389
4622
|
inputSchema: {
|
|
4390
4623
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
4391
4624
|
},
|
|
@@ -4530,7 +4763,7 @@ server.registerTool("update_effect_config", {
|
|
|
4530
4763
|
// unreachable through set_parameters (numbers only) — an agent could shape an MFX
|
|
4531
4764
|
// cell but never patch one to another, which is the whole product.
|
|
4532
4765
|
//
|
|
4533
|
-
// Validation mirrors parseRoutingEdges (interface/src/dsp/mfxRouting.ts): the
|
|
4766
|
+
// Validation mirrors parseRoutingEdges (interface/src/dsp/products/mfx/mfxRouting.ts): the
|
|
4534
4767
|
// plugin re-validates on receipt, so this pass exists to fail loudly here rather
|
|
4535
4768
|
// than have a typo silently drop a cable.
|
|
4536
4769
|
const MFX_ROUTING_SOURCE_HINT = "'input', 'sidechain', 'split_lo' / 'split_mid' / 'split_hi', or 'cellN'. " +
|
|
@@ -4634,7 +4867,7 @@ server.registerTool("set_mfx_edge_gain", {
|
|
|
4634
4867
|
description: "Read or set the gain for one cable in the current MFX patch. Gain is linear: 0 is silent, 1 is unity, " +
|
|
4635
4868
|
"and 1.5 is the maximum. The cable must already exist. Other cable gains are preserved.",
|
|
4636
4869
|
inputSchema: {
|
|
4637
|
-
op: z.enum(["get", "set"]),
|
|
4870
|
+
op: z.enum(["get", "set"]).describe("'get' returns every cable gain; 'set' writes one."),
|
|
4638
4871
|
from: z.string().optional().describe(`Cable source for 'set': ${MFX_ROUTING_SOURCE_HINT}`),
|
|
4639
4872
|
to: z.string().optional().describe(`Cable destination for 'set': ${MFX_ROUTING_SINK_HINT}`),
|
|
4640
4873
|
gain: z.number().min(0).max(1.5).optional().describe("Linear gain for 'set'."),
|
|
@@ -4680,9 +4913,12 @@ server.registerTool("configure_mfx_combo", {
|
|
|
4680
4913
|
op: z.enum([
|
|
4681
4914
|
"get", "setValue", "rename", "lock", "unlock", "rescan", "setTargets", "addTarget",
|
|
4682
4915
|
"removeTarget", "clearTargets", "invertTarget", "setTargetStop", "anchor",
|
|
4683
|
-
])
|
|
4916
|
+
]).describe("get reads the combo; setValue writes its value and every resolved target; rename relabels it (blank restores the role label); " +
|
|
4917
|
+
"lock freezes the current automatic assignment as manual; unlock and rescan return it to automatic; " +
|
|
4918
|
+
"setTargets/addTarget/removeTarget/clearTargets edit the manual target list; invertTarget swaps a target's from/to stops; " +
|
|
4919
|
+
"setTargetStop sets one target's stop value; anchor re-derives every target's stops from the current parameter values."),
|
|
4684
4920
|
index: z.number().int().min(1).max(3).default(1),
|
|
4685
|
-
value: z.number().
|
|
4921
|
+
value: z.number().optional().describe("Combo value (−100 to 100) for setValue, or a value in the target parameter's units for setTargetStop."),
|
|
4686
4922
|
name: z.string().max(24).optional().describe("New label for rename; blank restores the role label."),
|
|
4687
4923
|
paramId: z.string().optional().describe("Target parameter for add/remove/invert/setTargetStop."),
|
|
4688
4924
|
stop: z.enum(["from", "mid", "to"]).optional().describe("Target stop edited by setTargetStop."),
|
|
@@ -4707,6 +4943,8 @@ server.registerTool("configure_mfx_combo", {
|
|
|
4707
4943
|
if (op === "setValue") {
|
|
4708
4944
|
if (typeof value !== "number")
|
|
4709
4945
|
return error("setValue requires value.");
|
|
4946
|
+
if (value < -100 || value > 100)
|
|
4947
|
+
return error("Combo value must be between -100 and 100.");
|
|
4710
4948
|
const delta = { [`mfx_combo${index}`]: value };
|
|
4711
4949
|
for (const target of combo.targets) {
|
|
4712
4950
|
const info = parameterIndex.get(target.id);
|
|
@@ -4923,8 +5161,8 @@ server.registerTool("update_cell_config", {
|
|
|
4923
5161
|
"An out-of-vocabulary string would silently play as chromatic while junk persists into the preset, so the name is validated."),
|
|
4924
5162
|
scaleRoot: z.number().min(0).max(11).optional().describe("Root note of the scale (0=C, 1=C#, ..., 11=B)."),
|
|
4925
5163
|
pitchMode: z.number().int().min(0).max(3).optional().describe("Pitch engine: 0=Varispeed, 1=Formant, 2=Pitch Shift Quantize, 3=Varispeed Quantize."),
|
|
4926
|
-
stretchMode: z.number().int().min(0).max(3).optional().describe("Stretch engine: 0=OLA, 1=Cloud, 2=
|
|
4927
|
-
quantScale: z.number().int().min(0).max(9).optional().describe("Pitch quantize scale when pitchMode=2 (Pitch Shift Quantize) or 3 (Varispeed Quantize): 0=Chromatic, 1=Major, 2=Minor, 3=Penta Maj, 4=Penta Min, 5=Dorian, 6=Mixolydian, 7=Blues, 8=Harm Min, 9=Phrygian. Order locked to QuantScale in modules/
|
|
5164
|
+
stretchMode: z.number().int().min(0).max(3).optional().describe("Stretch engine: 0=Default (OLA), 1=Cloud, 2=Spectral, 3=Beats."),
|
|
5165
|
+
quantScale: z.number().int().min(0).max(9).optional().describe("Pitch quantize scale when pitchMode=2 (Pitch Shift Quantize) or 3 (Varispeed Quantize): 0=Chromatic, 1=Major, 2=Minor, 3=Penta Maj, 4=Penta Min, 5=Dorian, 6=Mixolydian, 7=Blues, 8=Harm Min, 9=Phrygian. Order locked to QuantScale in modules/dsp-core/Scale.h."),
|
|
4928
5166
|
quantRoot: z.number().int().min(0).max(11).optional().describe("Pitch quantize root when pitchMode=2 (Pitch Shift Quantize) or 3 (Varispeed Quantize): 0=C, 1=C#, ..., 11=B."),
|
|
4929
5167
|
grainDensity: z.number().int().min(1).max(24).optional().describe("Cloud stretch grain density when stretchMode=1."),
|
|
4930
5168
|
grainWindow: z.number().int().min(0).max(4).optional().describe("Cloud stretch grain envelope when stretchMode=1: 0=Hann, 1=Tukey, 2=Welch, 3=Punch, 4=Swell."),
|
|
@@ -5002,7 +5240,7 @@ server.registerTool("set_cell_mute", {
|
|
|
5002
5240
|
description: "Mute, unmute, solo, or unsolo a cell in the mixer — the real channel mute/solo, cutting the whole channel " +
|
|
5003
5241
|
"(source + all effect tails) rather than pulling the volume fader down. Soloing a cell silences every other " +
|
|
5004
5242
|
"non-soloed cell until unsoloed. cell{N}_mute / cell{N}_solo are live-polled state variables (no graph rebuild). " +
|
|
5005
|
-
"Prefer this over
|
|
5243
|
+
"Prefer this over pulling cell{N}_volume down: a fader move leaves effect tails audible, a channel mute cuts them.",
|
|
5006
5244
|
inputSchema: {
|
|
5007
5245
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
5008
5246
|
mute: z.boolean().optional().describe("Mute (true) or unmute (false) the cell's channel."),
|
|
@@ -5043,11 +5281,7 @@ server.registerTool("set_master_effects", {
|
|
|
5043
5281
|
"Check results with get_master_analysis (keep tp <= -1 dBTP, PSR >= 8 in the loudest section). " +
|
|
5044
5282
|
"The Tape is a vintage-tape coloration stage (ADAA tanh/asymmetric saturation + HF compression + wow/flutter), " +
|
|
5045
5283
|
"the single biggest lever for lo-fi / dusty / 'pushed' master character (hip-hop, lo-fi, Memphis, dub). " +
|
|
5046
|
-
"Pass enabled flags to toggle each on/off. Pass parameters to shape the sound. " +
|
|
5047
|
-
"For pumpy beats: enable punch, set threshold around -18 to -12 dB, fast attack (0.1-0.5ms), " +
|
|
5048
|
-
"medium release (80-200ms), ratio 4-8, autoMakeup true. " +
|
|
5049
|
-
"For lo-fi/Memphis/dusty glue: tapeEnabled=1, tapeMode=1 (Lo-Fi), masterTapeDrive 0.55-0.7, " +
|
|
5050
|
-
"masterTapeTone -0.4..-0.1 (darker), masterTapeWear 0.5-0.7 (wow/flutter). " +
|
|
5284
|
+
"Pass enabled flags to toggle each on/off. Pass parameters to shape the sound; genre recipes live in the beat-making-guide resource. " +
|
|
5051
5285
|
"Squeeze (masterSqueeze*) is the 3-band up/down density stage between Punch and Tape — for dense modern-electronic masters set masterSqueezeEnabled=1 with masterSqueezeDepth 0.2-0.4; it lifts quiet detail and limits loud bands per band (a character stage, not glue — Punch stays the glue). " +
|
|
5052
5286
|
"NOTE: the Tape and Squeeze stages require a PAM build that ships them (flat state keys, not manifest params). On older builds these keys are ignored.",
|
|
5053
5287
|
inputSchema: {
|
|
@@ -5081,7 +5315,7 @@ server.registerTool("set_master_effects", {
|
|
|
5081
5315
|
masterEqHsGain: z.number().min(-12).max(12).optional().describe("High-shelf gain dB (default 0)."),
|
|
5082
5316
|
masterWidth: z.number().min(0).max(2).optional().describe("Stereo width (0=mono, 1=unity, 2=double side; default 1). Check corr in get_master_analysis after widening."),
|
|
5083
5317
|
masterBassMonoFreq: z.number().min(0).max(300).optional().describe("Elliptical bass-mono corner Hz; < 20 = off. Engage (~100-120) when corrLow < +0.7."),
|
|
5084
|
-
masterClipEnabled: z.union([z.literal(0), z.literal(1)]).optional().describe("Enable the oversampled soft clipper (post-Tape, pre-Limiter).
|
|
5318
|
+
masterClipEnabled: z.union([z.literal(0), z.literal(1)]).optional().describe("Enable the oversampled soft clipper (post-Tape, pre-Limiter). Its 23 samples of latency are reported whether it is on or off, so toggling it never moves the host's compensation."),
|
|
5085
5319
|
masterClipDrive: z.number().min(0).max(12).optional().describe("Clipper drive dB (default 0). Staging guidance: the clipper shaves the sharpest 2-3 dB so the limiter works 1-2 dB."),
|
|
5086
5320
|
masterClipMix: z.number().min(0).max(1).optional().describe("Clipper parallel blend (1 = fully clipped)."),
|
|
5087
5321
|
masterClipMode: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]).optional().describe("Clipper transfer: 0=Smooth (knee-tanh), 1=Silicon diode (hard knee), 2=Germanium diode (earlier, softer knee), 3=LED (higher knee). Diode modes are exact antiparallel-pair solves, ADAA at the 2x rate. Default 0."),
|
|
@@ -5262,7 +5496,7 @@ server.registerTool("set_drum_synth_wave", {
|
|
|
5262
5496
|
"centre, tension −1..+1 with 0 = linear). PAM bakes them into a DC-removed, peak-normalized single cycle " +
|
|
5263
5497
|
"that the Wave engine's Draw param (p8) crossfades against the factory bank. Only the Wave engine reads it — " +
|
|
5264
5498
|
"select 'Wave' via set_drum_synth and raise Draw (p8) to hear it. Runs in the WebView (the curve transform " +
|
|
5265
|
-
"is single-sourced there), so PAM's window must be OPEN
|
|
5499
|
+
"is single-sourced there), so PAM's window must be OPEN with drum-synth wave editing support.",
|
|
5266
5500
|
inputSchema: {
|
|
5267
5501
|
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
5268
5502
|
points: z
|
|
@@ -5293,6 +5527,394 @@ server.registerTool("set_drum_synth_wave", {
|
|
|
5293
5527
|
return text(`Set cell ${cellId} Wave-engine custom waveform (${points.length} breakpoints). ` +
|
|
5294
5528
|
`Select the Wave engine and raise Draw (p8) to hear it. (Runs in the WebView — ensure PAM's window is open.)`);
|
|
5295
5529
|
});
|
|
5530
|
+
// Generative Players (per-cell PLAYER mode). Arming semantics mirror
|
|
5531
|
+
// interface/src/interop.ts setCellPlayer; the registry is shared/generativePlayers.json.
|
|
5532
|
+
const PLAYER_REF_HELP = `one of ${PLAYER_NAMES.join(", ")} (or its id 1..${GENERATIVE_PLAYERS.length}), ` +
|
|
5533
|
+
"'role' for the cell's role default, or 'off'";
|
|
5534
|
+
function playerProductError(manifest) {
|
|
5535
|
+
if (manifest && manifest.generativePatterns?.enabled !== true) {
|
|
5536
|
+
return error(`Generative players are PAM-only. ${manifest.name ?? "This product"}'s manifest exposes none.`);
|
|
5537
|
+
}
|
|
5538
|
+
return null;
|
|
5539
|
+
}
|
|
5540
|
+
const playerSlotArg = (n) => z
|
|
5541
|
+
.union([z.number().min(0).max(1), z.string()])
|
|
5542
|
+
.optional()
|
|
5543
|
+
.describe(`P${n} slot: 0..1 (snapped to the slot's detents) or a position label such as 'Techno' or 'Triad'. ` +
|
|
5544
|
+
"Its meaning is per player — list_players names it.");
|
|
5545
|
+
server.registerTool("list_players", {
|
|
5546
|
+
title: "List Generative Players",
|
|
5547
|
+
description: "Catalogue of PAM's generative Players — per-cell generators that replace a cell's drawn pattern with a " +
|
|
5548
|
+
"seeded, transport-synced cycle (EUCLID, KIT, TURING, WALK, MARKOV, RATCHET, PHASE, RIFF, MASK). Returns each " +
|
|
5549
|
+
"player's blurb, defaults, P-slot vocabulary (detented positions or 0..1 knobs) and curated preset names, plus " +
|
|
5550
|
+
"the role spread (which player each cell arms by default). Offline — no bridge needed. Arm one with set_player.",
|
|
5551
|
+
inputSchema: {
|
|
5552
|
+
player: z.string().optional().describe("Return one player's full detail (name or id). Omit for the whole roster."),
|
|
5553
|
+
presets: z
|
|
5554
|
+
.boolean()
|
|
5555
|
+
.optional()
|
|
5556
|
+
.describe("Include each curated preset's values, not only its name. Default false."),
|
|
5557
|
+
},
|
|
5558
|
+
annotations: { readOnlyHint: true },
|
|
5559
|
+
}, async ({ player, presets }) => {
|
|
5560
|
+
const manifest = await loadManifestForProductGuard();
|
|
5561
|
+
const available = manifest ? manifest.generativePatterns?.enabled === true : true;
|
|
5562
|
+
const spec = player ? resolvePlayerSpec(player) : undefined;
|
|
5563
|
+
if (player && !spec)
|
|
5564
|
+
return error(`Unknown player '${player}' — ${PLAYER_REF_HELP}.`);
|
|
5565
|
+
const specs = spec ? [spec] : GENERATIVE_PLAYERS;
|
|
5566
|
+
return json({
|
|
5567
|
+
available,
|
|
5568
|
+
...(available ? {} : { note: `${manifest?.name ?? "This product"} exposes no players; the catalogue is informational.` }),
|
|
5569
|
+
players: specs.map((entry) => ({
|
|
5570
|
+
...describePlayerSpec(entry, true),
|
|
5571
|
+
...(presets ? { presetValues: playerPresetsFor(entry.id) } : {}),
|
|
5572
|
+
})),
|
|
5573
|
+
roleSpread: roleSpreadSummary(),
|
|
5574
|
+
notes: [
|
|
5575
|
+
"A player replaces what the cell PLAYS, never the drawn pattern: set_player 'off' restores the grid exactly.",
|
|
5576
|
+
"Sequencer output needs the transport running (start_transport).",
|
|
5577
|
+
"Melodic players arm the cell poly with the root on its trigger note; 'off' restores both.",
|
|
5578
|
+
"KIT follows its Genre slot — pass genre to set_player so it matches the track.",
|
|
5579
|
+
"patternMode 'keep' layers the player over the drawn pattern instead of masking it.",
|
|
5580
|
+
],
|
|
5581
|
+
});
|
|
5582
|
+
});
|
|
5583
|
+
server.registerTool("set_player", {
|
|
5584
|
+
title: "Set Generative Player",
|
|
5585
|
+
description: "Arm, edit or switch off a cell's generative Player (PAM-only). Passing `player` arms that generator with the " +
|
|
5586
|
+
"roster's own voicing (or a curated `preset`), the track genre on genre-aware players, and a reversible poly " +
|
|
5587
|
+
"arm on melodic ones; omit `player` to edit the running player's values in place. Values land in both the cell " +
|
|
5588
|
+
"config and the cell{N}_player* automation params, like the plugin UI. 'off' restores the drawn pattern and the " +
|
|
5589
|
+
"cell's pre-arm polyphony. The player sounds only while the transport runs. The UI shows a running player " +
|
|
5590
|
+
"unprompted; to arm from the UI as well, update_settings { experimentalMode: true, experimentalGenerativePatterns: true }.",
|
|
5591
|
+
inputSchema: {
|
|
5592
|
+
cellIds: z
|
|
5593
|
+
.array(z.number().int().min(1).max(8))
|
|
5594
|
+
.min(1)
|
|
5595
|
+
.describe("Cells to write (1-8). Several cells take the same write, each snapped to its own player."),
|
|
5596
|
+
player: z
|
|
5597
|
+
.union([z.string(), z.number().int().min(0)])
|
|
5598
|
+
.optional()
|
|
5599
|
+
.describe(`Player to arm: ${PLAYER_REF_HELP}. Omit to edit the running player.`),
|
|
5600
|
+
preset: z.string().optional().describe("Curated preset name for the armed or running player — list_players lists them."),
|
|
5601
|
+
genre: z
|
|
5602
|
+
.enum(PLAYER_GENRES)
|
|
5603
|
+
.optional()
|
|
5604
|
+
.describe("Genre for genre-aware players (KIT's Genre slot). Defaults to the roster default when arming."),
|
|
5605
|
+
density: z.number().min(0).max(1).optional().describe("How much plays, 0..1. Audible from the next step."),
|
|
5606
|
+
mutate: z.number().min(0).max(1).optional().describe("Cycle-to-cycle evolution, 0..1 (below 0.02 locks the loop)."),
|
|
5607
|
+
p1: playerSlotArg(1),
|
|
5608
|
+
p2: playerSlotArg(2),
|
|
5609
|
+
p3: playerSlotArg(3),
|
|
5610
|
+
p4: playerSlotArg(4),
|
|
5611
|
+
p5: playerSlotArg(5),
|
|
5612
|
+
seed: z
|
|
5613
|
+
.union([z.number().int().min(0), z.literal("random")])
|
|
5614
|
+
.optional()
|
|
5615
|
+
.describe("Player seed (uint31) or 'random' to reroll — the DICE gesture."),
|
|
5616
|
+
patternMode: z
|
|
5617
|
+
.enum(PATTERN_MODES)
|
|
5618
|
+
.optional()
|
|
5619
|
+
.describe("'mask' (default): the player replaces the drawn pattern. 'keep': the drawn pattern keeps sounding and the player layers over it."),
|
|
5620
|
+
listenMode: z
|
|
5621
|
+
.enum(LISTEN_MODES)
|
|
5622
|
+
.optional()
|
|
5623
|
+
.describe("Ensemble listening: react to another cell's last completed cycle — complement its gaps, answer it, or shade velocity harmonically."),
|
|
5624
|
+
listenSource: z.number().int().min(0).max(8).optional().describe("Listening source: 0 = all other cells, 1-8 = one cell (never itself)."),
|
|
5625
|
+
listenAmount: z.number().min(0).max(1).optional().describe("Listening strength 0..1."),
|
|
5626
|
+
poly: z
|
|
5627
|
+
.boolean()
|
|
5628
|
+
.optional()
|
|
5629
|
+
.describe("Override the automatic poly arm: true forces poly, false keeps the cell mono even for a melodic player."),
|
|
5630
|
+
},
|
|
5631
|
+
}, async ({ cellIds, player, preset, genre, density, mutate, p1, p2, p3, p4, p5, seed, patternMode, listenMode, listenSource, listenAmount, poly }) => {
|
|
5632
|
+
const check = await requireBridgeForCells(cellIds);
|
|
5633
|
+
if (check !== true)
|
|
5634
|
+
return check;
|
|
5635
|
+
const manifest = await loadManifestForProductGuard();
|
|
5636
|
+
const gate = playerProductError(manifest);
|
|
5637
|
+
if (gate)
|
|
5638
|
+
return gate;
|
|
5639
|
+
const playerRef = typeof player === "string" ? player.trim().toLowerCase() : player;
|
|
5640
|
+
const wantsOff = playerRef === "off" || playerRef === 0;
|
|
5641
|
+
const wantsRole = playerRef === "role";
|
|
5642
|
+
const explicitSpec = player !== undefined && !wantsOff && !wantsRole ? resolvePlayerSpec(player) : undefined;
|
|
5643
|
+
if (player !== undefined && !wantsOff && !wantsRole && !explicitSpec) {
|
|
5644
|
+
return error(`Unknown player '${player}' — ${PLAYER_REF_HELP}.`);
|
|
5645
|
+
}
|
|
5646
|
+
const stateRaw = await bridge.send("getState");
|
|
5647
|
+
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
5648
|
+
const cellsObj = isRecord(state.cells) ? state.cells : {};
|
|
5649
|
+
const slotInputs = [p1, p2, p3, p4, p5];
|
|
5650
|
+
const flatDelta = {};
|
|
5651
|
+
const cellsDelta = {};
|
|
5652
|
+
const lines = [];
|
|
5653
|
+
for (const cellId of new Set(cellIds)) {
|
|
5654
|
+
const rawCell = cellsObj[String(cellId)];
|
|
5655
|
+
const cell = isRecord(rawCell) ? rawCell : {};
|
|
5656
|
+
const currentType = cellPlayerType(cell);
|
|
5657
|
+
const recorded = (key) => {
|
|
5658
|
+
const value = Number(cell[key]);
|
|
5659
|
+
return Number.isFinite(value) && value !== NO_POLY_ARM ? Math.round(value) : undefined;
|
|
5660
|
+
};
|
|
5661
|
+
if (wantsOff) {
|
|
5662
|
+
if (currentType <= 0) {
|
|
5663
|
+
lines.push(`Cell ${cellId}: no player running.`);
|
|
5664
|
+
continue;
|
|
5665
|
+
}
|
|
5666
|
+
const config = { playerType: 0, playerLastType: currentType };
|
|
5667
|
+
const prePoly = recorded("playerPrePoly");
|
|
5668
|
+
const preRoot = recorded("playerPreRoot");
|
|
5669
|
+
if (prePoly !== undefined)
|
|
5670
|
+
config.isPoly = prePoly;
|
|
5671
|
+
if (preRoot !== undefined)
|
|
5672
|
+
config.polyRootNote = preRoot;
|
|
5673
|
+
config.playerPrePoly = NO_POLY_ARM;
|
|
5674
|
+
config.playerPreRoot = NO_POLY_ARM;
|
|
5675
|
+
cellsDelta[String(cellId)] = config;
|
|
5676
|
+
lines.push(`Cell ${cellId}: ${getPlayerSpec(currentType)?.name ?? "player"} off — drawn pattern restored` +
|
|
5677
|
+
`${prePoly !== undefined || preRoot !== undefined ? ", pre-arm polyphony restored" : ""}.`);
|
|
5678
|
+
continue;
|
|
5679
|
+
}
|
|
5680
|
+
const roleConfig = wantsRole ? rolePlayerConfigFor(cellId) : undefined;
|
|
5681
|
+
const spec = explicitSpec ?? (roleConfig ? getPlayerSpec(roleConfig.playerType) : getPlayerSpec(currentType));
|
|
5682
|
+
if (!spec)
|
|
5683
|
+
return error(`Cell ${cellId} runs no player. Pass player to arm one — ${PLAYER_REF_HELP}.`);
|
|
5684
|
+
const arming = explicitSpec !== undefined || roleConfig !== undefined;
|
|
5685
|
+
const config = {};
|
|
5686
|
+
if (arming) {
|
|
5687
|
+
Object.assign(config, playerDefaultsFor(spec.id), playerGenreConfig(spec, genre));
|
|
5688
|
+
if (roleConfig?.playerP1 !== undefined)
|
|
5689
|
+
config.playerP1 = roleConfig.playerP1;
|
|
5690
|
+
if (roleConfig?.playerP5 !== undefined)
|
|
5691
|
+
config.playerP5 = roleConfig.playerP5;
|
|
5692
|
+
config.playerLastType = spec.id;
|
|
5693
|
+
}
|
|
5694
|
+
else if (genre) {
|
|
5695
|
+
Object.assign(config, playerGenreConfig(spec, genre));
|
|
5696
|
+
}
|
|
5697
|
+
if (preset) {
|
|
5698
|
+
const found = resolvePlayerPreset(spec, preset);
|
|
5699
|
+
if (!found) {
|
|
5700
|
+
const names = playerPresetsFor(spec.id).map((entry) => entry.name);
|
|
5701
|
+
return error(`${spec.name} has no preset '${preset}' — one of: ${names.join(", ")}.`);
|
|
5702
|
+
}
|
|
5703
|
+
Object.assign(config, presetConfig(found));
|
|
5704
|
+
}
|
|
5705
|
+
if (density !== undefined)
|
|
5706
|
+
config.playerDensity = density;
|
|
5707
|
+
if (mutate !== undefined)
|
|
5708
|
+
config.playerMutate = mutate;
|
|
5709
|
+
for (let i = 0; i < slotInputs.length; i++) {
|
|
5710
|
+
const input = slotInputs[i];
|
|
5711
|
+
if (input === undefined)
|
|
5712
|
+
continue;
|
|
5713
|
+
const slot = spec.slots[i];
|
|
5714
|
+
if (!slot)
|
|
5715
|
+
return error(`${spec.name} has no P${i + 1} slot (it has ${spec.slots.length}).`);
|
|
5716
|
+
const resolved = resolveSlotValue(slot, input);
|
|
5717
|
+
if (typeof resolved === "string")
|
|
5718
|
+
return error(`Cell ${cellId} ${spec.name} P${i + 1}: ${resolved}`);
|
|
5719
|
+
config[PLAYER_SLOT_FIELDS[i]] = resolved;
|
|
5720
|
+
}
|
|
5721
|
+
PLAYER_SLOT_FIELDS.forEach((field, i) => {
|
|
5722
|
+
if (config[field] !== undefined)
|
|
5723
|
+
config[field] = snapPlayerValue(spec.slots[i]?.steps, config[field]);
|
|
5724
|
+
});
|
|
5725
|
+
if (seed !== undefined)
|
|
5726
|
+
config.playerSeed = seed === "random" ? randomPlayerSeed() : Math.round(seed);
|
|
5727
|
+
if (patternMode)
|
|
5728
|
+
config.playerPatternMode = PATTERN_MODES.indexOf(patternMode);
|
|
5729
|
+
if (listenMode)
|
|
5730
|
+
config.playerListenMode = LISTEN_MODES.indexOf(listenMode);
|
|
5731
|
+
if (listenSource !== undefined)
|
|
5732
|
+
config.playerListenSource = listenSource;
|
|
5733
|
+
if (listenAmount !== undefined)
|
|
5734
|
+
config.playerListenAmount = listenAmount;
|
|
5735
|
+
// Reversible poly arm: record the pre-arm values once, never the armed state.
|
|
5736
|
+
const autoPoly = arming && poly !== false && (isMelodicPlayer(spec) || roleConfig?.isPoly === 1);
|
|
5737
|
+
let polyNote = "";
|
|
5738
|
+
if (autoPoly) {
|
|
5739
|
+
if (recorded("playerPrePoly") === undefined && recorded("playerPreRoot") === undefined) {
|
|
5740
|
+
config.playerPrePoly = Math.round(Number(cell.isPoly ?? 0)) || 0;
|
|
5741
|
+
config.playerPreRoot = Math.round(Number(cell.polyRootNote ?? 48)) || 48;
|
|
5742
|
+
}
|
|
5743
|
+
config.isPoly = 1;
|
|
5744
|
+
config.polyRootNote = Math.round(Number(cell.midiNote ?? 47 + cellId)) || 47 + cellId;
|
|
5745
|
+
polyNote = `; poly armed (root ${config.polyRootNote})`;
|
|
5746
|
+
}
|
|
5747
|
+
else if (poly !== undefined) {
|
|
5748
|
+
config.isPoly = poly ? 1 : 0;
|
|
5749
|
+
polyNote = `; poly ${poly ? "on" : "off"}`;
|
|
5750
|
+
}
|
|
5751
|
+
if (arming)
|
|
5752
|
+
config.playerType = spec.id;
|
|
5753
|
+
if (Object.keys(config).length === 0)
|
|
5754
|
+
return error("Nothing to write — pass player, preset, or at least one value.");
|
|
5755
|
+
cellsDelta[String(cellId)] = config;
|
|
5756
|
+
for (const field of MODULATABLE_PLAYER_FIELDS) {
|
|
5757
|
+
if (typeof config[field] === "number")
|
|
5758
|
+
flatDelta[`cell${cellId}_${field}`] = config[field];
|
|
5759
|
+
}
|
|
5760
|
+
const parts = [];
|
|
5761
|
+
if (config.playerDensity !== undefined)
|
|
5762
|
+
parts.push(`density ${playerDensityDisplay(config.playerDensity)}`);
|
|
5763
|
+
if (config.playerMutate !== undefined)
|
|
5764
|
+
parts.push(`mutate ${playerMutateDisplay(config.playerMutate)}`);
|
|
5765
|
+
spec.slots.forEach((slot, i) => {
|
|
5766
|
+
const value = config[PLAYER_SLOT_FIELDS[i]];
|
|
5767
|
+
if (value !== undefined)
|
|
5768
|
+
parts.push(`${slot.label}=${playerSlotDisplay(slot, value)}`);
|
|
5769
|
+
});
|
|
5770
|
+
if (config.playerSeed !== undefined)
|
|
5771
|
+
parts.push(`seed ${config.playerSeed}`);
|
|
5772
|
+
if (patternMode)
|
|
5773
|
+
parts.push(`pattern ${patternMode}`);
|
|
5774
|
+
if (listenMode)
|
|
5775
|
+
parts.push(`listen ${listenMode}`);
|
|
5776
|
+
lines.push(`Cell ${cellId}: ${spec.name} ${arming ? "armed" : "updated"} — ${parts.join(", ")}${polyNote}.`);
|
|
5777
|
+
}
|
|
5778
|
+
if (Object.keys(cellsDelta).length === 0)
|
|
5779
|
+
return text(lines.join("\n"));
|
|
5780
|
+
await applyKv({ ...flatDelta, cells: cellsDelta }, KV_OPTS_CELL_CONFIG);
|
|
5781
|
+
return text(`${lines.join("\n")}\n` +
|
|
5782
|
+
"Players sound only while the transport runs; get_design_overview reports each cell's player.");
|
|
5783
|
+
});
|
|
5784
|
+
/** A cell's sample as a readable file: absolute as given, else relative to a library root. */
|
|
5785
|
+
async function resolveCellSampleFile(filePath) {
|
|
5786
|
+
const expanded = expandHome(filePath);
|
|
5787
|
+
if (isAbsolute(expanded))
|
|
5788
|
+
return existsSync(expanded) ? expanded : undefined;
|
|
5789
|
+
const locations = await bridge.send("getDefaultLocations").catch(() => undefined);
|
|
5790
|
+
const roots = Array.isArray(locations)
|
|
5791
|
+
? locations.filter((entry) => isRecord(entry) && typeof entry.path === "string")
|
|
5792
|
+
: [];
|
|
5793
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
5794
|
+
const candidates = [];
|
|
5795
|
+
for (const root of roots) {
|
|
5796
|
+
const stripped = normalized.replace(/^Samples\/(User|Factory)\//, "");
|
|
5797
|
+
if (/user/i.test(root.name) && /^Samples\/User\//.test(normalized))
|
|
5798
|
+
candidates.push(join(root.path, stripped));
|
|
5799
|
+
if (/factory/i.test(root.name) && /^Samples\/Factory\//.test(normalized))
|
|
5800
|
+
candidates.push(join(root.path, stripped));
|
|
5801
|
+
candidates.push(join(root.path, normalized), join(root.path, stripped));
|
|
5802
|
+
}
|
|
5803
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
5804
|
+
}
|
|
5805
|
+
server.registerTool("set_slices", {
|
|
5806
|
+
title: "Set Sample Slices",
|
|
5807
|
+
description: "Slice the sample loaded in a cell: explicit `boundaries` (0..1 of the file), `count` equal slices, or " +
|
|
5808
|
+
"`transients` detected from the audio (WAV files, offline). Writes cell{N}_sliceBoundaries / _sliceCount — " +
|
|
5809
|
+
"what the slice lane, Slice Keymap, preview_slice and export_slice all read — and switches the Slice knob on. " +
|
|
5810
|
+
"`sequence:true` also chops the loop into the slice + note lanes so it plays back in order at native rate " +
|
|
5811
|
+
"(the Slice → Sequence gesture: modLatch on, both lanes at 1× over the loop's length in beats).",
|
|
5812
|
+
inputSchema: {
|
|
5813
|
+
cellId: z.number().int().min(1).max(8).describe("Cell number (1-8); it must have a sample loaded."),
|
|
5814
|
+
boundaries: z
|
|
5815
|
+
.array(z.number().min(0).max(1))
|
|
5816
|
+
.min(2)
|
|
5817
|
+
.max(MAX_SLICES + 1)
|
|
5818
|
+
.optional()
|
|
5819
|
+
.describe("Slice boundaries as 0..1 positions in the file, ascending; 0 and 1 are added when missing."),
|
|
5820
|
+
count: z.number().int().min(1).max(MAX_SLICES).optional().describe(`Equal slices, 1..${MAX_SLICES}.`),
|
|
5821
|
+
transients: z.boolean().optional().describe("Detect onsets in the sample and slice there (WAV only)."),
|
|
5822
|
+
sensitivity: z.number().min(0).max(1).optional().describe("Transient sensitivity 0..1 (default 0.5; higher finds more onsets)."),
|
|
5823
|
+
maxSlices: z.number().int().min(2).max(MAX_SLICES).optional().describe("Cap on transient slices (default 32)."),
|
|
5824
|
+
sequence: z.boolean().optional().describe("Also write the slice + note lanes so the slices play in order (loops)."),
|
|
5825
|
+
},
|
|
5826
|
+
}, async ({ cellId, boundaries, count, transients, sensitivity, maxSlices, sequence }) => {
|
|
5827
|
+
const check = await requireBridgeForCells(cellId);
|
|
5828
|
+
if (check !== true)
|
|
5829
|
+
return check;
|
|
5830
|
+
const manifest = await loadManifestForProductGuard();
|
|
5831
|
+
if (manifest && isMfxManifest(manifest))
|
|
5832
|
+
return error("set_slices needs a sampler cell; MFX cells carry no sample.");
|
|
5833
|
+
const modes = [boundaries !== undefined, count !== undefined, transients === true].filter(Boolean).length;
|
|
5834
|
+
if (modes !== 1)
|
|
5835
|
+
return error("Pass exactly one of boundaries, count, or transients:true.");
|
|
5836
|
+
const stateRaw = await bridge.send("getState");
|
|
5837
|
+
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
5838
|
+
const cellsObj = isRecord(state.cells) ? state.cells : {};
|
|
5839
|
+
const cell = isRecord(cellsObj[String(cellId)]) ? cellsObj[String(cellId)] : {};
|
|
5840
|
+
const filePath = [cell.filePath, cell.samplePath, state[`cell${cellId}_filePath`]].find((v) => typeof v === "string" && v.length > 0);
|
|
5841
|
+
if (!filePath)
|
|
5842
|
+
return error(`Cell ${cellId} has no sample loaded — load_sample first.`);
|
|
5843
|
+
let resolved;
|
|
5844
|
+
let wavDuration;
|
|
5845
|
+
if (boundaries)
|
|
5846
|
+
resolved = normalizeBoundaries(boundaries);
|
|
5847
|
+
else if (count !== undefined)
|
|
5848
|
+
resolved = equalBoundaries(count);
|
|
5849
|
+
else {
|
|
5850
|
+
const file = await resolveCellSampleFile(filePath);
|
|
5851
|
+
if (!file)
|
|
5852
|
+
return error(`Could not find '${filePath}' on disk to analyse — pass count or boundaries instead.`);
|
|
5853
|
+
let wav;
|
|
5854
|
+
try {
|
|
5855
|
+
wav = decodeWav(await readFile(file));
|
|
5856
|
+
}
|
|
5857
|
+
catch (e) {
|
|
5858
|
+
return error(`Transient slicing reads WAV files only (${e.message}) — pass count or boundaries for ${basename(file)}.`);
|
|
5859
|
+
}
|
|
5860
|
+
wavDuration = wav.durationSeconds;
|
|
5861
|
+
resolved = detectOnsetBoundaries(wav, sensitivity ?? 0.5, maxSlices ?? 32);
|
|
5862
|
+
if (resolved.length < 3)
|
|
5863
|
+
return error("No transients found above the threshold — raise sensitivity or pass count.");
|
|
5864
|
+
}
|
|
5865
|
+
const sliceCount = resolved.length - 1;
|
|
5866
|
+
const currentKnob = Number(state[`cell${cellId}_slice`]) || 0;
|
|
5867
|
+
const delta = {
|
|
5868
|
+
[`cell${cellId}_sliceBoundaries`]: resolved,
|
|
5869
|
+
[`cell${cellId}_sliceCount`]: sliceCount,
|
|
5870
|
+
};
|
|
5871
|
+
if (currentKnob < 1 || currentKnob > sliceCount)
|
|
5872
|
+
delta[`cell${cellId}_slice`] = 1;
|
|
5873
|
+
await applyKv(delta, KV_OPTS_CELL_CONFIG);
|
|
5874
|
+
let sequenceNote = "";
|
|
5875
|
+
if (sequence) {
|
|
5876
|
+
const transportRaw = await bridge.send("getTransport").catch(() => undefined);
|
|
5877
|
+
const bpm = Number(isRecord(transportRaw) ? transportRaw.bpm : undefined) || Number(state.bpm) || 120;
|
|
5878
|
+
let duration = wavDuration;
|
|
5879
|
+
if (duration === undefined) {
|
|
5880
|
+
const info = await bridge.send("getAudioFileInfo", filePath).catch(() => undefined);
|
|
5881
|
+
const parsed = typeof info === "string" ? JSON.parse(info) : info;
|
|
5882
|
+
duration = [parsed?.duration, parsed?.durationSeconds, parsed?.lengthSeconds, parsed?.length]
|
|
5883
|
+
.map(Number)
|
|
5884
|
+
.find((v) => Number.isFinite(v) && v > 0);
|
|
5885
|
+
}
|
|
5886
|
+
const loopBeats = duration ? (duration * bpm) / 60 : Number(state[`cell${cellId}_patternLengthSlice`]) || 16;
|
|
5887
|
+
const patternLength = Math.max(1, Math.min(32, duration ? Math.ceil(loopBeats) : loopBeats));
|
|
5888
|
+
const note = typeof cell.midiNote === "number" ? cell.midiNote : 48 + cellId - 1;
|
|
5889
|
+
const channel = typeof cell.midiChannel === "number" ? cell.midiChannel : 1;
|
|
5890
|
+
const sliceEvents = [];
|
|
5891
|
+
const noteEvents = [];
|
|
5892
|
+
for (let i = 0; i < sliceCount; i++) {
|
|
5893
|
+
const time = resolved[i] * loopBeats;
|
|
5894
|
+
const dur = (resolved[i + 1] - resolved[i]) * loopBeats;
|
|
5895
|
+
if (dur <= 0)
|
|
5896
|
+
continue;
|
|
5897
|
+
sliceEvents.push({ note, channel, time, duration: dur, velocity: 100, value: i + 1 });
|
|
5898
|
+
noteEvents.push({ note, channel, time, duration: dur, velocity: 100 });
|
|
5899
|
+
}
|
|
5900
|
+
await applyKv({
|
|
5901
|
+
[`cell${cellId}_sliceSequence`]: sliceEvents,
|
|
5902
|
+
[`cell${cellId}_midiSequence`]: noteEvents,
|
|
5903
|
+
[`cell${cellId}_patternLengthSlice`]: patternLength,
|
|
5904
|
+
[`cell${cellId}_patternLength`]: patternLength,
|
|
5905
|
+
[`cell${cellId}_patternSpeedSlice`]: 9,
|
|
5906
|
+
[`cell${cellId}_patternSpeed`]: 9,
|
|
5907
|
+
[`cell${cellId}_patternDirection`]: 0,
|
|
5908
|
+
[`cell${cellId}_slice`]: 1,
|
|
5909
|
+
cells: { [cellId]: { modLatch: 1 } },
|
|
5910
|
+
__forceRender__: true,
|
|
5911
|
+
}, KV_OPTS_PATTERN);
|
|
5912
|
+
sequenceNote = ` Chopped into the slice + note lanes over ${patternLength} beats at ${bpm} BPM; start_transport to hear it.`;
|
|
5913
|
+
}
|
|
5914
|
+
const positions = resolved.slice(1, -1).map((b) => `${Math.round(b * 1000) / 10}%`);
|
|
5915
|
+
return text(`Cell ${cellId}: ${sliceCount} slice${sliceCount === 1 ? "" : "s"}` +
|
|
5916
|
+
`${positions.length ? ` (cuts at ${positions.join(", ")})` : ""}.${sequenceNote}`);
|
|
5917
|
+
});
|
|
5296
5918
|
server.registerTool("add_param_seq", {
|
|
5297
5919
|
title: "Add Parameter Sequencer",
|
|
5298
5920
|
description: "Add a parameter sequencer (step modulation) targeting any modulatable parameter. " +
|
|
@@ -5309,8 +5931,16 @@ server.registerTool("add_param_seq", {
|
|
|
5309
5931
|
bipolar: z.union([z.literal(0), z.literal(1)]).default(0).describe("0 = unipolar, 1 = bipolar."),
|
|
5310
5932
|
length: z.number().min(0.0625).max(16).default(1).describe("Pattern length in bars."),
|
|
5311
5933
|
speed: z.number().min(0.0625).max(16).default(1).describe("Pattern speed multiplier."),
|
|
5934
|
+
freeMode: z
|
|
5935
|
+
.union([z.literal(0), z.literal(1)])
|
|
5936
|
+
.default(0)
|
|
5937
|
+
.describe("0 = locked to the source cell's clock and its own steps; 1 = mapped to the Free Sequencer (its clock AND its master steps, set_free_sequencer)."),
|
|
5938
|
+
stepMode: z
|
|
5939
|
+
.union([z.literal(0), z.literal(1)])
|
|
5940
|
+
.default(0)
|
|
5941
|
+
.describe("0 = Clock; 1 = Gate: advance one step per trigger of the source cell (cell-locked only; MFX has no Gate)."),
|
|
5312
5942
|
},
|
|
5313
|
-
}, async ({ paramId, cellId, sequence, strength, bipolar, length, speed }) => {
|
|
5943
|
+
}, async ({ paramId, cellId, sequence, strength, bipolar, length, speed, freeMode, stepMode }) => {
|
|
5314
5944
|
const check = await requireBridgeForCells(cellId);
|
|
5315
5945
|
if (check !== true)
|
|
5316
5946
|
return check;
|
|
@@ -5333,7 +5963,8 @@ server.registerTool("add_param_seq", {
|
|
|
5333
5963
|
bipolar,
|
|
5334
5964
|
boost: 0,
|
|
5335
5965
|
interpolate: 0,
|
|
5336
|
-
freeMode
|
|
5966
|
+
freeMode,
|
|
5967
|
+
stepMode,
|
|
5337
5968
|
};
|
|
5338
5969
|
paramSeqs.push(newSeq);
|
|
5339
5970
|
await applyKv({ paramSeqs }, KV_OPTS_MOD);
|
|
@@ -5346,11 +5977,19 @@ server.registerTool("update_param_seq", {
|
|
|
5346
5977
|
inputSchema: {
|
|
5347
5978
|
paramId: z.string().describe("Target parameter ID."),
|
|
5348
5979
|
sequence: z.array(z.number().min(0).max(1)).optional().describe("New step values (0-1)."),
|
|
5349
|
-
strength: z.number().min(0).max(1).optional(),
|
|
5350
|
-
length: z.number().min(0.0625).max(16).optional(),
|
|
5351
|
-
speed: z.number().min(0.0625).max(16).optional(),
|
|
5352
|
-
bipolar: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5353
|
-
enabled: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
5980
|
+
strength: z.number().min(0).max(1).optional().describe("Modulation depth (0-1)."),
|
|
5981
|
+
length: z.number().min(0.0625).max(16).optional().describe("Pattern length in bars."),
|
|
5982
|
+
speed: z.number().min(0.0625).max(16).optional().describe("Pattern speed multiplier."),
|
|
5983
|
+
bipolar: z.union([z.literal(0), z.literal(1)]).optional().describe("0 = unipolar, 1 = bipolar."),
|
|
5984
|
+
enabled: z.union([z.literal(0), z.literal(1)]).optional().describe("0 = bypass the sequencer, 1 = run it."),
|
|
5985
|
+
freeMode: z
|
|
5986
|
+
.union([z.literal(0), z.literal(1)])
|
|
5987
|
+
.optional()
|
|
5988
|
+
.describe("0 = cell-locked with its own steps; 1 = mapped to the Free Sequencer master contour (set_free_sequencer)."),
|
|
5989
|
+
stepMode: z
|
|
5990
|
+
.union([z.literal(0), z.literal(1)])
|
|
5991
|
+
.optional()
|
|
5992
|
+
.describe("0 = Clock; 1 = Gate (one step per source-cell trigger; falls back to Clock when free or on MFX)."),
|
|
5354
5993
|
},
|
|
5355
5994
|
}, async ({ paramId, ...updates }) => {
|
|
5356
5995
|
const check = await requireBridge();
|
|
@@ -5374,6 +6013,105 @@ server.registerTool("update_param_seq", {
|
|
|
5374
6013
|
await applyKv({ paramSeqs }, KV_OPTS_MOD);
|
|
5375
6014
|
return text(`Updated paramSeq ${paramId}: ${Object.keys(cleanUpdates).join(", ")}`);
|
|
5376
6015
|
});
|
|
6016
|
+
server.registerTool("set_free_sequencer", {
|
|
6017
|
+
title: "Set Free Sequencer",
|
|
6018
|
+
description: "Write the Free Sequencer's master step contour (top-level `freeSeqSequence`, 2..64 steps of 0..1). Every " +
|
|
6019
|
+
"parameter sequencer mapped to it (freeMode:1 via add_param_seq / update_param_seq) follows these steps AND its " +
|
|
6020
|
+
"clock live, each with its own strength, polarity and smoothing; cell-locked and Gate-mode sequencers keep " +
|
|
6021
|
+
"their own steps. An empty contour restores the legacy per-sequencer behaviour.",
|
|
6022
|
+
inputSchema: {
|
|
6023
|
+
sequence: z.array(z.number().min(0).max(1)).min(2).max(64).describe("Master step values, 0..1 each."),
|
|
6024
|
+
},
|
|
6025
|
+
}, async ({ sequence }) => {
|
|
6026
|
+
const check = await requireBridge();
|
|
6027
|
+
if (check !== true)
|
|
6028
|
+
return check;
|
|
6029
|
+
await bridge.send("applyFreeSeqSequenceUpdate", { sequence });
|
|
6030
|
+
return text(`Free Sequencer: ${sequence.length} steps written. Map a paramSeq with freeMode:1 to follow it.`);
|
|
6031
|
+
});
|
|
6032
|
+
server.registerTool("export_midi", {
|
|
6033
|
+
title: "Export MIDI",
|
|
6034
|
+
description: "Write the cells' drawn note patterns to a Standard MIDI File (type 1, 480 PPQ, tempo from the transport): " +
|
|
6035
|
+
"one track per cell on MIDI channel = cell number, each pattern tiled across `bars`. Pure file write — no " +
|
|
6036
|
+
"render, nothing changes in PAM. Generated Player cycles are not drawn patterns; FREEZE them first (in the UI) " +
|
|
6037
|
+
"to export what a player is playing. Default location is PAM's Exports folder.",
|
|
6038
|
+
inputSchema: {
|
|
6039
|
+
filename: z.string().optional().describe("Base name without extension (default pam_midi_<epoch>)."),
|
|
6040
|
+
cellIds: z.array(z.number().int().min(1).max(8)).optional().describe("Cells to include (default: every cell with notes)."),
|
|
6041
|
+
bars: z.number().min(1).max(64).optional().describe("Length in 4/4 bars (default: the longest included pattern cycle, at least 1)."),
|
|
6042
|
+
mode: z.enum(["combined", "stems"]).optional().describe("'combined' (default): one multi-track file. 'stems': one file per cell."),
|
|
6043
|
+
outputDir: z.string().optional().describe("Directory to write into (default: the Exports location from get_default_locations)."),
|
|
6044
|
+
},
|
|
6045
|
+
}, async ({ filename, cellIds, bars, mode, outputDir }) => {
|
|
6046
|
+
const check = await requireBridgeForCells(cellIds);
|
|
6047
|
+
if (check !== true)
|
|
6048
|
+
return check;
|
|
6049
|
+
const manifest = await loadManifestForProductGuard();
|
|
6050
|
+
if (manifest && isMfxManifest(manifest))
|
|
6051
|
+
return error("export_midi needs sampler cells with note patterns; MFX has none.");
|
|
6052
|
+
const [stateRaw, transportRaw, locations] = await Promise.all([
|
|
6053
|
+
bridge.send("getState"),
|
|
6054
|
+
bridge.send("getTransport").catch(() => undefined),
|
|
6055
|
+
bridge.send("getDefaultLocations").catch(() => undefined),
|
|
6056
|
+
]);
|
|
6057
|
+
const state = isRecord(stateRaw) ? stateRaw : {};
|
|
6058
|
+
const bpm = Number(isRecord(transportRaw) ? transportRaw.bpm : undefined) || Number(state.bpm) || 120;
|
|
6059
|
+
let dir = outputDir ? expandHome(outputDir) : undefined;
|
|
6060
|
+
if (!dir) {
|
|
6061
|
+
if (Array.isArray(locations)) {
|
|
6062
|
+
const exportsEntry = locations.find((entry) => isRecord(entry) && /export/i.test(String(entry.name)));
|
|
6063
|
+
if (isRecord(exportsEntry) && typeof exportsEntry.path === "string")
|
|
6064
|
+
dir = exportsEntry.path;
|
|
6065
|
+
}
|
|
6066
|
+
else if (isRecord(locations)) {
|
|
6067
|
+
dir = [locations.exports, locations.exportsLocation, locations.Exports].find((v) => typeof v === "string" && v.length > 0);
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
if (!dir)
|
|
6071
|
+
return error("Could not resolve PAM's Exports folder from get_default_locations — pass outputDir.");
|
|
6072
|
+
const maxCell = manifestMaxCellId(manifest) ?? 8;
|
|
6073
|
+
const ids = (cellIds?.length ? cellIds : Array.from({ length: maxCell }, (_, i) => i + 1)).filter((id) => id <= maxCell);
|
|
6074
|
+
const cells = ids
|
|
6075
|
+
.map((id) => ({
|
|
6076
|
+
id,
|
|
6077
|
+
sequence: state[`cell${id}_midiSequence`],
|
|
6078
|
+
patternLength: state[`cell${id}_patternLength`],
|
|
6079
|
+
speed: state[`cell${id}_patternSpeed`],
|
|
6080
|
+
}))
|
|
6081
|
+
.filter((cell) => Array.isArray(cell.sequence) && cell.sequence.length > 0);
|
|
6082
|
+
if (cells.length === 0)
|
|
6083
|
+
return error("No note patterns to export — the selected cells carry no notes.");
|
|
6084
|
+
const cycleBars = cells.map((cell) => {
|
|
6085
|
+
const length = Number(cell.patternLength);
|
|
6086
|
+
const speedIndex = Math.round(Number(cell.speed));
|
|
6087
|
+
const mul = Number.isFinite(speedIndex) ? patternSpeedMultiplier(speedIndex) : 1;
|
|
6088
|
+
return (Number.isFinite(length) && length > 0 ? length : 4) / mul / 4;
|
|
6089
|
+
});
|
|
6090
|
+
const totalBars = bars ?? Math.max(1, Math.ceil(Math.max(...cycleBars)));
|
|
6091
|
+
const totalBeats = totalBars * 4;
|
|
6092
|
+
const tracks = cells.map((cell) => ({
|
|
6093
|
+
name: `Cell ${cell.id}`,
|
|
6094
|
+
events: tilePatternEvents(cell.sequence, cell.patternLength, cell.speed, totalBeats, Math.min(16, cell.id)),
|
|
6095
|
+
}));
|
|
6096
|
+
const base = (filename?.trim() || `pam_midi_${Date.now()}`).replace(/\.mid$/i, "").replace(/[\\/:]/g, "_");
|
|
6097
|
+
await mkdir(dir, { recursive: true });
|
|
6098
|
+
const written = [];
|
|
6099
|
+
if (mode === "stems") {
|
|
6100
|
+
for (const track of tracks) {
|
|
6101
|
+
const path = join(dir, `${base}_cell${track.name.slice(5)}.mid`);
|
|
6102
|
+
await writeFile(path, buildMidiFile(bpm, [track], track.name));
|
|
6103
|
+
written.push(path);
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
else {
|
|
6107
|
+
const path = join(dir, `${base}.mid`);
|
|
6108
|
+
await writeFile(path, buildMidiFile(bpm, tracks, base));
|
|
6109
|
+
written.push(path);
|
|
6110
|
+
}
|
|
6111
|
+
const noteCount = tracks.reduce((sum, track) => sum + track.events.length, 0);
|
|
6112
|
+
return text(`Exported ${noteCount} notes from ${tracks.length} cell${tracks.length === 1 ? "" : "s"} over ${totalBars} bar${totalBars === 1 ? "" : "s"} at ${bpm} BPM.\n` +
|
|
6113
|
+
written.map((path) => `File: ${path}`).join("\n"));
|
|
6114
|
+
});
|
|
5377
6115
|
server.registerTool("remove_param_seq", {
|
|
5378
6116
|
title: "Remove Parameter Sequencer",
|
|
5379
6117
|
description: "Remove a parameter sequencer for the given paramId.",
|
|
@@ -5462,7 +6200,9 @@ server.registerTool("export_audio", {
|
|
|
5462
6200
|
"'per-bus' (per output bus) · 'song-mode' (the Song Mode timeline).\n\n" +
|
|
5463
6201
|
"NOTE on 'song-mode': the offline bounce applies EVERY section on its anchor (PAM ≥ 1.5.7 with " +
|
|
5464
6202
|
"offline song export; older builds render only the first section). Wall-clock sources " +
|
|
5465
|
-
"(Magenta/Notochord) render silent offline — use the `export_song` tool's live strategy for those."
|
|
6203
|
+
"(Magenta/Notochord) render silent offline — use the `export_song` tool's live strategy for those. " +
|
|
6204
|
+
"It issues the same render as `export_song` with strategy 'offline'; prefer `export_song` for arrangements, " +
|
|
6205
|
+
"which is the only route to the live and audio+video strategies.",
|
|
5466
6206
|
inputSchema: {
|
|
5467
6207
|
bars: z.number().min(1).max(128).default(4).describe("Bars to render (ignored for 'song-mode', which derives length from the timeline)."),
|
|
5468
6208
|
cellId: z
|
|
@@ -5480,9 +6220,9 @@ server.registerTool("export_audio", {
|
|
|
5480
6220
|
.optional()
|
|
5481
6221
|
.describe("For mode 'selected-cells': cells to bounce as stems."),
|
|
5482
6222
|
bitDepth: z
|
|
5483
|
-
.
|
|
6223
|
+
.union([z.literal(16), z.literal(24)])
|
|
5484
6224
|
.default(24)
|
|
5485
|
-
.describe("Bit depth
|
|
6225
|
+
.describe("Bit depth: 16 or 24."),
|
|
5486
6226
|
},
|
|
5487
6227
|
}, async ({ bars, cellId, mode, selectedCellIds, bitDepth }) => {
|
|
5488
6228
|
const check = await requireBridgeForCells(cellId, selectedCellIds);
|
|
@@ -6033,10 +6773,10 @@ if (!IS_MFX_SERVER)
|
|
|
6033
6773
|
},
|
|
6034
6774
|
}, ({ genre, bpm }) => promptText(`Build a beat in PAM${genre ? ` in the style of ${genre}` : ""}${bpm ? ` at ${bpm} BPM` : ""}. Work in stages, listening between them:\n\n` +
|
|
6035
6775
|
`1. connection_status to confirm the bridge, then get_design_overview for the current state.\n` +
|
|
6036
|
-
`2.
|
|
6037
|
-
`3. ${
|
|
6776
|
+
`2. ${bpm ? `set_bpm to ${bpm}` : "set_bpm to the genre's tempo"}.\n` +
|
|
6777
|
+
`3. Build the kit and groove with randomize target:'full'${genre ? ` and genreHint '${genre}'` : " (pass a genreHint)"}; native chains samples before patterns. The reply is queued: poll get_state for changed cells and cellN_midiSequence before editing/saving. For specific sounds: get_default_locations → list_samples → load_sample, verify paths, then randomize_patterns and verify sequences.\n` +
|
|
6038
6778
|
`4. start_transport — sequencer output needs the transport rolling.\n` +
|
|
6039
|
-
`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.\n` +
|
|
6779
|
+
`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` +
|
|
6040
6780
|
`6. save_preset once it works, and update_preset_metadata with genre tags and BPM.`));
|
|
6041
6781
|
if (!IS_MFX_SERVER)
|
|
6042
6782
|
server.registerPrompt("make-a-song", {
|
|
@@ -6109,7 +6849,7 @@ async function applyLoopersDelta(loopers, extra) {
|
|
|
6109
6849
|
server.registerTool("arm_looper_track", {
|
|
6110
6850
|
title: "Arm / Disarm Looper Track",
|
|
6111
6851
|
description: "Arm or disarm a looper track for recording. Arming alone does not start " +
|
|
6112
|
-
"recording — call start_looper_recording afterwards
|
|
6852
|
+
"recording — call start_looper_recording afterwards, or pass armedCellIds to start_looper_recording to arm and record in one call.",
|
|
6113
6853
|
inputSchema: {
|
|
6114
6854
|
cellId: z.number().min(1).max(8).describe("Looper track / cell (1-8)."),
|
|
6115
6855
|
armed: z.boolean().describe("true to arm, false to disarm."),
|
|
@@ -6123,8 +6863,8 @@ server.registerTool("arm_looper_track", {
|
|
|
6123
6863
|
});
|
|
6124
6864
|
server.registerTool("set_looper_track", {
|
|
6125
6865
|
title: "Set Looper Track State",
|
|
6126
|
-
description: "Update one or more per-track looper fields (mute, overdub mode, reverse, " +
|
|
6127
|
-
"pitch, warp, capture source, level). Pass only the fields you want to change.",
|
|
6866
|
+
description: "Update one or more per-track looper fields (mute, solo, overdub mode, reverse, " +
|
|
6867
|
+
"pitch, warp, capture source, level, auto-select and recorded BPM). Pass only the fields you want to change.",
|
|
6128
6868
|
inputSchema: {
|
|
6129
6869
|
cellId: z.number().min(1).max(8).describe("Looper track / cell (1-8)."),
|
|
6130
6870
|
mute: z.boolean().optional().describe("Mute / unmute."),
|
|
@@ -6427,7 +7167,7 @@ server.registerTool("looper_variation", {
|
|
|
6427
7167
|
data: z
|
|
6428
7168
|
.record(z.string(), z.unknown())
|
|
6429
7169
|
.optional()
|
|
6430
|
-
.describe("For 'save': the looper variation snapshot (object).
|
|
7170
|
+
.describe("For 'save': the looper variation snapshot (object). Omit this field to clear the slot instead."),
|
|
6431
7171
|
},
|
|
6432
7172
|
}, async (args) => {
|
|
6433
7173
|
const check = await requireBridge();
|
|
@@ -6505,21 +7245,21 @@ server.registerTool("queue_variation", {
|
|
|
6505
7245
|
inputSchema: {
|
|
6506
7246
|
index: z.number().min(0).max(7).describe("Variation slot 0-7."),
|
|
6507
7247
|
intervalBars: z.number().min(0.125).max(64).describe("Bars before switching."),
|
|
6508
|
-
fadeMs: z.number().min(0).max(2000).
|
|
7248
|
+
fadeMs: z.number().min(0).max(2000).optional().describe("Crossfade duration in ms. Omit all transition options to inherit the native Bar Sync/Song Mode settings."),
|
|
6509
7249
|
resetPhase: z
|
|
6510
7250
|
.boolean()
|
|
6511
|
-
.
|
|
6512
|
-
.describe("Reset pattern phase on switch."),
|
|
7251
|
+
.optional()
|
|
7252
|
+
.describe("Reset pattern phase on switch. Omit to inherit native settings."),
|
|
6513
7253
|
morphBars: z
|
|
6514
7254
|
.number()
|
|
6515
7255
|
.min(0.25)
|
|
6516
7256
|
.max(8)
|
|
6517
7257
|
.optional()
|
|
6518
|
-
.describe("Pre-anchor morph window in bars for this flip
|
|
7258
|
+
.describe("Pre-anchor morph window in bars for this flip. Omit all transition options to inherit native settings."),
|
|
6519
7259
|
morphMode: z
|
|
6520
7260
|
.enum(["auto", "param", "mix", "seq", "all"])
|
|
6521
7261
|
.optional()
|
|
6522
|
-
.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
|
|
7262
|
+
.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."),
|
|
6523
7263
|
morphCurve: z
|
|
6524
7264
|
.enum(["linear", "ease", "exp", "log"])
|
|
6525
7265
|
.optional()
|
|
@@ -6532,7 +7272,7 @@ server.registerTool("queue_variation", {
|
|
|
6532
7272
|
tailMaxBars: z.number().min(0.25).max(8).optional(),
|
|
6533
7273
|
})
|
|
6534
7274
|
.optional()
|
|
6535
|
-
.describe("Advanced transition settings for this flip
|
|
7275
|
+
.describe("Advanced transition settings for this flip: pattern policy for a " +
|
|
6536
7276
|
"seq/all morph, blend gain law for a mix/all morph, and the outgoing section's " +
|
|
6537
7277
|
"tail. Omitted fields keep the defaults (dissolve, equalPower, fade)."),
|
|
6538
7278
|
},
|
|
@@ -6543,8 +7283,8 @@ server.registerTool("queue_variation", {
|
|
|
6543
7283
|
await bridge.send("queueVariationOnBars", {
|
|
6544
7284
|
index,
|
|
6545
7285
|
intervalBars,
|
|
6546
|
-
fadeMs,
|
|
6547
|
-
resetPhase,
|
|
7286
|
+
...(fadeMs !== undefined && { fadeMs }),
|
|
7287
|
+
...(resetPhase !== undefined && { resetPhase }),
|
|
6548
7288
|
...(morphBars !== undefined && { morphBars }),
|
|
6549
7289
|
...(morphMode !== undefined && { morphMode }),
|
|
6550
7290
|
...(morphCurve !== undefined && { morphCurve }),
|
|
@@ -6585,7 +7325,7 @@ server.registerTool("update_variations", {
|
|
|
6585
7325
|
server.registerTool("variation_hold", {
|
|
6586
7326
|
title: "Variation Hold",
|
|
6587
7327
|
description: "'Hold while note pressed' mode for variations. Use this to map MIDI notes to momentary " +
|
|
6588
|
-
"variation switches
|
|
7328
|
+
"variation switches. Hold mode reverts when the note releases; latch mode stays.",
|
|
6589
7329
|
inputSchema: {
|
|
6590
7330
|
enabled: z.boolean().optional().describe("Toggle hold mode on/off."),
|
|
6591
7331
|
allNotesOff: z
|
|
@@ -6707,7 +7447,6 @@ server.registerTool("update_modulation", {
|
|
|
6707
7447
|
title: "Update Modulation",
|
|
6708
7448
|
description: "Apply a delta to a modulation source: LFO, Envelope, Vary, Macro, ParamSeq config, ParamSeq sequence data, " +
|
|
6709
7449
|
"or the Free Sequencer master pattern. " +
|
|
6710
|
-
"The payload is type-specific; see modulation update flows in interop.ts (applyLfoUpdate / updateEnvelope / etc.). " +
|
|
6711
7450
|
"Use add_modulation to first attach a source to a target — then this tool to edit it.",
|
|
6712
7451
|
inputSchema: {
|
|
6713
7452
|
type: z
|
|
@@ -6715,7 +7454,11 @@ server.registerTool("update_modulation", {
|
|
|
6715
7454
|
.describe("Which modulation system to update."),
|
|
6716
7455
|
payload: z
|
|
6717
7456
|
.record(z.string(), z.unknown())
|
|
6718
|
-
.describe("Update payload object
|
|
7457
|
+
.describe("Update payload object, one property per call. lfo, envelope, vary, macro and paramSeq all take " +
|
|
7458
|
+
"{ index, property, value }, where index is the 0-based position in that state array (get_state " +
|
|
7459
|
+
"section 'lfos' / 'envelopes' / 'vary' / 'macros' / 'paramSeqs') and property is the field name to " +
|
|
7460
|
+
"overwrite. paramSeqSequence takes { index, sequence: number[] }. freeSeqSequence takes " +
|
|
7461
|
+
"{ sequence: number[] } — the master step contour every free-mode ParamSeq follows."),
|
|
6719
7462
|
},
|
|
6720
7463
|
}, async ({ type, payload }) => {
|
|
6721
7464
|
const check = await requireBridge();
|
|
@@ -6741,13 +7484,6 @@ server.registerTool("update_modulation", {
|
|
|
6741
7484
|
// ============================================================
|
|
6742
7485
|
// TOOLS — Song mode
|
|
6743
7486
|
// ============================================================
|
|
6744
|
-
// Advanced transition spec (ADR-001, PAM >= 1.19.6; engine families live from
|
|
6745
|
-
// PAM >= 1.20.0). Honest contract: it persists, round-trips, and reads back
|
|
6746
|
-
// RESOLVED. Pattern policy (dissolve/wipe/cells), mix law (equal power / equal
|
|
6747
|
-
// gain / correlation), tail (cut/fade/ring) and phase run on song VARIATION
|
|
6748
|
-
// sections and queued bar-sync flips; preset destinations coerce to a param
|
|
6749
|
-
// glide, and offline bounces coerce ring→fade and blend→hard switch (each
|
|
6750
|
-
// reported, never silent). Wire enums are append-only.
|
|
6751
7487
|
const morphCurveEnum = z.enum(["linear", "ease", "exp", "log"]);
|
|
6752
7488
|
const transitionSpecSchema = z.object({
|
|
6753
7489
|
mode: z.enum(["auto", "param", "mix", "seq", "all"]).optional(),
|
|
@@ -6757,6 +7493,10 @@ const transitionSpecSchema = z.object({
|
|
|
6757
7493
|
paramCurve: morphCurveEnum.optional(),
|
|
6758
7494
|
patternCurve: morphCurveEnum.optional(),
|
|
6759
7495
|
mixCurve: morphCurveEnum.optional(),
|
|
7496
|
+
staggerBy: z.enum(["off", "cell", "category"]).optional(),
|
|
7497
|
+
staggerSpread: z.number().min(0).max(0.9).optional(),
|
|
7498
|
+
staggerOrder: z.enum(["forward", "reverse"]).optional(),
|
|
7499
|
+
relations: z.enum(["off", "stepped"]).optional(),
|
|
6760
7500
|
})
|
|
6761
7501
|
.optional(),
|
|
6762
7502
|
pattern: z.object({ policy: z.enum(["dissolve", "wipe", "cells"]).optional() }).optional(),
|
|
@@ -6769,23 +7509,25 @@ const transitionSpecSchema = z.object({
|
|
|
6769
7509
|
.optional(),
|
|
6770
7510
|
phase: z.enum(["inherit", "reset", "continue"]).optional(),
|
|
6771
7511
|
});
|
|
6772
|
-
const TRANSITION_SPEC_HONESTY = "Advanced transition spec
|
|
7512
|
+
const TRANSITION_SPEC_HONESTY = "Advanced transition spec: pattern.policy " +
|
|
6773
7513
|
"('dissolve' prob-morphs events, 'wipe' replaces them at a moving frontier, 'cells' hands " +
|
|
6774
7514
|
"whole cells over in ascending order), audio.mixLaw ('equalPower' default, 'equalGain' for " +
|
|
6775
7515
|
"correlated sections, 'correlation' auto-adapts), audio.tail ('fade' default, 'cut', 'ring' " +
|
|
6776
7516
|
"lets voices/FX decay up to tailMaxBars), phase ('reset'/'continue' overrides the song's " +
|
|
6777
7517
|
"Reset Phase per cable), window.patternCurve/mixCurve (per-domain easing; patternCurve " +
|
|
6778
|
-
"defaults linear). Runs on variation sections + queued flips, and on preset destinations " +
|
|
6779
|
-
"when the lanePresetSections setting is on (
|
|
7518
|
+
"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 " +
|
|
7519
|
+
"when the lanePresetSections setting is on (the dual-lane preset section " +
|
|
6780
7520
|
"pipeline; with it off, preset destinations coerce to a param glide). Offline bounces " +
|
|
6781
7521
|
"coerce ring→fade, blend→hard switch. 'get' reads back each cable's resolvedTransition " +
|
|
6782
7522
|
"(effective values after inheritance and capability coercion) and lastMorphResolution " +
|
|
6783
|
-
"(the engine's latest AUTO decision + reason
|
|
7523
|
+
"(the engine's latest AUTO decision + reason; published only for an AUTO request, and " +
|
|
7524
|
+
"one per song rather than one per cable).";
|
|
6784
7525
|
server.registerTool("song_mode", {
|
|
6785
7526
|
title: "Song Mode",
|
|
6786
7527
|
description: "Control PAM's song mode (timeline of variation/preset sections). Ops: 'setArrangement', " +
|
|
6787
7528
|
"'get', 'assignVx', 'setSequence', 'clear', 'toggle', 'setLoopPoints', 'toggleLoop', " +
|
|
6788
|
-
"'setCrossfadeMs', 'setPlayhead'
|
|
7529
|
+
"'setCrossfadeMs', 'setPlayhead'. WebView-routed (window must be OPEN): setArrangement, get, assignVx. " +
|
|
7530
|
+
"Headless: setSequence, clear, toggle, setLoopPoints, toggleLoop, setCrossfadeMs, setPlayhead.\n\n" +
|
|
6789
7531
|
"PREFER 'setArrangement' (PAM ≥ 1.5.7, window open): it authors real Song Mode grid nodes " +
|
|
6790
7532
|
"from a `sections` list, with an optional weighted `connections` graph. The result is " +
|
|
6791
7533
|
"visible/editable in the UI, and each section can pin a " +
|
|
@@ -6793,8 +7535,11 @@ server.registerTool("song_mode", {
|
|
|
6793
7535
|
"audio slot 1:1; see the vx_variation tool), optionally inside a specific VX scene via " +
|
|
6794
7536
|
"`vxScenePath` (the .vxpreset loads before the section plays, so sections can pull visuals " +
|
|
6795
7537
|
"from different scenes). A section can also carry `morphBars`/`morphCurve`/`morphMode` to glide the " +
|
|
6796
|
-
"incoming variation's continuous params over the bars before its boundary
|
|
6797
|
-
"advanced `transition` spec (
|
|
7538
|
+
"incoming variation's continuous params over the bars before its boundary, and an " +
|
|
7539
|
+
"advanced `transition` spec (see the field description). " +
|
|
7540
|
+
"A `transitionPreset` id (or `defaultTransitionPreset` at the top level) stamps a whole named " +
|
|
7541
|
+
"transition in one field; op 'get' lists the available ids in `transitionPresets` and reports " +
|
|
7542
|
+
"each cable's derived `transitionPreset`. " +
|
|
6798
7543
|
"'get' reads the arrangement back (nodes, compiled steps with the " +
|
|
6799
7544
|
"VX scene+slot each recalls, total bars, and each cable's `resolvedTransition` — the effective " +
|
|
6800
7545
|
"transition after inheritance and capability coercion, e.g. preset destinations report param); " +
|
|
@@ -6824,14 +7569,14 @@ server.registerTool("song_mode", {
|
|
|
6824
7569
|
repeat: z.number().int().min(1).optional().describe("Times to play before advancing (default 1)."),
|
|
6825
7570
|
muted: z.boolean().optional().describe("Pass through without an audio trigger."),
|
|
6826
7571
|
probability: z.number().min(0).max(1).optional().describe("Trigger probability (0-1)."),
|
|
6827
|
-
presetPath: z.string().optional().describe("Cross-preset section: at this section's boundary the song switches to THIS preset file, playing its variations[variationIndex] slot. With lanePresetSections on
|
|
7572
|
+
presetPath: z.string().optional().describe("Cross-preset section: at this section's boundary the song switches to THIS preset file, playing its variations[variationIndex] slot. With lanePresetSections on it transitions exactly like a variation section (prepared off the audible path, full morph family); otherwise the legacy in-place load applies."),
|
|
6828
7573
|
label: z.string().optional().describe("Section label shown on the grid node (e.g. 'Intro', 'Hook')."),
|
|
6829
7574
|
morphBars: z
|
|
6830
7575
|
.number()
|
|
6831
7576
|
.min(0)
|
|
6832
7577
|
.max(8)
|
|
6833
7578
|
.optional()
|
|
6834
|
-
.describe("Pre-anchor morph window in bars for the transition INTO this section — continuous params glide to the new values, landing at the boundary
|
|
7579
|
+
.describe("Pre-anchor morph window in bars for the transition INTO this section — continuous params glide to the new values, landing at the boundary. 0 = explicit hard switch (never morph, ignores the song's default); omit to inherit the default. Values in (0, 0.25) clamp up to 0.25."),
|
|
6835
7580
|
morphCurve: z
|
|
6836
7581
|
.enum(["linear", "ease", "exp", "log"])
|
|
6837
7582
|
.optional()
|
|
@@ -6839,8 +7584,12 @@ server.registerTool("song_mode", {
|
|
|
6839
7584
|
morphMode: z
|
|
6840
7585
|
.enum(["auto", "param", "mix", "seq", "all"])
|
|
6841
7586
|
.optional()
|
|
6842
|
-
.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
|
|
7587
|
+
.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."),
|
|
6843
7588
|
transition: transitionSpecSchema.optional().describe(TRANSITION_SPEC_HONESTY),
|
|
7589
|
+
transitionPreset: z
|
|
7590
|
+
.string()
|
|
7591
|
+
.optional()
|
|
7592
|
+
.describe("Named transition preset id — read the list from op 'get' (`transitionPresets`: the factory set plus the user's saved ones). Stamps every transition field onto the incoming cable; morphBars/morphCurve/morphMode/transition on the same object override the stamp field by field. Unknown ids fail with the known list."),
|
|
6844
7593
|
vxVariationIndex: z
|
|
6845
7594
|
.number()
|
|
6846
7595
|
.int()
|
|
@@ -6883,6 +7632,10 @@ server.registerTool("song_mode", {
|
|
|
6883
7632
|
.optional()
|
|
6884
7633
|
.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)."),
|
|
6885
7634
|
transition: transitionSpecSchema.optional().describe("Per-cable " + TRANSITION_SPEC_HONESTY),
|
|
7635
|
+
transitionPreset: z
|
|
7636
|
+
.string()
|
|
7637
|
+
.optional()
|
|
7638
|
+
.describe("Named transition preset id — read the list from op 'get' (`transitionPresets`: the factory set plus the user's saved ones). Stamps every transition field onto the incoming cable; morphBars/morphCurve/morphMode/transition on the same object override the stamp field by field. Unknown ids fail with the known list."),
|
|
6886
7639
|
}))
|
|
6887
7640
|
.optional()
|
|
6888
7641
|
.describe("For 'setArrangement': optional cable graph. Multiple cables may leave or enter a section. Each cable may carry its own morphBars/morphCurve/morphMode/transition."),
|
|
@@ -6890,6 +7643,11 @@ server.registerTool("song_mode", {
|
|
|
6890
7643
|
.nullable()
|
|
6891
7644
|
.optional()
|
|
6892
7645
|
.describe("For 'setArrangement': song-level advanced transition defaults inherited by cables without their own `transition`; null clears. " + TRANSITION_SPEC_HONESTY),
|
|
7646
|
+
defaultTransitionPreset: z
|
|
7647
|
+
.string()
|
|
7648
|
+
.nullable()
|
|
7649
|
+
.optional()
|
|
7650
|
+
.describe("For 'setArrangement': stamp a named preset as the song-level default transition (see transitionPreset)."),
|
|
6893
7651
|
loop: z.boolean().optional().describe("For 'setArrangement': loop the whole arrangement."),
|
|
6894
7652
|
nodeId: z.string().optional().describe("For 'assignVx': grid node id (from op 'get')."),
|
|
6895
7653
|
stepIndex: z.number().int().min(0).optional().describe("For 'assignVx': compiled step index (alternative to nodeId)."),
|
|
@@ -6948,29 +7706,37 @@ server.registerTool("song_mode", {
|
|
|
6948
7706
|
case "setArrangement": {
|
|
6949
7707
|
if (!args.sections || args.sections.length === 0)
|
|
6950
7708
|
return error("'setArrangement' requires sections");
|
|
7709
|
+
const requestId = mintVideoRequestId();
|
|
6951
7710
|
await sendVideoCommand({
|
|
6952
7711
|
op: "setSongArrangement",
|
|
7712
|
+
requestId,
|
|
6953
7713
|
sections: args.sections,
|
|
6954
7714
|
...(args.connections !== undefined && { connections: args.connections }),
|
|
6955
7715
|
...(args.defaultTransition !== undefined && { defaultTransition: args.defaultTransition }),
|
|
7716
|
+
...(args.defaultTransitionPreset !== undefined && { defaultTransitionPreset: args.defaultTransitionPreset }),
|
|
6956
7717
|
...(args.loop !== undefined && { loop: args.loop }),
|
|
6957
7718
|
...(args.enabled !== undefined && { enabled: args.enabled }),
|
|
6958
7719
|
...(args.crossfadeMs !== undefined && { crossfadeMs: args.crossfadeMs }),
|
|
6959
7720
|
});
|
|
6960
|
-
// The forward path is fire-and-forget — poll until the WebView's apply
|
|
6961
|
-
// republishes the snapshot (same pattern as resolveNewId).
|
|
6962
7721
|
const deadline = Date.now() + 3000;
|
|
6963
7722
|
let sm;
|
|
7723
|
+
let acknowledged = false;
|
|
6964
7724
|
for (;;) {
|
|
6965
7725
|
const state = await getVideoStateRaw();
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
7726
|
+
const ack = state.lastCommandAck;
|
|
7727
|
+
if (ack?.requestId === requestId) {
|
|
7728
|
+
if (ack.ok !== true)
|
|
7729
|
+
return error(ack.error ?? "Song arrangement was rejected.");
|
|
7730
|
+
sm = state.songMode;
|
|
7731
|
+
acknowledged = true;
|
|
7732
|
+
break;
|
|
7733
|
+
}
|
|
7734
|
+
if (Date.now() > deadline)
|
|
6971
7735
|
break;
|
|
6972
7736
|
await sleep(150);
|
|
6973
7737
|
}
|
|
7738
|
+
if (!acknowledged)
|
|
7739
|
+
return error("Song arrangement was sent but its completion was not acknowledged. Open PAM's window and use a current plugin build; inspect song_mode op:'get' before retrying.");
|
|
6974
7740
|
if (!sm || sm.available !== true)
|
|
6975
7741
|
return error("Arrangement command sent, but no song-mode read-model came back — this needs PAM ≥ 1.5.7 " +
|
|
6976
7742
|
"with the window OPEN. On older builds fall back to op 'setSequence' (headless raw steps).");
|
|
@@ -7055,11 +7821,13 @@ server.registerTool("export_song", {
|
|
|
7055
7821
|
"• 'live' with cellId — legacy headless master-mix capture into an empty cell (works with the window " +
|
|
7056
7822
|
"closed on PAM ≥ 1.5.0). The captured WAV also lands in PAM's Resamples folder.\n" +
|
|
7057
7823
|
"• 'offline' — the DETERMINISTIC multi-section bounce (PAM ≥ 1.5.7 with offline song export): every " +
|
|
7058
|
-
"variation section applies sample-accurately on its anchor
|
|
7824
|
+
"variation section applies sample-accurately on its anchor and renders faster than realtime; `bars` is still required and capped at 64. " +
|
|
7059
7825
|
"Audio-only works headless (window closed); includeVideo runs the offline video pump with per-section VX " +
|
|
7060
7826
|
"recalls (window open, WebCodecs). Caveats: preset-path steps hold the previous section's sound (pending), " +
|
|
7061
7827
|
"and wall-clock sources (Magenta, Notochord, camera layers) cannot render offline — use 'live' for those.\n\n" +
|
|
7062
7828
|
"Live paths render in REAL TIME (a 90 s song takes ~90 s) and cap at 64 bars per capture. " +
|
|
7829
|
+
"This is the entry point for arrangement bounces: strategy 'offline' issues the same render as " +
|
|
7830
|
+
"`export_audio` with mode 'song-mode', and the live and audio+video strategies exist only here. " +
|
|
7063
7831
|
"Verify audio results with get_audio_info (resolve cell captures against the Resamples location " +
|
|
7064
7832
|
"from get_default_locations).",
|
|
7065
7833
|
inputSchema: {
|
|
@@ -7068,7 +7836,7 @@ server.registerTool("export_song", {
|
|
|
7068
7836
|
.number()
|
|
7069
7837
|
.min(1)
|
|
7070
7838
|
.max(64)
|
|
7071
|
-
.describe("Total arrangement length in bars (the full timeline). Required for
|
|
7839
|
+
.describe("Total arrangement length in bars (the full timeline), 1-64. Required for every strategy."),
|
|
7072
7840
|
cellId: z
|
|
7073
7841
|
.number()
|
|
7074
7842
|
.min(1)
|
|
@@ -7082,7 +7850,10 @@ server.registerTool("export_song", {
|
|
|
7082
7850
|
name: z.string().optional().describe("Output name for WebView Song Export renders (default 'Song')."),
|
|
7083
7851
|
normalize: z.boolean().optional().describe("Normalize the captured audio (WebView Song Export paths)."),
|
|
7084
7852
|
encoderId: z.string().optional().describe("Recording encoder preset for includeVideo (e.g. mp4-h264)."),
|
|
7085
|
-
bitDepth: z
|
|
7853
|
+
bitDepth: z
|
|
7854
|
+
.union([z.literal(16), z.literal(24)])
|
|
7855
|
+
.default(24)
|
|
7856
|
+
.describe("Bit depth: 16 or 24 (legacy cell-capture + offline paths)."),
|
|
7086
7857
|
tailBars: z
|
|
7087
7858
|
.number()
|
|
7088
7859
|
.min(0)
|
|
@@ -7235,9 +8006,10 @@ server.registerTool("set_metronome", {
|
|
|
7235
8006
|
// ============================================================
|
|
7236
8007
|
server.registerTool("analyze_sample_tempo", {
|
|
7237
8008
|
title: "Analyze Sample Tempo",
|
|
7238
|
-
description: "Run BPM detection and transient/slice analysis on an audio file. " +
|
|
7239
|
-
"
|
|
7240
|
-
"
|
|
8009
|
+
description: "Run BPM detection and transient/slice analysis on an audio file. The result lands in the plugin's " +
|
|
8010
|
+
"sample-metadata cache and the editor's sample library; the bridge does not capture it, so there is no " +
|
|
8011
|
+
"get_state key that reads it back. Run it before resample_sample_to_bpm so the stretch has a source BPM, " +
|
|
8012
|
+
"and use get_audio_info for a file's own format properties.",
|
|
7241
8013
|
inputSchema: {
|
|
7242
8014
|
filePath: z.string().describe("Path to the audio file."),
|
|
7243
8015
|
transientSensitivity: z
|
|
@@ -7296,8 +8068,8 @@ server.registerTool("preview_slice", {
|
|
|
7296
8068
|
title: "Preview Slice",
|
|
7297
8069
|
description: "Audition a single slice of a cell's sample.",
|
|
7298
8070
|
inputSchema: {
|
|
7299
|
-
cellId: z.number().min(1).max(8),
|
|
7300
|
-
sliceNumber: z.number().min(0).describe("Slice index."),
|
|
8071
|
+
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
8072
|
+
sliceNumber: z.number().min(0).describe("Slice index (0-based)."),
|
|
7301
8073
|
},
|
|
7302
8074
|
}, async ({ cellId, sliceNumber }) => {
|
|
7303
8075
|
const check = await requireBridgeForCells(cellId);
|
|
@@ -7311,7 +8083,7 @@ server.registerTool("resample_sample_to_bpm", {
|
|
|
7311
8083
|
description: "Time-stretch a cell's sample to the current project BPM (or a target BPM). " +
|
|
7312
8084
|
"Non-destructive — use restore_original_sample to revert.",
|
|
7313
8085
|
inputSchema: {
|
|
7314
|
-
cellId: z.number().min(1).max(8),
|
|
8086
|
+
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
7315
8087
|
sourceBpm: z
|
|
7316
8088
|
.number()
|
|
7317
8089
|
.min(20)
|
|
@@ -7336,7 +8108,7 @@ server.registerTool("restore_original_sample", {
|
|
|
7336
8108
|
title: "Restore Original Sample",
|
|
7337
8109
|
description: "Revert a cell's sample to its un-edited source (undoes resample / trim / etc.).",
|
|
7338
8110
|
inputSchema: {
|
|
7339
|
-
cellId: z.number().min(1).max(8),
|
|
8111
|
+
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
7340
8112
|
},
|
|
7341
8113
|
}, async ({ cellId }) => {
|
|
7342
8114
|
const check = await requireBridgeForCells(cellId);
|
|
@@ -7363,8 +8135,8 @@ server.registerTool("export_slice", {
|
|
|
7363
8135
|
title: "Export Slice",
|
|
7364
8136
|
description: "Export a single slice of a cell's sample as a standalone WAV.",
|
|
7365
8137
|
inputSchema: {
|
|
7366
|
-
cellId: z.number().min(1).max(8),
|
|
7367
|
-
sliceIndex: z.number().min(0),
|
|
8138
|
+
cellId: z.number().min(1).max(8).describe("Cell number (1-8)."),
|
|
8139
|
+
sliceIndex: z.number().min(0).describe("Slice index (0-based)."),
|
|
7368
8140
|
},
|
|
7369
8141
|
}, async ({ cellId, sliceIndex }) => {
|
|
7370
8142
|
const check = await requireBridgeForCells(cellId);
|
|
@@ -7381,7 +8153,8 @@ server.registerTool("live_resample", {
|
|
|
7381
8153
|
description: "Capture PAM's audio graph output into a cell as a new sample, in real time. Ops: 'start' / 'stop'. " +
|
|
7382
8154
|
"Transport MUST be playing for the capture to produce audio. Because the capture runs live, " +
|
|
7383
8155
|
"Song Mode section changes, modulation, and variation switches are all captured faithfully — " +
|
|
7384
|
-
"this is the building block for full-track / Song Mode bounces (see export_song)
|
|
8156
|
+
"this is the building block for full-track / Song Mode bounces (see export_song). On finalize the captured clip " +
|
|
8157
|
+
"REPLACES whatever sample the destination cell held — use an empty cell, or reload the preset before saving anything afterwards.\n\n" +
|
|
7385
8158
|
"`source` selects what to record: 'cell' (the destination cell's own output), 'input' (PAM's audio input), " +
|
|
7386
8159
|
"or 'fullmix' (the whole master mix — use this to bounce the entire arrangement).",
|
|
7387
8160
|
inputSchema: {
|
|
@@ -7660,7 +8433,7 @@ server.registerTool("list_loaded_plugins", {
|
|
|
7660
8433
|
title: "List Loaded Plugins",
|
|
7661
8434
|
description: "List plugins currently hosted in PAM cells (instrument slot only — FX-side hosting is not exposed). " +
|
|
7662
8435
|
"Each entry: cellId, name, manufacturer, format, " +
|
|
7663
|
-
"path, isInstrument, bypassed, and the plugin's full param list (index, name, label, value, defaultValue). " +
|
|
8436
|
+
"path, isInstrument, bypassed, and the plugin's full param list (index, name, label, value, defaultValue, discrete, numSteps, and valueStrings for a discrete param). " +
|
|
7664
8437
|
"Use the param `index` with `set_plugin_param`.",
|
|
7665
8438
|
inputSchema: {},
|
|
7666
8439
|
}, async () => {
|
|
@@ -7702,7 +8475,8 @@ server.registerTool("load_plugin", {
|
|
|
7702
8475
|
...(args.uniqueId ? { uniqueId: args.uniqueId } : {}),
|
|
7703
8476
|
...(args.name ? { name: args.name } : {}),
|
|
7704
8477
|
}));
|
|
7705
|
-
|
|
8478
|
+
// Native answers a success with an object (`{ok: true, …}`), a failure with a string.
|
|
8479
|
+
return text(typeof raw === "string" ? raw : JSON.stringify(raw));
|
|
7706
8480
|
});
|
|
7707
8481
|
server.registerTool("unload_plugin", {
|
|
7708
8482
|
title: "Unload Plugin From Cell",
|
|
@@ -7725,10 +8499,10 @@ server.registerTool("set_plugin_param", {
|
|
|
7725
8499
|
description: "Set a parameter on a hosted plugin by cell + parameter index. Value is normalized 0..1. " +
|
|
7726
8500
|
"Use `list_loaded_plugins` to find param indices and names. " +
|
|
7727
8501
|
"This is the dedicated path for hosted plugin params — `set_parameters` won't reach them. " +
|
|
7728
|
-
"
|
|
8502
|
+
"It exists for a single instrument-slot value; use `set_plugin_params` for a batch or for the FX slot.",
|
|
7729
8503
|
inputSchema: {
|
|
7730
|
-
cellId: z.number().int().min(1),
|
|
7731
|
-
paramIndex: z.number().int().min(0),
|
|
8504
|
+
cellId: z.number().int().min(1).describe("Cell hosting the plugin."),
|
|
8505
|
+
paramIndex: z.number().int().min(0).describe("Parameter index from list_loaded_plugins."),
|
|
7732
8506
|
value: z.number().min(0).max(1).describe("Normalized 0..1."),
|
|
7733
8507
|
},
|
|
7734
8508
|
}, async (args) => {
|
|
@@ -7750,8 +8524,8 @@ server.registerTool("set_plugin_bypass", {
|
|
|
7750
8524
|
description: "Toggle bypass on the hosted plugin in a cell (instrument slot). Bypassed plugins are silenced; the cell sampler " +
|
|
7751
8525
|
"still plays in parallel.",
|
|
7752
8526
|
inputSchema: {
|
|
7753
|
-
cellId: z.number().int().min(1),
|
|
7754
|
-
bypassed: z.boolean(),
|
|
8527
|
+
cellId: z.number().int().min(1).describe("Cell hosting the plugin."),
|
|
8528
|
+
bypassed: z.boolean().describe("true silences the hosted plugin, false re-enables it."),
|
|
7755
8529
|
},
|
|
7756
8530
|
}, async (args) => {
|
|
7757
8531
|
const check = await requireBridge();
|
|
@@ -7972,7 +8746,9 @@ server.registerTool("get_sample_usage", {
|
|
|
7972
8746
|
description: "Cross-reference samples and presets. mode 'presetUsesSamples' lists the samples a preset references; " +
|
|
7973
8747
|
"mode 'presetsUsingSample' lists presets that reference a given sample (or any sample under a folder).",
|
|
7974
8748
|
inputSchema: {
|
|
7975
|
-
mode: z
|
|
8749
|
+
mode: z
|
|
8750
|
+
.enum(["presetUsesSamples", "presetsUsingSample"])
|
|
8751
|
+
.describe("Which direction to cross-reference: from a preset to its samples, or from a sample to the presets using it."),
|
|
7976
8752
|
path: z
|
|
7977
8753
|
.string()
|
|
7978
8754
|
.describe("Relative preset path (presetUsesSamples) or relative sample path (presetsUsingSample)."),
|
|
@@ -7997,7 +8773,9 @@ server.registerTool("repair_preset_samples", {
|
|
|
7997
8773
|
"library (optionally scoped to one preset). mode 'rescan' re-audits a preset for still-missing samples and " +
|
|
7998
8774
|
"reports them. Useful after moving a sample library or importing presets from another machine.",
|
|
7999
8775
|
inputSchema: {
|
|
8000
|
-
mode: z
|
|
8776
|
+
mode: z
|
|
8777
|
+
.enum(["fix", "rescan"])
|
|
8778
|
+
.describe("'fix' relinks moved samples by searching the library; 'rescan' only reports what is still missing."),
|
|
8001
8779
|
presetPath: z
|
|
8002
8780
|
.string()
|
|
8003
8781
|
.optional()
|
|
@@ -8041,7 +8819,9 @@ server.registerTool("preview_sample", {
|
|
|
8041
8819
|
server.registerTool("set_plugin_params", {
|
|
8042
8820
|
title: "Set Hosted Plugin Params (batch)",
|
|
8043
8821
|
description: "Set multiple hosted-plugin parameters in one call (more efficient than set_plugin_param per value). Values are " +
|
|
8044
|
-
"0..1 normalized. Use list_loaded_plugins to discover paramIndex values. role defaults to 'instrument'."
|
|
8822
|
+
"0..1 normalized. Use list_loaded_plugins to discover paramIndex values. role defaults to 'instrument'. " +
|
|
8823
|
+
"role:'effect' reaches the FX slot, but list_loaded_plugins reports instrument-slot plugins only, so " +
|
|
8824
|
+
"effect-slot param indices are not discoverable through MCP.",
|
|
8045
8825
|
inputSchema: {
|
|
8046
8826
|
cellId: z.number().int().min(1).describe("Cell number."),
|
|
8047
8827
|
role: z.enum(["instrument", "effect"]).optional().describe("Which hosted slot (default 'instrument')."),
|
|
@@ -8291,9 +9071,12 @@ if (existsSync(ASSISTANT_TOOLPACK_PATH))
|
|
|
8291
9071
|
playing: Boolean(transport.playing ?? transport.isPlaying),
|
|
8292
9072
|
preset,
|
|
8293
9073
|
sel: sel ?? null,
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
9074
|
+
muted: (overview.cells ?? []).filter((c) => c.muted).map((c) => c.cellId),
|
|
9075
|
+
cells: (overview.cells ?? []).flatMap((c) => {
|
|
9076
|
+
const name = c.sample?.path || c.sample?.name
|
|
9077
|
+
|| (c.synthEngine && c.synthEngine.id > 0 ? `${c.synthEngine.name}(synth)` : undefined);
|
|
9078
|
+
return name ? [{ id: c.cellId, name }] : [];
|
|
9079
|
+
}),
|
|
8297
9080
|
};
|
|
8298
9081
|
}
|
|
8299
9082
|
}
|