@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,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* editor domain — pattern editing in the CodeMirror editor.
|
|
3
3
|
*
|
|
4
|
-
* Owns one consolidated mutation tool (`edit_pattern(mode)`)
|
|
5
|
-
* `get_pattern` (kept
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Aliases forward to `edit_pattern` and will be removed in a future
|
|
9
|
-
* release per the deprecation policy.
|
|
4
|
+
* Owns one consolidated mutation tool (`edit_pattern(mode)`) with
|
|
5
|
+
* write/append/insert/replace/clear modes, plus `get_pattern` (kept
|
|
6
|
+
* separate — hot read path).
|
|
10
7
|
*/
|
|
8
|
+
import { empty, ok, withStashField } from './types.js';
|
|
11
9
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
10
|
+
import { ValidationError } from '../../utils/CategorisedError.js';
|
|
12
11
|
const SESSION_ID_PROP = {
|
|
13
12
|
session_id: {
|
|
14
13
|
type: 'string',
|
|
@@ -22,7 +21,8 @@ export const tools = [
|
|
|
22
21
|
'mode=write replaces the editor contents (default; mirrors the old write tool exactly, including optional pattern validation and auto_play). ' +
|
|
23
22
|
'mode=append concatenates `code` after the current pattern with a newline. ' +
|
|
24
23
|
'mode=insert places `code` at the given line `position`. ' +
|
|
25
|
-
'mode=replace
|
|
24
|
+
'mode=replace substitutes `search` with `replace` — the first occurrence only, unless replace_all is true. ' +
|
|
25
|
+
'The response reports how many occurrences matched, were replaced, and remain. ' +
|
|
26
26
|
'mode=clear empties the editor. ' +
|
|
27
27
|
'Example: edit_pattern({ mode: "write", pattern: "s(\\"bd\\")", auto_play: true }). ' +
|
|
28
28
|
'For reading the editor without mutating it use get_pattern; for the on-disk pattern catalog use pattern_store.',
|
|
@@ -36,9 +36,14 @@ export const tools = [
|
|
|
36
36
|
},
|
|
37
37
|
pattern: { type: 'string', description: 'Pattern code (mode=write)' },
|
|
38
38
|
code: { type: 'string', description: 'Code to append/insert (mode=append/insert)' },
|
|
39
|
-
position: {
|
|
39
|
+
position: {
|
|
40
|
+
type: 'number',
|
|
41
|
+
description: 'mode=insert: 0-indexed line to insert BEFORE. 0 puts the code at the top; '
|
|
42
|
+
+ 'a value equal to the line count appends. Out of range is an error, not an append.',
|
|
43
|
+
},
|
|
40
44
|
search: { type: 'string', description: 'Text to replace (mode=replace)' },
|
|
41
45
|
replace: { type: 'string', description: 'Replacement text (mode=replace)' },
|
|
46
|
+
replace_all: { type: 'boolean', description: 'mode=replace: replace every occurrence instead of just the first (default false)' },
|
|
42
47
|
auto_play: { type: 'boolean', description: 'Start playback after write (mode=write only, default: false)' },
|
|
43
48
|
validate: { type: 'boolean', description: 'Validate pattern before write (mode=write only, default: true)' },
|
|
44
49
|
...SESSION_ID_PROP,
|
|
@@ -50,60 +55,6 @@ export const tools = [
|
|
|
50
55
|
description: 'Get current pattern code',
|
|
51
56
|
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
52
57
|
},
|
|
53
|
-
{
|
|
54
|
-
name: 'write',
|
|
55
|
-
description: '[DEPRECATED — use edit_pattern({ mode: "write" }) instead] Write pattern to editor with optional auto-play and validation',
|
|
56
|
-
inputSchema: {
|
|
57
|
-
type: 'object',
|
|
58
|
-
properties: {
|
|
59
|
-
pattern: { type: 'string', description: 'Pattern code' },
|
|
60
|
-
auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
|
|
61
|
-
validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' },
|
|
62
|
-
...SESSION_ID_PROP,
|
|
63
|
-
},
|
|
64
|
-
required: ['pattern'],
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'append',
|
|
69
|
-
description: '[DEPRECATED — use edit_pattern({ mode: "append" }) instead] Append code to current pattern',
|
|
70
|
-
inputSchema: {
|
|
71
|
-
type: 'object',
|
|
72
|
-
properties: { code: { type: 'string', description: 'Code to append' }, ...SESSION_ID_PROP },
|
|
73
|
-
required: ['code'],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'insert',
|
|
78
|
-
description: '[DEPRECATED — use edit_pattern({ mode: "insert" }) instead] Insert code at specific line',
|
|
79
|
-
inputSchema: {
|
|
80
|
-
type: 'object',
|
|
81
|
-
properties: {
|
|
82
|
-
position: { type: 'number', description: 'Line number' },
|
|
83
|
-
code: { type: 'string', description: 'Code to insert' },
|
|
84
|
-
...SESSION_ID_PROP,
|
|
85
|
-
},
|
|
86
|
-
required: ['position', 'code'],
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: 'replace',
|
|
91
|
-
description: '[DEPRECATED — use edit_pattern({ mode: "replace" }) instead] Replace pattern section',
|
|
92
|
-
inputSchema: {
|
|
93
|
-
type: 'object',
|
|
94
|
-
properties: {
|
|
95
|
-
search: { type: 'string', description: 'Text to replace' },
|
|
96
|
-
replace: { type: 'string', description: 'Replacement text' },
|
|
97
|
-
...SESSION_ID_PROP,
|
|
98
|
-
},
|
|
99
|
-
required: ['search', 'replace'],
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'clear',
|
|
104
|
-
description: '[DEPRECATED — use edit_pattern({ mode: "clear" }) instead] Clear the editor',
|
|
105
|
-
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
106
|
-
},
|
|
107
58
|
];
|
|
108
59
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
109
60
|
async function doWrite(args, ctx, sid) {
|
|
@@ -141,20 +92,66 @@ async function doAppend(args, ctx, sid) {
|
|
|
141
92
|
return await ctx.writePatternSafe(current + '\n' + args.code, sid);
|
|
142
93
|
}
|
|
143
94
|
async function doInsert(args, ctx, sid) {
|
|
144
|
-
InputValidator.validatePositiveInteger(args.position, 'position');
|
|
145
95
|
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
146
96
|
const lines = (await ctx.getCurrentPatternSafe(sid)).split('\n');
|
|
147
|
-
|
|
97
|
+
// 0-indexed, and 0 is a legal position.
|
|
98
|
+
//
|
|
99
|
+
// `validatePositiveInteger` rejected 0 while the `splice` below is
|
|
100
|
+
// 0-indexed, so inserting at the TOP of a pattern was impossible
|
|
101
|
+
// through this tool — and `StrudelController.insertAtLine`, which does
|
|
102
|
+
// the same job, documents the parameter as 0-indexed and permits 0.
|
|
103
|
+
//
|
|
104
|
+
// The other end was unguarded: `position: 9999` on a three-line
|
|
105
|
+
// pattern passed validation and `splice` silently appended, where
|
|
106
|
+
// `insertAtLine` throws. Now it matches that contract exactly
|
|
107
|
+
// (#442), so the two ways of doing this cannot disagree.
|
|
108
|
+
const position = args.position;
|
|
109
|
+
if (!Number.isInteger(position) || position < 0 || position > lines.length) {
|
|
110
|
+
throw new ValidationError(`Invalid position: ${String(position)}. Must be an integer between 0 and ${String(lines.length)} ` +
|
|
111
|
+
`(0 inserts before the first line, ${String(lines.length)} appends).`);
|
|
112
|
+
}
|
|
113
|
+
lines.splice(position, 0, args.code);
|
|
148
114
|
return await ctx.writePatternSafe(lines.join('\n'), sid);
|
|
149
115
|
}
|
|
150
116
|
async function doReplace(args, ctx, sid) {
|
|
151
117
|
InputValidator.validateStringLength(args.search, 'search', 1000, true);
|
|
152
118
|
InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
|
|
119
|
+
const search = args.search ?? '';
|
|
120
|
+
if (search.length === 0) {
|
|
121
|
+
throw new Error('Invalid search: must be a non-empty string.');
|
|
122
|
+
}
|
|
153
123
|
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
124
|
+
// Literal string operations throughout — never a RegExp built from
|
|
125
|
+
// `search`. Interpolating caller text into a pattern is how #236
|
|
126
|
+
// happened: `(a+)+Z` against a crafted subject blocked the event loop
|
|
127
|
+
// for 25 seconds.
|
|
128
|
+
const matches = pattern.split(search).length - 1;
|
|
154
129
|
// Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
|
|
155
130
|
const safeReplacement = args.replace.replace(/\$/g, '$$$$');
|
|
156
|
-
const
|
|
157
|
-
|
|
131
|
+
const replaceAll = args.replace_all === true;
|
|
132
|
+
const updated = replaceAll
|
|
133
|
+
? pattern.replaceAll(search, safeReplacement)
|
|
134
|
+
: pattern.replace(search, safeReplacement);
|
|
135
|
+
const replaced = replaceAll ? matches : Math.min(matches, 1);
|
|
136
|
+
const remaining = matches - replaced;
|
|
137
|
+
const written = await ctx.writePatternSafe(updated, sid);
|
|
138
|
+
// Counts are reported under BOTH settings. Previously a caller had no
|
|
139
|
+
// way to learn that other occurrences survived — and these callers are
|
|
140
|
+
// LLM agents, which do not reliably re-read the pattern to check (#243).
|
|
141
|
+
// Zero matches is a valid-empty: the search ran and found nothing.
|
|
142
|
+
// Wrapped in a plain ok() it was indistinguishable from a replace
|
|
143
|
+
// that actually changed the pattern (#288).
|
|
144
|
+
const wrap = matches === 0 ? empty : ok;
|
|
145
|
+
return wrap(withStashField({
|
|
146
|
+
success: true,
|
|
147
|
+
message: matches === 0
|
|
148
|
+
? `No occurrences of ${JSON.stringify(search)} found`
|
|
149
|
+
: `Replaced ${String(replaced)} of ${String(matches)} occurrence(s)` +
|
|
150
|
+
(remaining > 0 ? `; ${String(remaining)} remain. Pass replace_all: true to replace them all.` : ''),
|
|
151
|
+
replaced,
|
|
152
|
+
remaining,
|
|
153
|
+
matches,
|
|
154
|
+
}, written));
|
|
158
155
|
}
|
|
159
156
|
async function doClear(ctx, sid) {
|
|
160
157
|
return await ctx.writePatternSafe('', sid);
|
|
@@ -177,12 +174,6 @@ export async function execute(name, args, ctx) {
|
|
|
177
174
|
throw new Error(`Invalid mode: ${mode}. Must be one of: write, append, insert, replace, clear`);
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
|
-
// Deprecated aliases — forward to consolidated handler.
|
|
181
|
-
case 'write': return await doWrite(args, ctx, sid);
|
|
182
|
-
case 'append': return await doAppend(args, ctx, sid);
|
|
183
|
-
case 'insert': return await doInsert(args, ctx, sid);
|
|
184
|
-
case 'replace': return await doReplace(args, ctx, sid);
|
|
185
|
-
case 'clear': return await doClear(ctx, sid);
|
|
186
177
|
case 'get_pattern':
|
|
187
178
|
return await ctx.getCurrentPatternSafe(sid);
|
|
188
179
|
default:
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* generate domain — pattern and music-theory generation (no browser needed).
|
|
3
3
|
*
|
|
4
|
-
* Owns (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Owns (3 tools):
|
|
5
|
+
* generate_part(role) — drums/bass/melody/fill layers
|
|
6
|
+
* music_theory(query) — scale notes, chord progressions
|
|
7
|
+
* generate_rhythm(type) — euclidean / polyrhythm patterns
|
|
8
8
|
*
|
|
9
|
-
* `compose`
|
|
10
|
-
* + play + optional AI feedback
|
|
11
|
-
* happens. Per the #110 audit, compose eventually absorbs generate_pattern
|
|
12
|
-
* and the four generate_* drums/bass/melody/fill tools collapse into
|
|
13
|
-
* `generate_part(role)`, while scale+chord_prog become `music_theory`.
|
|
9
|
+
* Full-composition generation lives in `compose` (compose.ts), which
|
|
10
|
+
* orchestrates init + generate + play + optional AI feedback.
|
|
14
11
|
*/
|
|
12
|
+
import { withStashNotice } from './types.js';
|
|
15
13
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
14
|
+
import { BASS_STYLES, DRUM_STYLES, resolveBassStyle, resolveDrumStyle } from '../../services/StyleRegistry.js';
|
|
16
15
|
const SESSION_ID_PROP = {
|
|
17
16
|
session_id: {
|
|
18
17
|
type: 'string',
|
|
@@ -20,21 +19,6 @@ const SESSION_ID_PROP = {
|
|
|
20
19
|
},
|
|
21
20
|
};
|
|
22
21
|
export const tools = [
|
|
23
|
-
{
|
|
24
|
-
name: 'generate_pattern',
|
|
25
|
-
description: '[DEPRECATED — use compose instead, which auto-inits and returns richer metadata] Generate complete pattern from style with optional auto-play.',
|
|
26
|
-
inputSchema: {
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
style: { type: 'string', description: 'Music style (techno/house/dnb/ambient/etc)' },
|
|
30
|
-
key: { type: 'string', description: 'Musical key' },
|
|
31
|
-
bpm: { type: 'number', description: 'Tempo in BPM' },
|
|
32
|
-
auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' },
|
|
33
|
-
...SESSION_ID_PROP,
|
|
34
|
-
},
|
|
35
|
-
required: ['style'],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
22
|
{
|
|
39
23
|
name: 'generate_part',
|
|
40
24
|
description: 'Generate a single instrumental layer and append it to the current session pattern. ' +
|
|
@@ -60,51 +44,11 @@ export const tools = [
|
|
|
60
44
|
required: ['role'],
|
|
61
45
|
},
|
|
62
46
|
},
|
|
63
|
-
{
|
|
64
|
-
name: 'generate_drums',
|
|
65
|
-
description: '[DEPRECATED — use generate_part({ role: "drums" }) instead] Generate drum pattern',
|
|
66
|
-
inputSchema: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: {
|
|
69
|
-
style: { type: 'string', description: 'Drum style' },
|
|
70
|
-
complexity: { type: 'number', description: 'Complexity (0-1)' },
|
|
71
|
-
...SESSION_ID_PROP,
|
|
72
|
-
},
|
|
73
|
-
required: ['style'],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'generate_bassline',
|
|
78
|
-
description: '[DEPRECATED — use generate_part({ role: "bass" }) instead] Generate bassline',
|
|
79
|
-
inputSchema: {
|
|
80
|
-
type: 'object',
|
|
81
|
-
properties: {
|
|
82
|
-
key: { type: 'string', description: 'Musical key' },
|
|
83
|
-
style: { type: 'string', description: 'Bass style' },
|
|
84
|
-
...SESSION_ID_PROP,
|
|
85
|
-
},
|
|
86
|
-
required: ['key', 'style'],
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: 'generate_melody',
|
|
91
|
-
description: '[DEPRECATED — use generate_part({ role: "melody" }) instead] Generate melody from scale',
|
|
92
|
-
inputSchema: {
|
|
93
|
-
type: 'object',
|
|
94
|
-
properties: {
|
|
95
|
-
scale: { type: 'string', description: 'Scale name' },
|
|
96
|
-
root: { type: 'string', description: 'Root note' },
|
|
97
|
-
length: { type: 'number', description: 'Number of notes' },
|
|
98
|
-
...SESSION_ID_PROP,
|
|
99
|
-
},
|
|
100
|
-
required: ['scale', 'root'],
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
47
|
{
|
|
104
48
|
name: 'music_theory',
|
|
105
49
|
description: 'Music-theory queries. ' +
|
|
106
50
|
'query=scale returns the notes of a scale (e.g. "C major scale: C, D, E, F, G, A, B"). ' +
|
|
107
|
-
'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session
|
|
51
|
+
'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session. ' +
|
|
108
52
|
'Example: music_theory({ query: "scale", root: "C", scale: "major" }). ' +
|
|
109
53
|
'For pattern generation (drums/bass/melody) use generate_part; for rhythmic patterns use generate_rhythm.',
|
|
110
54
|
inputSchema: {
|
|
@@ -120,31 +64,6 @@ export const tools = [
|
|
|
120
64
|
required: ['query'],
|
|
121
65
|
},
|
|
122
66
|
},
|
|
123
|
-
{
|
|
124
|
-
name: 'generate_scale',
|
|
125
|
-
description: '[DEPRECATED — use music_theory({ query: "scale" }) instead] Generate scale notes',
|
|
126
|
-
inputSchema: {
|
|
127
|
-
type: 'object',
|
|
128
|
-
properties: {
|
|
129
|
-
root: { type: 'string', description: 'Root note' },
|
|
130
|
-
scale: { type: 'string', description: 'Scale type' },
|
|
131
|
-
},
|
|
132
|
-
required: ['root', 'scale'],
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: 'generate_chord_progression',
|
|
137
|
-
description: '[DEPRECATED — use music_theory({ query: "chord_progression" }) instead] Generate chord progression',
|
|
138
|
-
inputSchema: {
|
|
139
|
-
type: 'object',
|
|
140
|
-
properties: {
|
|
141
|
-
key: { type: 'string', description: 'Key' },
|
|
142
|
-
style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' },
|
|
143
|
-
...SESSION_ID_PROP,
|
|
144
|
-
},
|
|
145
|
-
required: ['key', 'style'],
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
67
|
{
|
|
149
68
|
name: 'generate_rhythm',
|
|
150
69
|
description: 'Generate a rhythmic pattern and append it to the current session. ' +
|
|
@@ -166,52 +85,19 @@ export const tools = [
|
|
|
166
85
|
required: ['type'],
|
|
167
86
|
},
|
|
168
87
|
},
|
|
169
|
-
{
|
|
170
|
-
name: 'generate_euclidean',
|
|
171
|
-
description: '[DEPRECATED — use generate_rhythm({ type: "euclidean" }) instead] Generate Euclidean rhythm',
|
|
172
|
-
inputSchema: {
|
|
173
|
-
type: 'object',
|
|
174
|
-
properties: {
|
|
175
|
-
hits: { type: 'number', description: 'Number of hits' },
|
|
176
|
-
steps: { type: 'number', description: 'Total steps' },
|
|
177
|
-
sound: { type: 'string', description: 'Sound to use' },
|
|
178
|
-
...SESSION_ID_PROP,
|
|
179
|
-
},
|
|
180
|
-
required: ['hits', 'steps'],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: 'generate_polyrhythm',
|
|
185
|
-
description: '[DEPRECATED — use generate_rhythm({ type: "polyrhythm" }) instead] Generate polyrhythm',
|
|
186
|
-
inputSchema: {
|
|
187
|
-
type: 'object',
|
|
188
|
-
properties: {
|
|
189
|
-
sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
|
|
190
|
-
patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' },
|
|
191
|
-
...SESSION_ID_PROP,
|
|
192
|
-
},
|
|
193
|
-
required: ['sounds', 'patterns'],
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: 'generate_fill',
|
|
198
|
-
description: '[DEPRECATED — use generate_part({ role: "fill" }) instead] Generate drum fill',
|
|
199
|
-
inputSchema: {
|
|
200
|
-
type: 'object',
|
|
201
|
-
properties: {
|
|
202
|
-
style: { type: 'string', description: 'Fill style' },
|
|
203
|
-
bars: { type: 'number', description: 'Number of bars' },
|
|
204
|
-
...SESSION_ID_PROP,
|
|
205
|
-
},
|
|
206
|
-
required: ['style'],
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
88
|
];
|
|
210
89
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
90
|
+
/**
|
|
91
|
+
* Appends generated material to the current pattern and writes it back.
|
|
92
|
+
*
|
|
93
|
+
* @returns Whatever `writePatternSafe` returned — callers must pass it
|
|
94
|
+
* to `withStashNotice` rather than discard it, because a write with
|
|
95
|
+
* no browser up is a stash, not a write (#285).
|
|
96
|
+
*/
|
|
211
97
|
async function appendOrSet(generated, ctx, sessionId) {
|
|
212
98
|
const current = await ctx.getCurrentPatternSafe(sessionId);
|
|
213
99
|
const combined = current ? current + '\n' + generated : generated;
|
|
214
|
-
await ctx.writePatternSafe(combined, sessionId);
|
|
100
|
+
return await ctx.writePatternSafe(combined, sessionId);
|
|
215
101
|
}
|
|
216
102
|
function doScale(args, ctx) {
|
|
217
103
|
InputValidator.validateRootNote(args.root);
|
|
@@ -224,38 +110,52 @@ async function doChordProgression(args, ctx, sid) {
|
|
|
224
110
|
InputValidator.validateChordStyle(args.style);
|
|
225
111
|
const progression = ctx.theory.generateChordProgression(args.key, args.style);
|
|
226
112
|
const chordPattern = ctx.generator.generateChords(progression);
|
|
227
|
-
await appendOrSet(chordPattern, ctx, sid);
|
|
228
|
-
return `Generated ${args.style} progression in ${args.key}: ${progression}
|
|
113
|
+
const written = await appendOrSet(chordPattern, ctx, sid);
|
|
114
|
+
return withStashNotice(`Generated ${args.style} progression in ${args.key}: ${progression}`, written);
|
|
229
115
|
}
|
|
230
116
|
async function doEuclidean(args, ctx, sid) {
|
|
231
117
|
InputValidator.validateEuclidean(args.hits, args.steps);
|
|
232
118
|
if (args.sound)
|
|
233
119
|
InputValidator.validateStringLength(args.sound, 'sound', 100, false);
|
|
234
120
|
const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
|
|
235
|
-
await appendOrSet(euclidean, ctx, sid);
|
|
236
|
-
return `Generated Euclidean rhythm (${args.hits}/${args.steps})
|
|
121
|
+
const written = await appendOrSet(euclidean, ctx, sid);
|
|
122
|
+
return withStashNotice(`Generated Euclidean rhythm (${args.hits}/${args.steps})`, written);
|
|
237
123
|
}
|
|
238
124
|
async function doPolyrhythm(args, ctx, sid) {
|
|
239
125
|
args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
|
|
240
126
|
args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
|
|
241
127
|
const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
|
|
242
|
-
await appendOrSet(poly, ctx, sid);
|
|
243
|
-
return 'Generated polyrhythm';
|
|
128
|
+
const written = await appendOrSet(poly, ctx, sid);
|
|
129
|
+
return withStashNotice('Generated polyrhythm', written);
|
|
244
130
|
}
|
|
245
131
|
async function doDrums(args, ctx, sid) {
|
|
246
132
|
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
247
133
|
if (args.complexity !== undefined)
|
|
248
134
|
InputValidator.validateNormalizedValue(args.complexity, 'complexity');
|
|
135
|
+
// Say which style actually produced the notes. An unknown genre falls
|
|
136
|
+
// back to techno, and echoing the request back made that invisible (#279).
|
|
137
|
+
const resolution = resolveDrumStyle(args.style);
|
|
249
138
|
const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
|
|
250
|
-
await appendOrSet(drums, ctx, sid);
|
|
251
|
-
return
|
|
139
|
+
const written = await appendOrSet(drums, ctx, sid);
|
|
140
|
+
return withStashNotice(resolution.supported
|
|
141
|
+
? `Generated ${resolution.resolved} drums`
|
|
142
|
+
: `No drum pattern for style "${args.style}" — generated ${resolution.resolved} drums instead. ` +
|
|
143
|
+
`Styles with their own drums: ${DRUM_STYLES.join(', ')}.`, written);
|
|
252
144
|
}
|
|
253
145
|
async function doBassline(args, ctx, sid) {
|
|
254
146
|
InputValidator.validateRootNote(args.key);
|
|
255
147
|
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
148
|
+
// role=bass echoed args.style back while the generator had silently
|
|
149
|
+
// fallen back to techno — #279 in the same file, eight lines below
|
|
150
|
+
// the sibling it fixed (#294).
|
|
151
|
+
const resolution = resolveBassStyle(args.style);
|
|
256
152
|
const bass = ctx.generator.generateBassline(args.key, args.style);
|
|
257
|
-
await appendOrSet(bass, ctx, sid);
|
|
258
|
-
return
|
|
153
|
+
const written = await appendOrSet(bass, ctx, sid);
|
|
154
|
+
return withStashNotice(resolution.supported
|
|
155
|
+
? `Generated ${resolution.resolved} bassline in ${args.key}`
|
|
156
|
+
: `No bassline for style "${args.style}" — generated ${resolution.resolved} ` +
|
|
157
|
+
`bassline in ${args.key} instead. ` +
|
|
158
|
+
`Styles with their own bassline: ${BASS_STYLES.join(', ')}.`, written);
|
|
259
159
|
}
|
|
260
160
|
async function doMelody(args, ctx, sid) {
|
|
261
161
|
InputValidator.validateRootNote(args.root);
|
|
@@ -264,34 +164,20 @@ async function doMelody(args, ctx, sid) {
|
|
|
264
164
|
InputValidator.validatePositiveInteger(args.length, 'length');
|
|
265
165
|
const scale = ctx.theory.generateScale(args.root, args.scale);
|
|
266
166
|
const melody = ctx.generator.generateMelody(scale, args.length || 8);
|
|
267
|
-
await appendOrSet(melody, ctx, sid);
|
|
268
|
-
return `Generated melody in ${args.root} ${args.scale}
|
|
167
|
+
const written = await appendOrSet(melody, ctx, sid);
|
|
168
|
+
return withStashNotice(`Generated melody in ${args.root} ${args.scale}`, written);
|
|
269
169
|
}
|
|
270
170
|
async function doFill(args, ctx, sid) {
|
|
271
171
|
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
272
172
|
if (args.bars !== undefined)
|
|
273
173
|
InputValidator.validatePositiveInteger(args.bars, 'bars');
|
|
274
174
|
const fill = ctx.generator.generateFill(args.style, args.bars || 1);
|
|
275
|
-
await appendOrSet(fill, ctx, sid);
|
|
276
|
-
return `Generated ${args.bars || 1} bar fill
|
|
175
|
+
const written = await appendOrSet(fill, ctx, sid);
|
|
176
|
+
return withStashNotice(`Generated ${args.bars || 1} bar fill`, written);
|
|
277
177
|
}
|
|
278
178
|
export async function execute(name, args, ctx) {
|
|
279
179
|
const sid = args?.session_id;
|
|
280
180
|
switch (name) {
|
|
281
|
-
case 'generate_pattern': {
|
|
282
|
-
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
283
|
-
if (args.key)
|
|
284
|
-
InputValidator.validateRootNote(args.key);
|
|
285
|
-
if (args.bpm !== undefined)
|
|
286
|
-
InputValidator.validateBPM(args.bpm);
|
|
287
|
-
const generated = ctx.generator.generateCompletePattern(args.style, args.key || 'C', args.bpm || 120);
|
|
288
|
-
await ctx.writePatternSafe(generated, sid);
|
|
289
|
-
if (args.auto_play && (sid || ctx.isInitialized())) {
|
|
290
|
-
await ctx.getController(sid).play();
|
|
291
|
-
return `Generated ${args.style} pattern. Playing.`;
|
|
292
|
-
}
|
|
293
|
-
return `Generated ${args.style} pattern`;
|
|
294
|
-
}
|
|
295
181
|
case 'generate_part': {
|
|
296
182
|
const role = args?.role;
|
|
297
183
|
switch (role) {
|
|
@@ -303,9 +189,6 @@ export async function execute(name, args, ctx) {
|
|
|
303
189
|
throw new Error(`Invalid role: ${role}. Must be one of: drums, bass, melody, fill`);
|
|
304
190
|
}
|
|
305
191
|
}
|
|
306
|
-
case 'generate_drums': return await doDrums(args, ctx, sid);
|
|
307
|
-
case 'generate_bassline': return await doBassline(args, ctx, sid);
|
|
308
|
-
case 'generate_melody': return await doMelody(args, ctx, sid);
|
|
309
192
|
case 'music_theory': {
|
|
310
193
|
const q = args?.query;
|
|
311
194
|
if (q !== 'scale' && q !== 'chord_progression') {
|
|
@@ -313,8 +196,6 @@ export async function execute(name, args, ctx) {
|
|
|
313
196
|
}
|
|
314
197
|
return q === 'scale' ? doScale(args, ctx) : await doChordProgression(args, ctx, sid);
|
|
315
198
|
}
|
|
316
|
-
case 'generate_scale': return doScale(args, ctx);
|
|
317
|
-
case 'generate_chord_progression': return await doChordProgression(args, ctx, sid);
|
|
318
199
|
case 'generate_rhythm': {
|
|
319
200
|
const t = args?.type;
|
|
320
201
|
if (t !== 'euclidean' && t !== 'polyrhythm') {
|
|
@@ -324,9 +205,6 @@ export async function execute(name, args, ctx) {
|
|
|
324
205
|
? await doEuclidean(args, ctx, sid)
|
|
325
206
|
: await doPolyrhythm(args, ctx, sid);
|
|
326
207
|
}
|
|
327
|
-
case 'generate_euclidean': return await doEuclidean(args, ctx, sid);
|
|
328
|
-
case 'generate_polyrhythm': return await doPolyrhythm(args, ctx, sid);
|
|
329
|
-
case 'generate_fill': return await doFill(args, ctx, sid);
|
|
330
208
|
default:
|
|
331
209
|
throw new Error(`generate module does not handle tool: ${name}`);
|
|
332
210
|
}
|