@williamzujkowski/live-coding-music-mcp 3.0.0 → 4.1.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 +212 -780
- package/dist/AudioAnalyzer.js +1294 -91
- package/dist/PatternStore.js +276 -23
- package/dist/StrudelController.js +388 -56
- package/dist/server/resources.js +27 -20
- package/dist/server/server.js +391 -83
- package/dist/server/tools/ai.js +129 -152
- package/dist/server/tools/analysis.js +170 -91
- package/dist/server/tools/capture.js +128 -55
- package/dist/server/tools/compose.js +28 -42
- package/dist/server/tools/diagnostics.js +23 -53
- package/dist/server/tools/editor.js +63 -72
- package/dist/server/tools/generate.js +45 -167
- package/dist/server/tools/history.js +35 -66
- package/dist/server/tools/playback.js +7 -22
- package/dist/server/tools/session.js +41 -47
- package/dist/server/tools/storage.js +173 -50
- package/dist/server/tools/transform.js +136 -199
- package/dist/server/tools/types.js +209 -14
- package/dist/services/AudioCaptureService.js +302 -33
- package/dist/services/AudioExportService.js +318 -0
- package/dist/services/BrowserOnlyFunctions.js +149 -0
- package/dist/services/EventDensityProbe.js +324 -0
- package/dist/services/GeminiService.js +221 -36
- package/dist/services/IsolatedEngineRunner.js +308 -0
- package/dist/services/IsolatedStrudelEngine.js +135 -0
- package/dist/services/LocalPatternEngine.js +16 -0
- package/dist/services/MIDIExportService.js +765 -71
- package/dist/services/MIDIImportService.js +474 -0
- package/dist/services/MusicTheory.js +85 -13
- package/dist/services/PatternGenerator.js +168 -85
- package/dist/services/PatternSandbox.js +359 -0
- package/dist/services/SessionManager.js +338 -65
- package/dist/services/StrudelEngine.js +218 -12
- package/dist/services/StrudelEngineHelpers.js +24 -8
- package/dist/services/StyleRegistry.js +204 -0
- package/dist/services/Transferable.js +114 -0
- package/dist/services/ai/AiTransport.js +56 -0
- package/dist/services/ai/AudioMeasurements.js +155 -0
- package/dist/services/ai/CliTransport.js +256 -0
- package/dist/services/engineChild.js +83 -0
- package/dist/services/engineChildPath.js +28 -0
- package/dist/utils/CategorisedError.js +59 -0
- package/dist/utils/ErrorRecovery.js +130 -110
- package/dist/utils/InputValidator.js +55 -7
- package/dist/utils/PatternValidator.js +136 -17
- package/dist/utils/SafePath.js +112 -0
- package/dist/utils/ServerConfig.js +248 -0
- package/dist/utils/TableLookup.js +41 -0
- package/dist/utils/Tempo.js +143 -0
- package/package.json +17 -5
- package/dist/services/__mocks__/StrudelEngine.js +0 -157
- package/dist/types/index.js +0 -1
|
@@ -1,19 +1,17 @@
|
|
|
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
|
*/
|
|
11
|
+
import { empty, err, withStashField, withStashNotice } from './types.js';
|
|
16
12
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
13
|
+
import { BEATS_PER_CYCLE } from '../../utils/Tempo.js';
|
|
14
|
+
import { lookup } from '../../utils/TableLookup.js';
|
|
17
15
|
const ENERGY_LEVELS = {
|
|
18
16
|
0: { tempoAdjust: -20, roomAmount: 0.5, densityAdjust: '.slow(4)', description: 'minimal/ambient' },
|
|
19
17
|
1: { tempoAdjust: -15, roomAmount: 0.4, densityAdjust: '.slow(3)', description: 'very sparse' },
|
|
@@ -43,7 +41,7 @@ function transposePattern(pattern, semitones) {
|
|
|
43
41
|
'f#': 6, 'g': 7, 'g#': 8, 'a': 9, 'a#': 10, 'b': 11,
|
|
44
42
|
};
|
|
45
43
|
const currentNote = note.toLowerCase();
|
|
46
|
-
const noteValue = noteMap
|
|
44
|
+
const noteValue = lookup(noteMap, currentNote, 0);
|
|
47
45
|
const newNoteValue = (noteValue + semitones + 12) % 12;
|
|
48
46
|
const noteNames = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b'];
|
|
49
47
|
const newOctave = parseInt(octave) + Math.floor((noteValue + semitones) / 12);
|
|
@@ -65,9 +63,9 @@ export const tools = [
|
|
|
65
63
|
'op=stretch slows by `factor` (>1 slower, <1 faster). ' +
|
|
66
64
|
'op=quantize snaps to the `grid` (e.g. "1/16"). ' +
|
|
67
65
|
'op=humanize adds rand-nudge timing of `amount` (0-1). ' +
|
|
68
|
-
'op=swing applies `.swing(amount)
|
|
69
|
-
'op=scale applies a `root`/`scale` filter to notes
|
|
70
|
-
'op=vary returns a variation of `type` (subtle/moderate/extreme/glitch/evolving
|
|
66
|
+
'op=swing applies `.swing(amount)`. ' +
|
|
67
|
+
'op=scale applies a `root`/`scale` filter to notes. ' +
|
|
68
|
+
'op=vary returns a variation of `type` (subtle/moderate/extreme/glitch/evolving). ' +
|
|
71
69
|
'Example: transform({ op: "transpose", semitones: 7 }). ' +
|
|
72
70
|
'For effects (add/remove) use effect; for mood/energy/refine use shape; for tempo use set_tempo.',
|
|
73
71
|
inputSchema: {
|
|
@@ -90,59 +88,11 @@ export const tools = [
|
|
|
90
88
|
required: ['op'],
|
|
91
89
|
},
|
|
92
90
|
},
|
|
93
|
-
{
|
|
94
|
-
name: 'transpose',
|
|
95
|
-
description: '[DEPRECATED — use transform({ op: "transpose" }) instead] Transpose notes by semitones',
|
|
96
|
-
inputSchema: {
|
|
97
|
-
type: 'object',
|
|
98
|
-
properties: { semitones: { type: 'number', description: 'Semitones to transpose' }, ...SESSION_ID_PROP },
|
|
99
|
-
required: ['semitones'],
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'reverse',
|
|
104
|
-
description: '[DEPRECATED — use transform({ op: "reverse" }) instead] Reverse pattern',
|
|
105
|
-
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: 'stretch',
|
|
109
|
-
description: '[DEPRECATED — use transform({ op: "stretch" }) instead] Time stretch pattern',
|
|
110
|
-
inputSchema: {
|
|
111
|
-
type: 'object',
|
|
112
|
-
properties: { factor: { type: 'number', description: 'Stretch factor' }, ...SESSION_ID_PROP },
|
|
113
|
-
required: ['factor'],
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: 'quantize',
|
|
118
|
-
description: '[DEPRECATED — use transform({ op: "quantize" }) instead] Quantize to grid',
|
|
119
|
-
inputSchema: {
|
|
120
|
-
type: 'object',
|
|
121
|
-
properties: { grid: { type: 'string', description: 'Grid size (e.g., "1/16")' }, ...SESSION_ID_PROP },
|
|
122
|
-
required: ['grid'],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: 'humanize',
|
|
127
|
-
description: '[DEPRECATED — use transform({ op: "humanize" }) instead] Add human timing variation',
|
|
128
|
-
inputSchema: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: { amount: { type: 'number', description: 'Humanization amount (0-1)' }, ...SESSION_ID_PROP },
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: 'generate_variation',
|
|
135
|
-
description: '[DEPRECATED — use transform({ op: "vary" }) instead] Create pattern variations (mis-named today; it transforms, not generates)',
|
|
136
|
-
inputSchema: {
|
|
137
|
-
type: 'object',
|
|
138
|
-
properties: { type: { type: 'string', description: 'Variation type (subtle/moderate/extreme/glitch/evolving)' }, ...SESSION_ID_PROP },
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
91
|
{
|
|
142
92
|
name: 'effect',
|
|
143
93
|
description: 'Add or remove a Strudel effect on the current session pattern. ' +
|
|
144
94
|
'action=add appends `.<effect>(<params>)`. ' +
|
|
145
|
-
'action=remove strips
|
|
95
|
+
'action=remove strips EVERY `.<effect>(...)` call from the pattern, not just the last. ' +
|
|
146
96
|
'Example: effect({ action: "add", effect: "lpf", params: "1000" }). ' +
|
|
147
97
|
'For higher-level effect bundles (mood/energy/refine) use shape; for raw transforms use transform.',
|
|
148
98
|
inputSchema: {
|
|
@@ -156,59 +106,15 @@ export const tools = [
|
|
|
156
106
|
required: ['action', 'effect'],
|
|
157
107
|
},
|
|
158
108
|
},
|
|
159
|
-
{
|
|
160
|
-
name: 'add_effect',
|
|
161
|
-
description: '[DEPRECATED — use effect({ action: "add" }) instead] Add effect to pattern',
|
|
162
|
-
inputSchema: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: {
|
|
165
|
-
effect: { type: 'string', description: 'Effect name' },
|
|
166
|
-
params: { type: 'string', description: 'Effect parameters' },
|
|
167
|
-
...SESSION_ID_PROP,
|
|
168
|
-
},
|
|
169
|
-
required: ['effect'],
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: 'remove_effect',
|
|
174
|
-
description: '[DEPRECATED — use effect({ action: "remove" }) instead] Remove effect',
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: 'object',
|
|
177
|
-
properties: { effect: { type: 'string', description: 'Effect to remove' }, ...SESSION_ID_PROP },
|
|
178
|
-
required: ['effect'],
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
109
|
{
|
|
182
110
|
name: 'set_tempo',
|
|
183
|
-
description: 'Set BPM',
|
|
111
|
+
description: 'Set BPM. Writes setcpm(bpm/4), assuming one bar of 4/4 per cycle.',
|
|
184
112
|
inputSchema: {
|
|
185
113
|
type: 'object',
|
|
186
114
|
properties: { bpm: { type: 'number', description: 'Tempo in BPM' }, ...SESSION_ID_PROP },
|
|
187
115
|
required: ['bpm'],
|
|
188
116
|
},
|
|
189
117
|
},
|
|
190
|
-
{
|
|
191
|
-
name: 'add_swing',
|
|
192
|
-
description: '[DEPRECATED — use transform({ op: "swing" }) instead] Add swing to pattern',
|
|
193
|
-
inputSchema: {
|
|
194
|
-
type: 'object',
|
|
195
|
-
properties: { amount: { type: 'number', description: 'Swing amount (0-1)' }, ...SESSION_ID_PROP },
|
|
196
|
-
required: ['amount'],
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name: 'apply_scale',
|
|
201
|
-
description: '[DEPRECATED — use transform({ op: "scale" }) instead] Apply scale to notes',
|
|
202
|
-
inputSchema: {
|
|
203
|
-
type: 'object',
|
|
204
|
-
properties: {
|
|
205
|
-
scale: { type: 'string', description: 'Scale name' },
|
|
206
|
-
root: { type: 'string', description: 'Root note' },
|
|
207
|
-
...SESSION_ID_PROP,
|
|
208
|
-
},
|
|
209
|
-
required: ['scale', 'root'],
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
118
|
{
|
|
213
119
|
name: 'shape',
|
|
214
120
|
description: 'Shape the current pattern along one of three high-level dimensions. ' +
|
|
@@ -236,42 +142,6 @@ export const tools = [
|
|
|
236
142
|
required: ['dimension'],
|
|
237
143
|
},
|
|
238
144
|
},
|
|
239
|
-
{
|
|
240
|
-
name: 'shift_mood',
|
|
241
|
-
description: '[DEPRECATED — use shape({ dimension: "mood" }) instead] Transform current pattern to match a different emotional mood. Moods: dark, euphoric, melancholic, aggressive, dreamy, peaceful, energetic.',
|
|
242
|
-
inputSchema: {
|
|
243
|
-
type: 'object',
|
|
244
|
-
properties: {
|
|
245
|
-
target_mood: {
|
|
246
|
-
type: 'string',
|
|
247
|
-
enum: ['dark', 'euphoric', 'melancholic', 'aggressive', 'dreamy', 'peaceful', 'energetic'],
|
|
248
|
-
description: 'Target mood',
|
|
249
|
-
},
|
|
250
|
-
intensity: { type: 'number', minimum: 0, maximum: 1, description: 'How strongly to apply the mood transformation (0-1, default: 0.5)' },
|
|
251
|
-
auto_play: { type: 'boolean', description: 'Start playback after transformation (default: true)' },
|
|
252
|
-
...SESSION_ID_PROP,
|
|
253
|
-
},
|
|
254
|
-
required: ['target_mood'],
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: 'set_energy',
|
|
259
|
-
description: '[DEPRECATED — use shape({ dimension: "energy" }) instead] Adjust energy level 0-10. Auto-plays after applying.',
|
|
260
|
-
inputSchema: {
|
|
261
|
-
type: 'object',
|
|
262
|
-
properties: { level: { type: 'number', description: 'Energy level from 0 (minimal) to 10 (maximum)' }, ...SESSION_ID_PROP },
|
|
263
|
-
required: ['level'],
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
name: 'refine',
|
|
268
|
-
description: '[DEPRECATED — use shape({ dimension: "refine" }) instead] Incrementally refine current pattern: faster/slower/louder/quieter/brighter/darker/"more reverb"/drier.',
|
|
269
|
-
inputSchema: {
|
|
270
|
-
type: 'object',
|
|
271
|
-
properties: { direction: { type: 'string', description: 'Refinement direction: faster, slower, louder, quieter, brighter, darker, "more reverb", or drier' }, ...SESSION_ID_PROP },
|
|
272
|
-
required: ['direction'],
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
145
|
];
|
|
276
146
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
277
147
|
// ---- per-op helpers (shared by transform({op}) and the deprecated aliases)
|
|
@@ -280,73 +150,139 @@ async function opTranspose(args, ctx, sid) {
|
|
|
280
150
|
throw new Error('Semitones must be an integer');
|
|
281
151
|
}
|
|
282
152
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
283
|
-
await ctx.writePatternSafe(transposePattern(p, args.semitones), sid);
|
|
284
|
-
return `Transposed ${args.semitones} semitones
|
|
153
|
+
const written = await ctx.writePatternSafe(transposePattern(p, args.semitones), sid);
|
|
154
|
+
return withStashNotice(`Transposed ${args.semitones} semitones`, written);
|
|
285
155
|
}
|
|
286
156
|
async function opReverse(ctx, sid) {
|
|
287
157
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
288
|
-
await ctx.writePatternSafe(p + '.rev', sid);
|
|
289
|
-
return 'Pattern reversed';
|
|
158
|
+
const written = await ctx.writePatternSafe(p + '.rev', sid);
|
|
159
|
+
return withStashNotice('Pattern reversed', written);
|
|
290
160
|
}
|
|
291
161
|
async function opStretch(args, ctx, sid) {
|
|
292
162
|
InputValidator.validateGain(args.factor);
|
|
293
163
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
294
|
-
await ctx.writePatternSafe(p + `.slow(${args.factor})`, sid);
|
|
295
|
-
return `Stretched by factor of ${args.factor}
|
|
164
|
+
const written = await ctx.writePatternSafe(p + `.slow(${args.factor})`, sid);
|
|
165
|
+
return withStashNotice(`Stretched by factor of ${args.factor}`, written);
|
|
296
166
|
}
|
|
297
167
|
async function opQuantize(args, ctx, sid) {
|
|
298
168
|
InputValidator.validateStringLength(args.grid, 'grid', 50, false);
|
|
299
169
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
300
|
-
await ctx.writePatternSafe(p + `.struct("${args.grid}")`, sid);
|
|
301
|
-
return `Quantized to ${args.grid} grid
|
|
170
|
+
const written = await ctx.writePatternSafe(p + `.struct("${args.grid}")`, sid);
|
|
171
|
+
return withStashNotice(`Quantized to ${args.grid} grid`, written);
|
|
302
172
|
}
|
|
303
173
|
async function opHumanize(args, ctx, sid) {
|
|
304
174
|
if (args.amount !== undefined)
|
|
305
175
|
InputValidator.validateNormalizedValue(args.amount, 'amount');
|
|
306
176
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
307
177
|
const amt = args.amount || 0.01;
|
|
308
|
-
await ctx.writePatternSafe(p + `.nudge(rand.range(-${amt}, ${amt}))`, sid);
|
|
309
|
-
return 'Added human timing';
|
|
178
|
+
const written = await ctx.writePatternSafe(p + `.nudge(rand.range(-${amt}, ${amt}))`, sid);
|
|
179
|
+
return withStashNotice('Added human timing', written);
|
|
310
180
|
}
|
|
311
181
|
async function opVary(args, ctx, sid) {
|
|
312
182
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
313
183
|
const varied = ctx.generator.generateVariation(p, args.type || 'subtle');
|
|
314
|
-
await ctx.writePatternSafe(varied, sid);
|
|
315
|
-
return `Added ${args.type || 'subtle'} variation
|
|
184
|
+
const written = await ctx.writePatternSafe(varied, sid);
|
|
185
|
+
return withStashNotice(`Added ${args.type || 'subtle'} variation`, written);
|
|
316
186
|
}
|
|
317
187
|
async function opSwing(args, ctx, sid) {
|
|
318
188
|
InputValidator.validateNormalizedValue(args.amount, 'amount');
|
|
319
189
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
320
|
-
await ctx.writePatternSafe(p + `.swing(${args.amount})`, sid);
|
|
321
|
-
return `Added swing: ${args.amount}
|
|
190
|
+
const written = await ctx.writePatternSafe(p + `.swing(${args.amount})`, sid);
|
|
191
|
+
return withStashNotice(`Added swing: ${args.amount}`, written);
|
|
322
192
|
}
|
|
323
193
|
async function opScale(args, ctx, sid) {
|
|
324
194
|
InputValidator.validateStringLength(args.scale, 'scale', 50, false);
|
|
195
|
+
// Concatenated straight into the pattern source below, so it must be a
|
|
196
|
+
// scale name and not arbitrary code — the same reason `opEffectAdd`
|
|
197
|
+
// validates its effect name (#236). A length check is not a content
|
|
198
|
+
// check: `scale: 'minor") .gain(9) //'` closed the string literal and
|
|
199
|
+
// appended a call, which is how it got past `PatternValidator`'s
|
|
200
|
+
// dangerous-gain rule — that runs on the pattern before this is
|
|
201
|
+
// appended to it (#440).
|
|
202
|
+
//
|
|
203
|
+
// `validateScaleName` already existed and `generate.ts` already used
|
|
204
|
+
// it; this path never did.
|
|
205
|
+
InputValidator.validateScaleName(args.scale);
|
|
325
206
|
InputValidator.validateRootNote(args.root);
|
|
326
207
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
327
|
-
await ctx.writePatternSafe(p + `.scale("${args.root}:${args.scale}")`, sid);
|
|
328
|
-
return `Applied ${args.root} ${args.scale} scale
|
|
208
|
+
const written = await ctx.writePatternSafe(p + `.scale("${args.root}:${args.scale}")`, sid);
|
|
209
|
+
return withStashNotice(`Applied ${args.root} ${args.scale} scale`, written);
|
|
329
210
|
}
|
|
330
211
|
async function opEffectAdd(args, ctx, sid) {
|
|
331
212
|
InputValidator.validateStringLength(args.effect, 'effect', 100, false);
|
|
213
|
+
// Concatenated straight into the pattern source, so it must be a plain
|
|
214
|
+
// method name and not arbitrary code (#236).
|
|
215
|
+
InputValidator.validateIdentifier(args.effect, 'effect');
|
|
332
216
|
if (args.params)
|
|
333
217
|
InputValidator.validateStringLength(args.params, 'params', 1000, true);
|
|
334
218
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
335
219
|
const withEffect = args.params
|
|
336
220
|
? p + `.${args.effect}(${args.params})`
|
|
337
221
|
: p + `.${args.effect}()`;
|
|
338
|
-
await ctx.writePatternSafe(withEffect, sid);
|
|
339
|
-
return `Added ${args.effect} effect
|
|
222
|
+
const written = await ctx.writePatternSafe(withEffect, sid);
|
|
223
|
+
return withStashNotice(`Added ${args.effect} effect`, written);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Removes every `.effect(...)` call from a pattern.
|
|
227
|
+
*
|
|
228
|
+
* Deliberately not a RegExp. The old implementation interpolated the
|
|
229
|
+
* caller's effect name into `new RegExp(...)`, which handed the caller
|
|
230
|
+
* regex *syntax* — `(a+)+Z` against a crafted pattern blocked the event
|
|
231
|
+
* loop for 25 seconds, and a synchronous String.replace has no timeout to
|
|
232
|
+
* break out of (#236).
|
|
233
|
+
*
|
|
234
|
+
* Scanning also fixes a correctness bug the regex had: `[^)]*` stops at
|
|
235
|
+
* the FIRST `)`, so `.lpf(add(1,2))` was left as a stray `)`. This walks
|
|
236
|
+
* balanced parentheses instead.
|
|
237
|
+
*
|
|
238
|
+
* @param pattern - Pattern source to strip
|
|
239
|
+
* @param effect - Effect name (validated as an identifier by the caller)
|
|
240
|
+
* @returns The pattern with every matching call removed
|
|
241
|
+
*/
|
|
242
|
+
function stripEffectCalls(pattern, effect) {
|
|
243
|
+
const needle = `.${effect}(`;
|
|
244
|
+
let out = '';
|
|
245
|
+
let cursor = 0;
|
|
246
|
+
for (;;) {
|
|
247
|
+
const start = pattern.indexOf(needle, cursor);
|
|
248
|
+
if (start === -1)
|
|
249
|
+
break;
|
|
250
|
+
// Walk to the matching close paren, respecting nesting.
|
|
251
|
+
let depth = 0;
|
|
252
|
+
let end = -1;
|
|
253
|
+
for (let i = start + needle.length - 1; i < pattern.length; i++) {
|
|
254
|
+
const ch = pattern[i];
|
|
255
|
+
if (ch === '(')
|
|
256
|
+
depth++;
|
|
257
|
+
else if (ch === ')') {
|
|
258
|
+
depth--;
|
|
259
|
+
if (depth === 0) {
|
|
260
|
+
end = i;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Unbalanced: leave the rest untouched rather than mangling it.
|
|
266
|
+
if (end === -1)
|
|
267
|
+
break;
|
|
268
|
+
out += pattern.slice(cursor, start);
|
|
269
|
+
cursor = end + 1;
|
|
270
|
+
}
|
|
271
|
+
return out + pattern.slice(cursor);
|
|
340
272
|
}
|
|
341
273
|
async function opEffectRemove(args, ctx, sid) {
|
|
342
274
|
InputValidator.validateStringLength(args.effect, 'effect', 100, false);
|
|
275
|
+
// Must be a plain identifier: it is matched literally below, and was
|
|
276
|
+
// previously interpolated into a RegExp (#236).
|
|
277
|
+
InputValidator.validateIdentifier(args.effect, 'effect');
|
|
343
278
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
344
|
-
const
|
|
345
|
-
|
|
279
|
+
const stripped = stripEffectCalls(p, args.effect);
|
|
280
|
+
// The strip ran and matched nothing. That is a valid-empty result,
|
|
281
|
+
// not a failure and not a plain success (#288).
|
|
346
282
|
if (stripped === p)
|
|
347
|
-
return `No ${args.effect} effect found to remove
|
|
348
|
-
await ctx.writePatternSafe(stripped, sid);
|
|
349
|
-
return `Removed ${args.effect} effect
|
|
283
|
+
return empty(`No ${args.effect} effect found to remove`);
|
|
284
|
+
const written = await ctx.writePatternSafe(stripped, sid);
|
|
285
|
+
return withStashNotice(`Removed ${args.effect} effect`, written);
|
|
350
286
|
}
|
|
351
287
|
export async function execute(name, args, ctx) {
|
|
352
288
|
const sid = args?.session_id;
|
|
@@ -369,15 +305,6 @@ export async function execute(name, args, ctx) {
|
|
|
369
305
|
throw new Error(`Invalid op: ${op}. Must be one of: transpose, reverse, stretch, quantize, humanize, swing, scale, vary`);
|
|
370
306
|
}
|
|
371
307
|
}
|
|
372
|
-
// Deprecated aliases — forward to per-op helpers.
|
|
373
|
-
case 'transpose': return await opTranspose(args, ctx, sid);
|
|
374
|
-
case 'reverse': return await opReverse(ctx, sid);
|
|
375
|
-
case 'stretch': return await opStretch(args, ctx, sid);
|
|
376
|
-
case 'quantize': return await opQuantize(args, ctx, sid);
|
|
377
|
-
case 'humanize': return await opHumanize(args, ctx, sid);
|
|
378
|
-
case 'generate_variation': return await opVary(args, ctx, sid);
|
|
379
|
-
case 'add_swing': return await opSwing(args, ctx, sid);
|
|
380
|
-
case 'apply_scale': return await opScale(args, ctx, sid);
|
|
381
308
|
case 'effect': {
|
|
382
309
|
const a = args?.action;
|
|
383
310
|
if (a !== 'add' && a !== 'remove') {
|
|
@@ -387,13 +314,15 @@ export async function execute(name, args, ctx) {
|
|
|
387
314
|
? await opEffectAdd(args, ctx, sid)
|
|
388
315
|
: await opEffectRemove(args, ctx, sid);
|
|
389
316
|
}
|
|
390
|
-
case 'add_effect': return await opEffectAdd(args, ctx, sid);
|
|
391
|
-
case 'remove_effect': return await opEffectRemove(args, ctx, sid);
|
|
392
317
|
case 'set_tempo': {
|
|
393
318
|
InputValidator.validateBPM(args.bpm);
|
|
394
319
|
const p = await ctx.getCurrentPatternSafe(sid);
|
|
395
|
-
|
|
396
|
-
|
|
320
|
+
// `setcpm` is CYCLES per minute, and the Strudel convention this
|
|
321
|
+
// project follows is one bar of four beats per cycle — so the
|
|
322
|
+
// divisor is what makes `set_tempo({ bpm: 130 })` produce 130 BPM
|
|
323
|
+
// rather than 520 (#395).
|
|
324
|
+
const written = await ctx.writePatternSafe(`setcpm(${String(args.bpm)}/${String(BEATS_PER_CYCLE)})\n${p}`, sid);
|
|
325
|
+
return withStashNotice(`Set tempo to ${args.bpm} BPM`, written);
|
|
397
326
|
}
|
|
398
327
|
case 'shape': {
|
|
399
328
|
const dim = args?.dimension;
|
|
@@ -405,21 +334,30 @@ export async function execute(name, args, ctx) {
|
|
|
405
334
|
throw new Error(`Invalid dimension: ${dim}. Must be one of: mood, energy, refine`);
|
|
406
335
|
}
|
|
407
336
|
}
|
|
408
|
-
case 'shift_mood': return await shiftMood(args, ctx, sid);
|
|
409
|
-
case 'set_energy': return await setEnergyLevel(args.level, ctx, sid);
|
|
410
|
-
case 'refine': return await refinePattern(args.direction, ctx, sid);
|
|
411
337
|
default:
|
|
412
338
|
throw new Error(`transform module does not handle tool: ${name}`);
|
|
413
339
|
}
|
|
414
340
|
}
|
|
415
341
|
async function shiftMood(args, ctx, sid) {
|
|
416
342
|
const mood = args.target_mood?.toLowerCase()?.trim();
|
|
417
|
-
|
|
343
|
+
// hasOwn, not a bare index: MOOD_PROFILES is a plain object literal,
|
|
344
|
+
// so MOOD_PROFILES['constructor'] returned Object — truthy, so the
|
|
345
|
+
// `if (!profile)` guard below never fired. The result was
|
|
346
|
+
// s("bd*4").slow(NaN).lpf(NaN).room(NaN).gain(NaN) written into the
|
|
347
|
+
// editor and reported as success:true (#308).
|
|
348
|
+
const profile = typeof mood === 'string' && Object.hasOwn(MOOD_PROFILES, mood)
|
|
349
|
+
? MOOD_PROFILES[mood]
|
|
350
|
+
: undefined;
|
|
418
351
|
if (!profile) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
352
|
+
// `err('validation', ...)`, not a failure-shaped object.
|
|
353
|
+
//
|
|
354
|
+
// The dispatcher categorises those by running the MESSAGE through
|
|
355
|
+
// the string matcher, and "Valid moods: ..." contains none of the
|
|
356
|
+
// phrases it looks for — so a caller's typo landed in `internal`,
|
|
357
|
+
// which is the non-retryable fall-through for a fault that is not
|
|
358
|
+
// the caller's (#453). This is the audit #382 describes, at four
|
|
359
|
+
// sites it did not reach.
|
|
360
|
+
return err('validation', `Unknown mood: ${args.target_mood}. Valid moods: ${Object.keys(MOOD_PROFILES).join(', ')}.`);
|
|
423
361
|
}
|
|
424
362
|
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
425
363
|
if (!pattern || pattern.trim().length === 0) {
|
|
@@ -463,11 +401,11 @@ async function shiftMood(args, ctx, sid) {
|
|
|
463
401
|
result += `.gain(${g.toFixed(2)})`;
|
|
464
402
|
applied.push(`gain ${profile.gainMod > 0 ? '+' : ''}${Math.round(profile.gainMod * 100 * intensity)}%`);
|
|
465
403
|
}
|
|
466
|
-
await ctx.writePatternSafe(result, sid);
|
|
404
|
+
const written = await ctx.writePatternSafe(result, sid);
|
|
467
405
|
if (args.auto_play !== false && (sid || ctx.isInitialized())) {
|
|
468
406
|
await ctx.getController(sid).play();
|
|
469
407
|
}
|
|
470
|
-
return { success: true, target_mood: mood, intensity, applied_effects: applied };
|
|
408
|
+
return withStashField({ success: true, target_mood: mood, intensity, applied_effects: applied }, written);
|
|
471
409
|
}
|
|
472
410
|
async function setEnergyLevel(level, ctx, sid) {
|
|
473
411
|
if (level < 0 || level > 10 || !Number.isInteger(level)) {
|
|
@@ -482,10 +420,10 @@ async function setEnergyLevel(level, ctx, sid) {
|
|
|
482
420
|
if (config.densityAdjust)
|
|
483
421
|
result += config.densityAdjust;
|
|
484
422
|
result += `.room(${config.roomAmount})`;
|
|
485
|
-
await ctx.writePatternSafe(result, sid);
|
|
423
|
+
const written = await ctx.writePatternSafe(result, sid);
|
|
486
424
|
if (sid || ctx.isInitialized())
|
|
487
425
|
await ctx.getController(sid).play();
|
|
488
|
-
return { success: true, level, description: config.description };
|
|
426
|
+
return withStashField({ success: true, level, description: config.description }, written);
|
|
489
427
|
}
|
|
490
428
|
async function refinePattern(direction, ctx, sid) {
|
|
491
429
|
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
@@ -520,14 +458,13 @@ async function refinePattern(direction, ctx, sid) {
|
|
|
520
458
|
modification = '.room(0.1)';
|
|
521
459
|
break;
|
|
522
460
|
default:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
};
|
|
461
|
+
// See the mood branch above: the caller's argument, categorised
|
|
462
|
+
// `validation` rather than `internal` (#453).
|
|
463
|
+
return err('validation', `Unknown refinement direction: ${direction}. Supported: faster, slower, louder, quieter, brighter, darker, "more reverb", drier.`);
|
|
527
464
|
}
|
|
528
|
-
await ctx.writePatternSafe(pattern + modification, sid);
|
|
465
|
+
const written = await ctx.writePatternSafe(pattern + modification, sid);
|
|
529
466
|
if (sid || ctx.isInitialized())
|
|
530
467
|
await ctx.getController(sid).play();
|
|
531
|
-
return { success: true, direction: dir, applied: modification };
|
|
468
|
+
return withStashField({ success: true, direction: dir, applied: modification }, written);
|
|
532
469
|
}
|
|
533
470
|
export const transformModule = { tools, toolNames, execute };
|