@williamzujkowski/live-coding-music-mcp 2.0.0 → 4.0.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 +175 -249
- package/dist/AudioAnalyzer.js +0 -1
- package/dist/PatternStore.js +0 -1
- package/dist/index.js +0 -0
- package/dist/server/resources.js +175 -0
- package/dist/server/server.js +227 -349
- package/dist/server/tools/ai.js +59 -59
- package/dist/server/tools/analysis.js +245 -82
- package/dist/server/tools/capture.js +51 -42
- package/dist/server/tools/compose.js +145 -0
- package/dist/server/tools/diagnostics.js +77 -72
- package/dist/server/tools/editor.js +100 -99
- package/dist/server/tools/generate.js +146 -175
- package/dist/server/tools/history.js +132 -124
- package/dist/server/tools/playback.js +41 -26
- package/dist/server/tools/session.js +93 -93
- package/dist/server/tools/storage.js +61 -53
- package/dist/server/tools/transform.js +193 -206
- package/dist/server/tools/types.js +66 -1
- package/dist/services/AudioCaptureService.js +4 -3
- package/dist/services/GeminiService.js +1 -1
- package/dist/services/MIDIExportService.js +2 -4
- package/dist/services/SessionManager.js +4 -2
- package/dist/services/StrudelEngine.js +24 -126
- package/dist/services/StrudelEngineHelpers.js +151 -0
- package/package.json +1 -1
- package/dist/__tests__/MusicTheory.test.js +0 -245
- package/dist/__tests__/PatternGenerator.test.js +0 -337
- package/dist/__tests__/PatternStore.test.js +0 -153
- package/dist/__tests__/utils/MockPlaywright.js +0 -130
- package/dist/server/EnhancedMCPServer.js +0 -654
- package/dist/server/EnhancedMCPServerFixed.js +0 -2506
|
@@ -1,74 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* storage domain — pattern persistence
|
|
2
|
+
* storage domain — pattern persistence.
|
|
3
3
|
*
|
|
4
|
-
* Owns (
|
|
5
|
-
* #110 audit: single `pattern_store` tool with `action` enum + params,
|
|
6
|
-
* which also resolves the `list` / `list_sessions` naming collision.
|
|
7
|
-
* For now we keep the individual verbs.
|
|
4
|
+
* Owns one consolidated tool (`pattern_store`) with save/load/list actions.
|
|
8
5
|
*/
|
|
9
6
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
description: '
|
|
14
|
-
inputSchema: {
|
|
15
|
-
type: 'object',
|
|
16
|
-
properties: {
|
|
17
|
-
name: { type: 'string', description: 'Pattern name' },
|
|
18
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
19
|
-
},
|
|
20
|
-
required: ['name'],
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: 'load',
|
|
25
|
-
description: 'Load saved pattern',
|
|
26
|
-
inputSchema: {
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
name: { type: 'string', description: 'Pattern name' },
|
|
30
|
-
},
|
|
31
|
-
required: ['name'],
|
|
32
|
-
},
|
|
7
|
+
const SESSION_ID_PROP = {
|
|
8
|
+
session_id: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Optional session ID (#108). Sources/targets the named session\'s current pattern; the on-disk catalog is shared across sessions.',
|
|
33
11
|
},
|
|
12
|
+
};
|
|
13
|
+
export const tools = [
|
|
34
14
|
{
|
|
35
|
-
name: '
|
|
36
|
-
description: '
|
|
15
|
+
name: 'pattern_store',
|
|
16
|
+
description: 'Persist patterns to disk and read them back. ' +
|
|
17
|
+
'Use action=save to write the current session pattern under a name; ' +
|
|
18
|
+
'action=load to restore a named pattern into the current session; ' +
|
|
19
|
+
'action=list to enumerate the on-disk catalog (optionally filtered by tag). ' +
|
|
20
|
+
'Example: pattern_store({ action: "save", name: "my-jam", tags: ["techno"] }). ' +
|
|
21
|
+
'For session lifecycle (create/destroy/list active sessions) use the session tool — pattern_store deals with on-disk patterns, not runtime sessions.',
|
|
37
22
|
inputSchema: {
|
|
38
23
|
type: 'object',
|
|
39
24
|
properties: {
|
|
40
|
-
|
|
25
|
+
action: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
enum: ['save', 'load', 'list'],
|
|
28
|
+
description: 'Which on-disk operation to perform',
|
|
29
|
+
},
|
|
30
|
+
name: { type: 'string', description: 'Pattern name (required for save/load)' },
|
|
31
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to attach (save only)' },
|
|
32
|
+
tag: { type: 'string', description: 'Filter by tag (list only)' },
|
|
33
|
+
...SESSION_ID_PROP,
|
|
41
34
|
},
|
|
35
|
+
required: ['action'],
|
|
42
36
|
},
|
|
43
37
|
},
|
|
44
38
|
];
|
|
45
39
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
40
|
+
async function doSave(args, ctx, sid) {
|
|
41
|
+
InputValidator.validateStringLength(args.name, 'name', 255, false);
|
|
42
|
+
const toSave = await ctx.getCurrentPatternSafe(sid);
|
|
43
|
+
if (!toSave) {
|
|
44
|
+
return 'No pattern to save';
|
|
45
|
+
}
|
|
46
|
+
await ctx.store.save(args.name, toSave, args.tags || []);
|
|
47
|
+
return `Pattern saved as "${args.name}"`;
|
|
48
|
+
}
|
|
49
|
+
async function doLoad(args, ctx, sid) {
|
|
50
|
+
InputValidator.validateStringLength(args.name, 'name', 255, false);
|
|
51
|
+
const saved = await ctx.store.load(args.name);
|
|
52
|
+
if (saved) {
|
|
53
|
+
await ctx.writePatternSafe(saved.content, sid);
|
|
54
|
+
return `Loaded pattern "${args.name}"`;
|
|
55
|
+
}
|
|
56
|
+
return `Pattern "${args.name}" not found`;
|
|
57
|
+
}
|
|
58
|
+
async function doList(args, ctx) {
|
|
59
|
+
if (args?.tag) {
|
|
60
|
+
InputValidator.validateStringLength(args.tag, 'tag', 100, false);
|
|
61
|
+
}
|
|
62
|
+
const patterns = await ctx.store.list(args?.tag);
|
|
63
|
+
return patterns.map(p => `• ${p.name} [${p.tags.join(', ')}] - ${p.timestamp}`).join('\n') || 'No patterns found';
|
|
64
|
+
}
|
|
46
65
|
export async function execute(name, args, ctx) {
|
|
66
|
+
const sid = args?.session_id;
|
|
47
67
|
switch (name) {
|
|
48
|
-
case '
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return 'No pattern to save';
|
|
68
|
+
case 'pattern_store': {
|
|
69
|
+
const action = args?.action;
|
|
70
|
+
if (!action || !['save', 'load', 'list'].includes(action)) {
|
|
71
|
+
throw new Error(`Invalid action: ${action}. Must be one of: save, load, list`);
|
|
53
72
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
InputValidator.validateStringLength(args.name, 'name', 255, false);
|
|
59
|
-
const saved = await ctx.store.load(args.name);
|
|
60
|
-
if (saved) {
|
|
61
|
-
await ctx.writePatternSafe(saved.content);
|
|
62
|
-
return `Loaded pattern "${args.name}"`;
|
|
63
|
-
}
|
|
64
|
-
return `Pattern "${args.name}" not found`;
|
|
65
|
-
}
|
|
66
|
-
case 'list': {
|
|
67
|
-
if (args?.tag) {
|
|
68
|
-
InputValidator.validateStringLength(args.tag, 'tag', 100, false);
|
|
73
|
+
switch (action) {
|
|
74
|
+
case 'save': return await doSave(args, ctx, sid);
|
|
75
|
+
case 'load': return await doLoad(args, ctx, sid);
|
|
76
|
+
case 'list': return await doList(args, ctx);
|
|
69
77
|
}
|
|
70
|
-
|
|
71
|
-
return
|
|
78
|
+
// Unreachable due to enum check above, but TypeScript wants it.
|
|
79
|
+
return undefined;
|
|
72
80
|
}
|
|
73
81
|
default:
|
|
74
82
|
throw new Error(`storage module does not handle tool: ${name}`);
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* transform domain — mutate the current pattern.
|
|
3
3
|
*
|
|
4
|
-
* Owns (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - `
|
|
10
|
-
* - `effect` (add/remove enum) — 2 tools
|
|
11
|
-
* - `shape` (mood/energy/refine enum) — 3 tools
|
|
12
|
-
* - `set_tempo` stays — high-traffic verb
|
|
13
|
-
*
|
|
14
|
-
* For now we keep the individual verbs and just move them out of server.ts.
|
|
4
|
+
* Owns (4 tools):
|
|
5
|
+
* - `transform` (op enum: transpose/reverse/stretch/quantize/humanize/
|
|
6
|
+
* swing/scale/vary)
|
|
7
|
+
* - `effect` (action enum: add/remove)
|
|
8
|
+
* - `shape` (dimension enum: mood/energy/refine)
|
|
9
|
+
* - `set_tempo` — high-traffic standalone verb
|
|
15
10
|
*/
|
|
16
11
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
17
12
|
const ENERGY_LEVELS = {
|
|
@@ -50,74 +45,62 @@ function transposePattern(pattern, semitones) {
|
|
|
50
45
|
return noteNames[newNoteValue] + newOctave;
|
|
51
46
|
});
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
description: '
|
|
57
|
-
inputSchema: {
|
|
58
|
-
type: 'object',
|
|
59
|
-
properties: { semitones: { type: 'number', description: 'Semitones to transpose' } },
|
|
60
|
-
required: ['semitones'],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'reverse',
|
|
65
|
-
description: 'Reverse pattern',
|
|
66
|
-
inputSchema: { type: 'object', properties: {} },
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'stretch',
|
|
70
|
-
description: 'Time stretch pattern',
|
|
71
|
-
inputSchema: {
|
|
72
|
-
type: 'object',
|
|
73
|
-
properties: { factor: { type: 'number', description: 'Stretch factor' } },
|
|
74
|
-
required: ['factor'],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: 'quantize',
|
|
79
|
-
description: 'Quantize to grid',
|
|
80
|
-
inputSchema: {
|
|
81
|
-
type: 'object',
|
|
82
|
-
properties: { grid: { type: 'string', description: 'Grid size (e.g., "1/16")' } },
|
|
83
|
-
required: ['grid'],
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: 'humanize',
|
|
88
|
-
description: 'Add human timing variation',
|
|
89
|
-
inputSchema: {
|
|
90
|
-
type: 'object',
|
|
91
|
-
properties: { amount: { type: 'number', description: 'Humanization amount (0-1)' } },
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: 'generate_variation',
|
|
96
|
-
description: 'Create pattern variations',
|
|
97
|
-
inputSchema: {
|
|
98
|
-
type: 'object',
|
|
99
|
-
properties: { type: { type: 'string', description: 'Variation type (subtle/moderate/extreme/glitch/evolving)' } },
|
|
100
|
-
},
|
|
48
|
+
const SESSION_ID_PROP = {
|
|
49
|
+
session_id: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Optional session ID (#108). Omit to use default session.',
|
|
101
52
|
},
|
|
53
|
+
};
|
|
54
|
+
export const tools = [
|
|
102
55
|
{
|
|
103
|
-
name: '
|
|
104
|
-
description: '
|
|
56
|
+
name: 'transform',
|
|
57
|
+
description: 'Apply a single transform op to the current session pattern. ' +
|
|
58
|
+
'op=transpose shifts notes by `semitones`. ' +
|
|
59
|
+
'op=reverse appends `.rev` to the pattern. ' +
|
|
60
|
+
'op=stretch slows by `factor` (>1 slower, <1 faster). ' +
|
|
61
|
+
'op=quantize snaps to the `grid` (e.g. "1/16"). ' +
|
|
62
|
+
'op=humanize adds rand-nudge timing of `amount` (0-1). ' +
|
|
63
|
+
'op=swing applies `.swing(amount)`. ' +
|
|
64
|
+
'op=scale applies a `root`/`scale` filter to notes. ' +
|
|
65
|
+
'op=vary returns a variation of `type` (subtle/moderate/extreme/glitch/evolving). ' +
|
|
66
|
+
'Example: transform({ op: "transpose", semitones: 7 }). ' +
|
|
67
|
+
'For effects (add/remove) use effect; for mood/energy/refine use shape; for tempo use set_tempo.',
|
|
105
68
|
inputSchema: {
|
|
106
69
|
type: 'object',
|
|
107
70
|
properties: {
|
|
108
|
-
|
|
109
|
-
|
|
71
|
+
op: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
enum: ['transpose', 'reverse', 'stretch', 'quantize', 'humanize', 'swing', 'scale', 'vary'],
|
|
74
|
+
description: 'Which transform to apply',
|
|
75
|
+
},
|
|
76
|
+
semitones: { type: 'number', description: 'op=transpose: integer semitones to shift' },
|
|
77
|
+
factor: { type: 'number', description: 'op=stretch: stretch factor' },
|
|
78
|
+
grid: { type: 'string', description: 'op=quantize: grid size (e.g. "1/16")' },
|
|
79
|
+
amount: { type: 'number', description: 'op=humanize/swing: amount 0-1' },
|
|
80
|
+
root: { type: 'string', description: 'op=scale: root note (e.g. "C")' },
|
|
81
|
+
scale: { type: 'string', description: 'op=scale: scale name (e.g. "minor")' },
|
|
82
|
+
type: { type: 'string', description: 'op=vary: variation type (subtle/moderate/extreme/glitch/evolving)' },
|
|
83
|
+
...SESSION_ID_PROP,
|
|
110
84
|
},
|
|
111
|
-
required: ['
|
|
85
|
+
required: ['op'],
|
|
112
86
|
},
|
|
113
87
|
},
|
|
114
88
|
{
|
|
115
|
-
name: '
|
|
116
|
-
description: '
|
|
89
|
+
name: 'effect',
|
|
90
|
+
description: 'Add or remove a Strudel effect on the current session pattern. ' +
|
|
91
|
+
'action=add appends `.<effect>(<params>)`. ' +
|
|
92
|
+
'action=remove strips the last `.<effect>(...)` call from the pattern. ' +
|
|
93
|
+
'Example: effect({ action: "add", effect: "lpf", params: "1000" }). ' +
|
|
94
|
+
'For higher-level effect bundles (mood/energy/refine) use shape; for raw transforms use transform.',
|
|
117
95
|
inputSchema: {
|
|
118
96
|
type: 'object',
|
|
119
|
-
properties: {
|
|
120
|
-
|
|
97
|
+
properties: {
|
|
98
|
+
action: { type: 'string', enum: ['add', 'remove'], description: 'add or remove the effect' },
|
|
99
|
+
effect: { type: 'string', description: 'Effect name (e.g. lpf, room, delay)' },
|
|
100
|
+
params: { type: 'string', description: 'Effect parameters (action=add only)' },
|
|
101
|
+
...SESSION_ID_PROP,
|
|
102
|
+
},
|
|
103
|
+
required: ['action', 'effect'],
|
|
121
104
|
},
|
|
122
105
|
},
|
|
123
106
|
{
|
|
@@ -125,160 +108,164 @@ export const tools = [
|
|
|
125
108
|
description: 'Set BPM',
|
|
126
109
|
inputSchema: {
|
|
127
110
|
type: 'object',
|
|
128
|
-
properties: { bpm: { type: 'number', description: 'Tempo in BPM' } },
|
|
111
|
+
properties: { bpm: { type: 'number', description: 'Tempo in BPM' }, ...SESSION_ID_PROP },
|
|
129
112
|
required: ['bpm'],
|
|
130
113
|
},
|
|
131
114
|
},
|
|
132
115
|
{
|
|
133
|
-
name: '
|
|
134
|
-
description: '
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
name: 'apply_scale',
|
|
143
|
-
description: 'Apply scale to notes',
|
|
144
|
-
inputSchema: {
|
|
145
|
-
type: 'object',
|
|
146
|
-
properties: {
|
|
147
|
-
scale: { type: 'string', description: 'Scale name' },
|
|
148
|
-
root: { type: 'string', description: 'Root note' },
|
|
149
|
-
},
|
|
150
|
-
required: ['scale', 'root'],
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: 'shift_mood',
|
|
155
|
-
description: 'Transform current pattern to match a different emotional mood by adjusting tempo, effects, and note choices. Moods: dark, euphoric, melancholic, aggressive, dreamy, peaceful, energetic.',
|
|
116
|
+
name: 'shape',
|
|
117
|
+
description: 'Shape the current pattern along one of three high-level dimensions. ' +
|
|
118
|
+
'dimension=mood applies a mood profile (dark/euphoric/melancholic/aggressive/dreamy/peaceful/energetic) with optional intensity 0-1. ' +
|
|
119
|
+
'dimension=energy applies an energy level (integer 0-10). ' +
|
|
120
|
+
'dimension=refine applies a directional refinement: faster/slower/louder/quieter/brighter/darker/"more reverb"/drier. ' +
|
|
121
|
+
'All three auto-play by default. ' +
|
|
122
|
+
'Example: shape({ dimension: "mood", target_mood: "dark", intensity: 0.8 }). ' +
|
|
123
|
+
'For raw transform ops use transform; for explicit effects use effect.',
|
|
156
124
|
inputSchema: {
|
|
157
125
|
type: 'object',
|
|
158
126
|
properties: {
|
|
127
|
+
dimension: { type: 'string', enum: ['mood', 'energy', 'refine'], description: 'Which dimension to shape along' },
|
|
159
128
|
target_mood: {
|
|
160
129
|
type: 'string',
|
|
161
130
|
enum: ['dark', 'euphoric', 'melancholic', 'aggressive', 'dreamy', 'peaceful', 'energetic'],
|
|
162
|
-
description: '
|
|
131
|
+
description: 'dimension=mood: target mood',
|
|
163
132
|
},
|
|
164
|
-
intensity: { type: 'number', minimum: 0, maximum: 1, description: '
|
|
165
|
-
|
|
133
|
+
intensity: { type: 'number', minimum: 0, maximum: 1, description: 'dimension=mood: intensity 0-1 (default 0.5)' },
|
|
134
|
+
level: { type: 'number', description: 'dimension=energy: integer level 0-10' },
|
|
135
|
+
direction: { type: 'string', description: 'dimension=refine: faster, slower, louder, quieter, brighter, darker, "more reverb", drier' },
|
|
136
|
+
auto_play: { type: 'boolean', description: 'Start playback after shape (default true)' },
|
|
137
|
+
...SESSION_ID_PROP,
|
|
166
138
|
},
|
|
167
|
-
required: ['
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: 'set_energy',
|
|
172
|
-
description: 'Adjust the overall energy level of the current pattern on a 0-10 scale. 0: minimal/ambient, 1-2: sparse, 3-4: light/relaxed, 5-6: normal/moderate, 7-8: driving/intense, 9-10: maximum. Auto-plays after applying energy level.',
|
|
173
|
-
inputSchema: {
|
|
174
|
-
type: 'object',
|
|
175
|
-
properties: { level: { type: 'number', description: 'Energy level from 0 (minimal) to 10 (maximum)' } },
|
|
176
|
-
required: ['level'],
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
name: 'refine',
|
|
181
|
-
description: 'Incrementally refine the current pattern with simple directional commands. Supports: faster/slower (tempo), louder/quieter (gain), brighter/darker (filter cutoff), "more reverb"/drier (reverb). Auto-plays after applying refinement.',
|
|
182
|
-
inputSchema: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
properties: { direction: { type: 'string', description: 'Refinement direction: faster, slower, louder, quieter, brighter, darker, "more reverb", or drier' } },
|
|
185
|
-
required: ['direction'],
|
|
139
|
+
required: ['dimension'],
|
|
186
140
|
},
|
|
187
141
|
},
|
|
188
142
|
];
|
|
189
143
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
144
|
+
// ---- per-op helpers (shared by transform({op}) and the deprecated aliases)
|
|
145
|
+
async function opTranspose(args, ctx, sid) {
|
|
146
|
+
if (typeof args.semitones !== 'number' || !Number.isInteger(args.semitones)) {
|
|
147
|
+
throw new Error('Semitones must be an integer');
|
|
148
|
+
}
|
|
149
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
150
|
+
await ctx.writePatternSafe(transposePattern(p, args.semitones), sid);
|
|
151
|
+
return `Transposed ${args.semitones} semitones`;
|
|
152
|
+
}
|
|
153
|
+
async function opReverse(ctx, sid) {
|
|
154
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
155
|
+
await ctx.writePatternSafe(p + '.rev', sid);
|
|
156
|
+
return 'Pattern reversed';
|
|
157
|
+
}
|
|
158
|
+
async function opStretch(args, ctx, sid) {
|
|
159
|
+
InputValidator.validateGain(args.factor);
|
|
160
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
161
|
+
await ctx.writePatternSafe(p + `.slow(${args.factor})`, sid);
|
|
162
|
+
return `Stretched by factor of ${args.factor}`;
|
|
163
|
+
}
|
|
164
|
+
async function opQuantize(args, ctx, sid) {
|
|
165
|
+
InputValidator.validateStringLength(args.grid, 'grid', 50, false);
|
|
166
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
167
|
+
await ctx.writePatternSafe(p + `.struct("${args.grid}")`, sid);
|
|
168
|
+
return `Quantized to ${args.grid} grid`;
|
|
169
|
+
}
|
|
170
|
+
async function opHumanize(args, ctx, sid) {
|
|
171
|
+
if (args.amount !== undefined)
|
|
172
|
+
InputValidator.validateNormalizedValue(args.amount, 'amount');
|
|
173
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
174
|
+
const amt = args.amount || 0.01;
|
|
175
|
+
await ctx.writePatternSafe(p + `.nudge(rand.range(-${amt}, ${amt}))`, sid);
|
|
176
|
+
return 'Added human timing';
|
|
177
|
+
}
|
|
178
|
+
async function opVary(args, ctx, sid) {
|
|
179
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
180
|
+
const varied = ctx.generator.generateVariation(p, args.type || 'subtle');
|
|
181
|
+
await ctx.writePatternSafe(varied, sid);
|
|
182
|
+
return `Added ${args.type || 'subtle'} variation`;
|
|
183
|
+
}
|
|
184
|
+
async function opSwing(args, ctx, sid) {
|
|
185
|
+
InputValidator.validateNormalizedValue(args.amount, 'amount');
|
|
186
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
187
|
+
await ctx.writePatternSafe(p + `.swing(${args.amount})`, sid);
|
|
188
|
+
return `Added swing: ${args.amount}`;
|
|
189
|
+
}
|
|
190
|
+
async function opScale(args, ctx, sid) {
|
|
191
|
+
InputValidator.validateStringLength(args.scale, 'scale', 50, false);
|
|
192
|
+
InputValidator.validateRootNote(args.root);
|
|
193
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
194
|
+
await ctx.writePatternSafe(p + `.scale("${args.root}:${args.scale}")`, sid);
|
|
195
|
+
return `Applied ${args.root} ${args.scale} scale`;
|
|
196
|
+
}
|
|
197
|
+
async function opEffectAdd(args, ctx, sid) {
|
|
198
|
+
InputValidator.validateStringLength(args.effect, 'effect', 100, false);
|
|
199
|
+
if (args.params)
|
|
200
|
+
InputValidator.validateStringLength(args.params, 'params', 1000, true);
|
|
201
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
202
|
+
const withEffect = args.params
|
|
203
|
+
? p + `.${args.effect}(${args.params})`
|
|
204
|
+
: p + `.${args.effect}()`;
|
|
205
|
+
await ctx.writePatternSafe(withEffect, sid);
|
|
206
|
+
return `Added ${args.effect} effect`;
|
|
207
|
+
}
|
|
208
|
+
async function opEffectRemove(args, ctx, sid) {
|
|
209
|
+
InputValidator.validateStringLength(args.effect, 'effect', 100, false);
|
|
210
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
211
|
+
const regex = new RegExp(`\\.${args.effect}\\([^)]*\\)`, 'g');
|
|
212
|
+
const stripped = p.replace(regex, '');
|
|
213
|
+
if (stripped === p)
|
|
214
|
+
return `No ${args.effect} effect found to remove`;
|
|
215
|
+
await ctx.writePatternSafe(stripped, sid);
|
|
216
|
+
return `Removed ${args.effect} effect`;
|
|
217
|
+
}
|
|
190
218
|
export async function execute(name, args, ctx) {
|
|
219
|
+
const sid = args?.session_id;
|
|
220
|
+
if (!sid && !ctx.isInitialized()) {
|
|
221
|
+
return 'Browser not initialized. Run init first.';
|
|
222
|
+
}
|
|
191
223
|
switch (name) {
|
|
192
|
-
case '
|
|
193
|
-
|
|
194
|
-
|
|
224
|
+
case 'transform': {
|
|
225
|
+
const op = args?.op;
|
|
226
|
+
switch (op) {
|
|
227
|
+
case 'transpose': return await opTranspose(args, ctx, sid);
|
|
228
|
+
case 'reverse': return await opReverse(ctx, sid);
|
|
229
|
+
case 'stretch': return await opStretch(args, ctx, sid);
|
|
230
|
+
case 'quantize': return await opQuantize(args, ctx, sid);
|
|
231
|
+
case 'humanize': return await opHumanize(args, ctx, sid);
|
|
232
|
+
case 'swing': return await opSwing(args, ctx, sid);
|
|
233
|
+
case 'scale': return await opScale(args, ctx, sid);
|
|
234
|
+
case 'vary': return await opVary(args, ctx, sid);
|
|
235
|
+
default:
|
|
236
|
+
throw new Error(`Invalid op: ${op}. Must be one of: transpose, reverse, stretch, quantize, humanize, swing, scale, vary`);
|
|
195
237
|
}
|
|
196
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
197
|
-
await ctx.writePatternSafe(transposePattern(p, args.semitones));
|
|
198
|
-
return `Transposed ${args.semitones} semitones`;
|
|
199
238
|
}
|
|
200
|
-
case '
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
await ctx.writePatternSafe(p + `.slow(${args.factor})`);
|
|
209
|
-
return `Stretched by factor of ${args.factor}`;
|
|
210
|
-
}
|
|
211
|
-
case 'quantize': {
|
|
212
|
-
InputValidator.validateStringLength(args.grid, 'grid', 50, false);
|
|
213
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
214
|
-
await ctx.writePatternSafe(p + `.struct("${args.grid}")`);
|
|
215
|
-
return `Quantized to ${args.grid} grid`;
|
|
216
|
-
}
|
|
217
|
-
case 'humanize': {
|
|
218
|
-
if (args.amount !== undefined)
|
|
219
|
-
InputValidator.validateNormalizedValue(args.amount, 'amount');
|
|
220
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
221
|
-
const amt = args.amount || 0.01;
|
|
222
|
-
await ctx.writePatternSafe(p + `.nudge(rand.range(-${amt}, ${amt}))`);
|
|
223
|
-
return 'Added human timing';
|
|
224
|
-
}
|
|
225
|
-
case 'generate_variation': {
|
|
226
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
227
|
-
const varied = ctx.generator.generateVariation(p, args.type || 'subtle');
|
|
228
|
-
await ctx.writePatternSafe(varied);
|
|
229
|
-
return `Added ${args.type || 'subtle'} variation`;
|
|
230
|
-
}
|
|
231
|
-
case 'add_effect': {
|
|
232
|
-
InputValidator.validateStringLength(args.effect, 'effect', 100, false);
|
|
233
|
-
if (args.params)
|
|
234
|
-
InputValidator.validateStringLength(args.params, 'params', 1000, true);
|
|
235
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
236
|
-
const withEffect = args.params
|
|
237
|
-
? p + `.${args.effect}(${args.params})`
|
|
238
|
-
: p + `.${args.effect}()`;
|
|
239
|
-
await ctx.writePatternSafe(withEffect);
|
|
240
|
-
return `Added ${args.effect} effect`;
|
|
241
|
-
}
|
|
242
|
-
case 'remove_effect': {
|
|
243
|
-
InputValidator.validateStringLength(args.effect, 'effect', 100, false);
|
|
244
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
245
|
-
const regex = new RegExp(`\\.${args.effect}\\([^)]*\\)`, 'g');
|
|
246
|
-
const stripped = p.replace(regex, '');
|
|
247
|
-
if (stripped === p)
|
|
248
|
-
return `No ${args.effect} effect found to remove`;
|
|
249
|
-
await ctx.writePatternSafe(stripped);
|
|
250
|
-
return `Removed ${args.effect} effect`;
|
|
239
|
+
case 'effect': {
|
|
240
|
+
const a = args?.action;
|
|
241
|
+
if (a !== 'add' && a !== 'remove') {
|
|
242
|
+
throw new Error(`Invalid action: ${a}. Must be one of: add, remove`);
|
|
243
|
+
}
|
|
244
|
+
return a === 'add'
|
|
245
|
+
? await opEffectAdd(args, ctx, sid)
|
|
246
|
+
: await opEffectRemove(args, ctx, sid);
|
|
251
247
|
}
|
|
252
248
|
case 'set_tempo': {
|
|
253
249
|
InputValidator.validateBPM(args.bpm);
|
|
254
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
255
|
-
await ctx.writePatternSafe(`setcpm(${args.bpm})\n${p}
|
|
250
|
+
const p = await ctx.getCurrentPatternSafe(sid);
|
|
251
|
+
await ctx.writePatternSafe(`setcpm(${args.bpm})\n${p}`, sid);
|
|
256
252
|
return `Set tempo to ${args.bpm} BPM`;
|
|
257
253
|
}
|
|
258
|
-
case '
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const p = await ctx.getCurrentPatternSafe();
|
|
268
|
-
await ctx.writePatternSafe(p + `.scale("${args.root}:${args.scale}")`);
|
|
269
|
-
return `Applied ${args.root} ${args.scale} scale`;
|
|
254
|
+
case 'shape': {
|
|
255
|
+
const dim = args?.dimension;
|
|
256
|
+
switch (dim) {
|
|
257
|
+
case 'mood': return await shiftMood(args, ctx, sid);
|
|
258
|
+
case 'energy': return await setEnergyLevel(args.level, ctx, sid);
|
|
259
|
+
case 'refine': return await refinePattern(args.direction, ctx, sid);
|
|
260
|
+
default:
|
|
261
|
+
throw new Error(`Invalid dimension: ${dim}. Must be one of: mood, energy, refine`);
|
|
262
|
+
}
|
|
270
263
|
}
|
|
271
|
-
case 'shift_mood':
|
|
272
|
-
return await shiftMood(args, ctx);
|
|
273
|
-
case 'set_energy':
|
|
274
|
-
return await setEnergyLevel(args.level, ctx);
|
|
275
|
-
case 'refine':
|
|
276
|
-
return await refinePattern(args.direction, ctx);
|
|
277
264
|
default:
|
|
278
265
|
throw new Error(`transform module does not handle tool: ${name}`);
|
|
279
266
|
}
|
|
280
267
|
}
|
|
281
|
-
async function shiftMood(args, ctx) {
|
|
268
|
+
async function shiftMood(args, ctx, sid) {
|
|
282
269
|
const mood = args.target_mood?.toLowerCase()?.trim();
|
|
283
270
|
const profile = MOOD_PROFILES[mood];
|
|
284
271
|
if (!profile) {
|
|
@@ -287,7 +274,7 @@ async function shiftMood(args, ctx) {
|
|
|
287
274
|
error: `Unknown mood: ${args.target_mood}. Valid moods: ${Object.keys(MOOD_PROFILES).join(', ')}.`,
|
|
288
275
|
};
|
|
289
276
|
}
|
|
290
|
-
const pattern = await ctx.getCurrentPatternSafe();
|
|
277
|
+
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
291
278
|
if (!pattern || pattern.trim().length === 0) {
|
|
292
279
|
return { success: false, error: 'No pattern to transform. Write a pattern first.' };
|
|
293
280
|
}
|
|
@@ -329,17 +316,17 @@ async function shiftMood(args, ctx) {
|
|
|
329
316
|
result += `.gain(${g.toFixed(2)})`;
|
|
330
317
|
applied.push(`gain ${profile.gainMod > 0 ? '+' : ''}${Math.round(profile.gainMod * 100 * intensity)}%`);
|
|
331
318
|
}
|
|
332
|
-
await ctx.writePatternSafe(result);
|
|
333
|
-
if (args.auto_play !== false && ctx.isInitialized()) {
|
|
334
|
-
await ctx.
|
|
319
|
+
await ctx.writePatternSafe(result, sid);
|
|
320
|
+
if (args.auto_play !== false && (sid || ctx.isInitialized())) {
|
|
321
|
+
await ctx.getController(sid).play();
|
|
335
322
|
}
|
|
336
323
|
return { success: true, target_mood: mood, intensity, applied_effects: applied };
|
|
337
324
|
}
|
|
338
|
-
async function setEnergyLevel(level, ctx) {
|
|
325
|
+
async function setEnergyLevel(level, ctx, sid) {
|
|
339
326
|
if (level < 0 || level > 10 || !Number.isInteger(level)) {
|
|
340
327
|
return { success: false, error: 'Energy level must be an integer from 0 to 10.' };
|
|
341
328
|
}
|
|
342
|
-
const pattern = await ctx.getCurrentPatternSafe();
|
|
329
|
+
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
343
330
|
if (!pattern || pattern.trim().length === 0) {
|
|
344
331
|
return { success: false, error: 'No pattern to adjust. Write a pattern first.' };
|
|
345
332
|
}
|
|
@@ -348,13 +335,13 @@ async function setEnergyLevel(level, ctx) {
|
|
|
348
335
|
if (config.densityAdjust)
|
|
349
336
|
result += config.densityAdjust;
|
|
350
337
|
result += `.room(${config.roomAmount})`;
|
|
351
|
-
await ctx.writePatternSafe(result);
|
|
352
|
-
if (ctx.isInitialized())
|
|
353
|
-
await ctx.
|
|
338
|
+
await ctx.writePatternSafe(result, sid);
|
|
339
|
+
if (sid || ctx.isInitialized())
|
|
340
|
+
await ctx.getController(sid).play();
|
|
354
341
|
return { success: true, level, description: config.description };
|
|
355
342
|
}
|
|
356
|
-
async function refinePattern(direction, ctx) {
|
|
357
|
-
const pattern = await ctx.getCurrentPatternSafe();
|
|
343
|
+
async function refinePattern(direction, ctx, sid) {
|
|
344
|
+
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
358
345
|
if (!pattern || pattern.trim().length === 0) {
|
|
359
346
|
return { success: false, error: 'No pattern to refine. Write a pattern first.' };
|
|
360
347
|
}
|
|
@@ -391,9 +378,9 @@ async function refinePattern(direction, ctx) {
|
|
|
391
378
|
error: `Unknown refinement direction: ${direction}. Supported: faster, slower, louder, quieter, brighter, darker, "more reverb", drier.`,
|
|
392
379
|
};
|
|
393
380
|
}
|
|
394
|
-
await ctx.writePatternSafe(pattern + modification);
|
|
395
|
-
if (ctx.isInitialized())
|
|
396
|
-
await ctx.
|
|
381
|
+
await ctx.writePatternSafe(pattern + modification, sid);
|
|
382
|
+
if (sid || ctx.isInitialized())
|
|
383
|
+
await ctx.getController(sid).play();
|
|
397
384
|
return { success: true, direction: dir, applied: modification };
|
|
398
385
|
}
|
|
399
386
|
export const transformModule = { tools, toolNames, execute };
|