@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,126 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* editor domain — pattern editing in the CodeMirror editor.
|
|
3
3
|
*
|
|
4
|
-
* Owns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `get_pattern` stays separate (hot read path).
|
|
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).
|
|
8
7
|
*/
|
|
9
8
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
description: '
|
|
14
|
-
inputSchema: {
|
|
15
|
-
type: 'object',
|
|
16
|
-
properties: {
|
|
17
|
-
pattern: { type: 'string', description: 'Pattern code' },
|
|
18
|
-
auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
|
|
19
|
-
validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' },
|
|
20
|
-
},
|
|
21
|
-
required: ['pattern'],
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
name: 'append',
|
|
26
|
-
description: 'Append code to current pattern',
|
|
27
|
-
inputSchema: {
|
|
28
|
-
type: 'object',
|
|
29
|
-
properties: { code: { type: 'string', description: 'Code to append' } },
|
|
30
|
-
required: ['code'],
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: 'insert',
|
|
35
|
-
description: 'Insert code at specific line',
|
|
36
|
-
inputSchema: {
|
|
37
|
-
type: 'object',
|
|
38
|
-
properties: {
|
|
39
|
-
position: { type: 'number', description: 'Line number' },
|
|
40
|
-
code: { type: 'string', description: 'Code to insert' },
|
|
41
|
-
},
|
|
42
|
-
required: ['position', 'code'],
|
|
43
|
-
},
|
|
9
|
+
const SESSION_ID_PROP = {
|
|
10
|
+
session_id: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Optional session ID (#108). Omit to use default session.',
|
|
44
13
|
},
|
|
14
|
+
};
|
|
15
|
+
export const tools = [
|
|
45
16
|
{
|
|
46
|
-
name: '
|
|
47
|
-
description: '
|
|
17
|
+
name: 'edit_pattern',
|
|
18
|
+
description: 'Mutate the current session pattern. ' +
|
|
19
|
+
'mode=write replaces the editor contents (default; mirrors the old write tool exactly, including optional pattern validation and auto_play). ' +
|
|
20
|
+
'mode=append concatenates `code` after the current pattern with a newline. ' +
|
|
21
|
+
'mode=insert places `code` at the given line `position`. ' +
|
|
22
|
+
'mode=replace runs a single string-replace from `search` to `replace`. ' +
|
|
23
|
+
'mode=clear empties the editor. ' +
|
|
24
|
+
'Example: edit_pattern({ mode: "write", pattern: "s(\\"bd\\")", auto_play: true }). ' +
|
|
25
|
+
'For reading the editor without mutating it use get_pattern; for the on-disk pattern catalog use pattern_store.',
|
|
48
26
|
inputSchema: {
|
|
49
27
|
type: 'object',
|
|
50
28
|
properties: {
|
|
51
|
-
|
|
52
|
-
|
|
29
|
+
mode: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: ['write', 'append', 'insert', 'replace', 'clear'],
|
|
32
|
+
description: 'Which edit operation to perform (default: write)',
|
|
33
|
+
},
|
|
34
|
+
pattern: { type: 'string', description: 'Pattern code (mode=write)' },
|
|
35
|
+
code: { type: 'string', description: 'Code to append/insert (mode=append/insert)' },
|
|
36
|
+
position: { type: 'number', description: 'Line number (mode=insert)' },
|
|
37
|
+
search: { type: 'string', description: 'Text to replace (mode=replace)' },
|
|
38
|
+
replace: { type: 'string', description: 'Replacement text (mode=replace)' },
|
|
39
|
+
auto_play: { type: 'boolean', description: 'Start playback after write (mode=write only, default: false)' },
|
|
40
|
+
validate: { type: 'boolean', description: 'Validate pattern before write (mode=write only, default: true)' },
|
|
41
|
+
...SESSION_ID_PROP,
|
|
53
42
|
},
|
|
54
|
-
required: ['search', 'replace'],
|
|
55
43
|
},
|
|
56
44
|
},
|
|
57
|
-
{
|
|
58
|
-
name: 'clear',
|
|
59
|
-
description: 'Clear the editor',
|
|
60
|
-
inputSchema: { type: 'object', properties: {} },
|
|
61
|
-
},
|
|
62
45
|
{
|
|
63
46
|
name: 'get_pattern',
|
|
64
47
|
description: 'Get current pattern code',
|
|
65
|
-
inputSchema: { type: 'object', properties: {} },
|
|
48
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
66
49
|
},
|
|
67
50
|
];
|
|
68
51
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
ctx.logger.warn('Pattern validation threw error, continuing with write');
|
|
89
|
-
}
|
|
52
|
+
async function doWrite(args, ctx, sid) {
|
|
53
|
+
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true);
|
|
54
|
+
const controller = ctx.getController(sid);
|
|
55
|
+
// Validate pattern if requested (default: true) — issue #40
|
|
56
|
+
if (args.validate !== false && typeof controller.validatePattern === 'function') {
|
|
57
|
+
try {
|
|
58
|
+
const validation = await controller.validatePattern(args.pattern);
|
|
59
|
+
if (validation && !validation.valid) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
errors: validation.errors,
|
|
63
|
+
warnings: validation.warnings,
|
|
64
|
+
suggestions: validation.suggestions,
|
|
65
|
+
message: `Pattern validation failed: ${validation.errors.join('; ')}`,
|
|
66
|
+
};
|
|
90
67
|
}
|
|
91
|
-
const writeResult = await ctx.writePatternSafe(args.pattern);
|
|
92
|
-
// Auto-play if requested — issue #38
|
|
93
|
-
if (args.auto_play && ctx.isInitialized()) {
|
|
94
|
-
await ctx.controller.play();
|
|
95
|
-
return `${writeResult}. Playing.`;
|
|
96
|
-
}
|
|
97
|
-
return writeResult;
|
|
98
|
-
}
|
|
99
|
-
case 'append': {
|
|
100
|
-
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
101
|
-
const current = await ctx.getCurrentPatternSafe();
|
|
102
|
-
return await ctx.writePatternSafe(current + '\n' + args.code);
|
|
103
68
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
107
|
-
const lines = (await ctx.getCurrentPatternSafe()).split('\n');
|
|
108
|
-
lines.splice(args.position, 0, args.code);
|
|
109
|
-
return await ctx.writePatternSafe(lines.join('\n'));
|
|
69
|
+
catch {
|
|
70
|
+
ctx.logger.warn('Pattern validation threw error, continuing with write');
|
|
110
71
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
72
|
+
}
|
|
73
|
+
const writeResult = await ctx.writePatternSafe(args.pattern, sid);
|
|
74
|
+
// Auto-play if requested — issue #38
|
|
75
|
+
if (args.auto_play) {
|
|
76
|
+
await controller.play();
|
|
77
|
+
return `${writeResult}. Playing.`;
|
|
78
|
+
}
|
|
79
|
+
return writeResult;
|
|
80
|
+
}
|
|
81
|
+
async function doAppend(args, ctx, sid) {
|
|
82
|
+
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
83
|
+
const current = await ctx.getCurrentPatternSafe(sid);
|
|
84
|
+
return await ctx.writePatternSafe(current + '\n' + args.code, sid);
|
|
85
|
+
}
|
|
86
|
+
async function doInsert(args, ctx, sid) {
|
|
87
|
+
InputValidator.validatePositiveInteger(args.position, 'position');
|
|
88
|
+
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
89
|
+
const lines = (await ctx.getCurrentPatternSafe(sid)).split('\n');
|
|
90
|
+
lines.splice(args.position, 0, args.code);
|
|
91
|
+
return await ctx.writePatternSafe(lines.join('\n'), sid);
|
|
92
|
+
}
|
|
93
|
+
async function doReplace(args, ctx, sid) {
|
|
94
|
+
InputValidator.validateStringLength(args.search, 'search', 1000, true);
|
|
95
|
+
InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
|
|
96
|
+
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
97
|
+
// Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
|
|
98
|
+
const safeReplacement = args.replace.replace(/\$/g, '$$$$');
|
|
99
|
+
const replaced = pattern.replace(args.search, safeReplacement);
|
|
100
|
+
return await ctx.writePatternSafe(replaced, sid);
|
|
101
|
+
}
|
|
102
|
+
async function doClear(ctx, sid) {
|
|
103
|
+
return await ctx.writePatternSafe('', sid);
|
|
104
|
+
}
|
|
105
|
+
export async function execute(name, args, ctx) {
|
|
106
|
+
const sid = args?.session_id;
|
|
107
|
+
if (!sid && !ctx.isInitialized()) {
|
|
108
|
+
return 'Browser not initialized. Run init first.';
|
|
109
|
+
}
|
|
110
|
+
switch (name) {
|
|
111
|
+
case 'edit_pattern': {
|
|
112
|
+
const mode = args?.mode ?? 'write';
|
|
113
|
+
switch (mode) {
|
|
114
|
+
case 'write': return await doWrite(args, ctx, sid);
|
|
115
|
+
case 'append': return await doAppend(args, ctx, sid);
|
|
116
|
+
case 'insert': return await doInsert(args, ctx, sid);
|
|
117
|
+
case 'replace': return await doReplace(args, ctx, sid);
|
|
118
|
+
case 'clear': return await doClear(ctx, sid);
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`Invalid mode: ${mode}. Must be one of: write, append, insert, replace, clear`);
|
|
121
|
+
}
|
|
119
122
|
}
|
|
120
|
-
case 'clear':
|
|
121
|
-
return await ctx.writePatternSafe('');
|
|
122
123
|
case 'get_pattern':
|
|
123
|
-
return await ctx.getCurrentPatternSafe();
|
|
124
|
+
return await ctx.getCurrentPatternSafe(sid);
|
|
124
125
|
default:
|
|
125
126
|
throw new Error(`editor module does not handle tool: ${name}`);
|
|
126
127
|
}
|
|
@@ -1,215 +1,186 @@
|
|
|
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
|
*/
|
|
15
12
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
description: '
|
|
20
|
-
inputSchema: {
|
|
21
|
-
type: 'object',
|
|
22
|
-
properties: {
|
|
23
|
-
style: { type: 'string', description: 'Music style (techno/house/dnb/ambient/etc)' },
|
|
24
|
-
key: { type: 'string', description: 'Musical key' },
|
|
25
|
-
bpm: { type: 'number', description: 'Tempo in BPM' },
|
|
26
|
-
auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' },
|
|
27
|
-
},
|
|
28
|
-
required: ['style'],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'generate_drums',
|
|
33
|
-
description: 'Generate drum pattern',
|
|
34
|
-
inputSchema: {
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
style: { type: 'string', description: 'Drum style' },
|
|
38
|
-
complexity: { type: 'number', description: 'Complexity (0-1)' },
|
|
39
|
-
},
|
|
40
|
-
required: ['style'],
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: 'generate_bassline',
|
|
45
|
-
description: 'Generate bassline',
|
|
46
|
-
inputSchema: {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: {
|
|
49
|
-
key: { type: 'string', description: 'Musical key' },
|
|
50
|
-
style: { type: 'string', description: 'Bass style' },
|
|
51
|
-
},
|
|
52
|
-
required: ['key', 'style'],
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'generate_melody',
|
|
57
|
-
description: 'Generate melody from scale',
|
|
58
|
-
inputSchema: {
|
|
59
|
-
type: 'object',
|
|
60
|
-
properties: {
|
|
61
|
-
scale: { type: 'string', description: 'Scale name' },
|
|
62
|
-
root: { type: 'string', description: 'Root note' },
|
|
63
|
-
length: { type: 'number', description: 'Number of notes' },
|
|
64
|
-
},
|
|
65
|
-
required: ['scale', 'root'],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'generate_scale',
|
|
70
|
-
description: 'Generate scale notes',
|
|
71
|
-
inputSchema: {
|
|
72
|
-
type: 'object',
|
|
73
|
-
properties: {
|
|
74
|
-
root: { type: 'string', description: 'Root note' },
|
|
75
|
-
scale: { type: 'string', description: 'Scale type' },
|
|
76
|
-
},
|
|
77
|
-
required: ['root', 'scale'],
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'generate_chord_progression',
|
|
82
|
-
description: 'Generate chord progression',
|
|
83
|
-
inputSchema: {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
key: { type: 'string', description: 'Key' },
|
|
87
|
-
style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' },
|
|
88
|
-
},
|
|
89
|
-
required: ['key', 'style'],
|
|
90
|
-
},
|
|
13
|
+
const SESSION_ID_PROP = {
|
|
14
|
+
session_id: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Optional session ID (#108). Omit to use default session.',
|
|
91
17
|
},
|
|
18
|
+
};
|
|
19
|
+
export const tools = [
|
|
92
20
|
{
|
|
93
|
-
name: '
|
|
94
|
-
description: 'Generate
|
|
21
|
+
name: 'generate_part',
|
|
22
|
+
description: 'Generate a single instrumental layer and append it to the current session pattern. ' +
|
|
23
|
+
'role=drums takes `style` (e.g. "techno"/"house") and optional `complexity` 0-1. ' +
|
|
24
|
+
'role=bass takes `key` (e.g. "C") + `style`. ' +
|
|
25
|
+
'role=melody takes `root`/`scale` (e.g. C/minor) and optional `length` (notes). ' +
|
|
26
|
+
'role=fill takes `style` and optional `bars`. ' +
|
|
27
|
+
'Example: generate_part({ role: "drums", style: "techno", complexity: 0.7 }). ' +
|
|
28
|
+
'For full compositions use compose; for rhythmic patterns use generate_rhythm; for music-theory queries use music_theory.',
|
|
95
29
|
inputSchema: {
|
|
96
30
|
type: 'object',
|
|
97
31
|
properties: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
32
|
+
role: { type: 'string', enum: ['drums', 'bass', 'melody', 'fill'], description: 'Which part to generate' },
|
|
33
|
+
style: { type: 'string', description: 'role=drums/bass/fill: musical style' },
|
|
34
|
+
complexity: { type: 'number', description: 'role=drums: complexity 0-1 (default 0.5)' },
|
|
35
|
+
key: { type: 'string', description: 'role=bass: musical key' },
|
|
36
|
+
root: { type: 'string', description: 'role=melody: root note' },
|
|
37
|
+
scale: { type: 'string', description: 'role=melody: scale name' },
|
|
38
|
+
length: { type: 'number', description: 'role=melody: number of notes (default 8)' },
|
|
39
|
+
bars: { type: 'number', description: 'role=fill: number of bars (default 1)' },
|
|
40
|
+
...SESSION_ID_PROP,
|
|
101
41
|
},
|
|
102
|
-
required: ['
|
|
42
|
+
required: ['role'],
|
|
103
43
|
},
|
|
104
44
|
},
|
|
105
45
|
{
|
|
106
|
-
name: '
|
|
107
|
-
description: '
|
|
46
|
+
name: 'music_theory',
|
|
47
|
+
description: 'Music-theory queries. ' +
|
|
48
|
+
'query=scale returns the notes of a scale (e.g. "C major scale: C, D, E, F, G, A, B"). ' +
|
|
49
|
+
'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session. ' +
|
|
50
|
+
'Example: music_theory({ query: "scale", root: "C", scale: "major" }). ' +
|
|
51
|
+
'For pattern generation (drums/bass/melody) use generate_part; for rhythmic patterns use generate_rhythm.',
|
|
108
52
|
inputSchema: {
|
|
109
53
|
type: 'object',
|
|
110
54
|
properties: {
|
|
111
|
-
|
|
112
|
-
|
|
55
|
+
query: { type: 'string', enum: ['scale', 'chord_progression'], description: 'Which theory query' },
|
|
56
|
+
root: { type: 'string', description: 'Root note (query=scale; also used as key for chord_progression)' },
|
|
57
|
+
scale: { type: 'string', description: 'Scale type (query=scale)' },
|
|
58
|
+
key: { type: 'string', description: 'Key (query=chord_progression)' },
|
|
59
|
+
style: { type: 'string', description: 'Style (query=chord_progression: pop/jazz/blues/etc)' },
|
|
60
|
+
...SESSION_ID_PROP,
|
|
113
61
|
},
|
|
114
|
-
required: ['
|
|
62
|
+
required: ['query'],
|
|
115
63
|
},
|
|
116
64
|
},
|
|
117
65
|
{
|
|
118
|
-
name: '
|
|
119
|
-
description: 'Generate
|
|
66
|
+
name: 'generate_rhythm',
|
|
67
|
+
description: 'Generate a rhythmic pattern and append it to the current session. ' +
|
|
68
|
+
'type=euclidean produces a Euclidean rhythm with `hits` evenly distributed across `steps` (optional `sound` param, default "bd"). ' +
|
|
69
|
+
'type=polyrhythm overlays multiple sound layers with given pattern numbers. ' +
|
|
70
|
+
'Example: generate_rhythm({ type: "euclidean", hits: 3, steps: 8, sound: "hh" }). ' +
|
|
71
|
+
'For complete patterns (drums/bass/melody) use generate_part; for whole compositions use compose.',
|
|
120
72
|
inputSchema: {
|
|
121
73
|
type: 'object',
|
|
122
74
|
properties: {
|
|
123
|
-
|
|
124
|
-
|
|
75
|
+
type: { type: 'string', enum: ['euclidean', 'polyrhythm'], description: 'Rhythm type' },
|
|
76
|
+
hits: { type: 'number', description: 'type=euclidean: hits count' },
|
|
77
|
+
steps: { type: 'number', description: 'type=euclidean: total steps' },
|
|
78
|
+
sound: { type: 'string', description: 'type=euclidean: sound to use (default bd)' },
|
|
79
|
+
sounds: { type: 'array', items: { type: 'string' }, description: 'type=polyrhythm: sounds per layer' },
|
|
80
|
+
patterns: { type: 'array', items: { type: 'number' }, description: 'type=polyrhythm: pattern numbers per layer' },
|
|
81
|
+
...SESSION_ID_PROP,
|
|
125
82
|
},
|
|
126
|
-
required: ['
|
|
83
|
+
required: ['type'],
|
|
127
84
|
},
|
|
128
85
|
},
|
|
129
86
|
];
|
|
130
87
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
131
|
-
async function appendOrSet(generated, ctx) {
|
|
132
|
-
const current = await ctx.getCurrentPatternSafe();
|
|
88
|
+
async function appendOrSet(generated, ctx, sessionId) {
|
|
89
|
+
const current = await ctx.getCurrentPatternSafe(sessionId);
|
|
133
90
|
const combined = current ? current + '\n' + generated : generated;
|
|
134
|
-
await ctx.writePatternSafe(combined);
|
|
91
|
+
await ctx.writePatternSafe(combined, sessionId);
|
|
92
|
+
}
|
|
93
|
+
function doScale(args, ctx) {
|
|
94
|
+
InputValidator.validateRootNote(args.root);
|
|
95
|
+
InputValidator.validateScaleName(args.scale);
|
|
96
|
+
const notes = ctx.theory.generateScale(args.root, args.scale);
|
|
97
|
+
return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
|
|
98
|
+
}
|
|
99
|
+
async function doChordProgression(args, ctx, sid) {
|
|
100
|
+
InputValidator.validateRootNote(args.key);
|
|
101
|
+
InputValidator.validateChordStyle(args.style);
|
|
102
|
+
const progression = ctx.theory.generateChordProgression(args.key, args.style);
|
|
103
|
+
const chordPattern = ctx.generator.generateChords(progression);
|
|
104
|
+
await appendOrSet(chordPattern, ctx, sid);
|
|
105
|
+
return `Generated ${args.style} progression in ${args.key}: ${progression}`;
|
|
106
|
+
}
|
|
107
|
+
async function doEuclidean(args, ctx, sid) {
|
|
108
|
+
InputValidator.validateEuclidean(args.hits, args.steps);
|
|
109
|
+
if (args.sound)
|
|
110
|
+
InputValidator.validateStringLength(args.sound, 'sound', 100, false);
|
|
111
|
+
const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
|
|
112
|
+
await appendOrSet(euclidean, ctx, sid);
|
|
113
|
+
return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
|
|
114
|
+
}
|
|
115
|
+
async function doPolyrhythm(args, ctx, sid) {
|
|
116
|
+
args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
|
|
117
|
+
args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
|
|
118
|
+
const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
|
|
119
|
+
await appendOrSet(poly, ctx, sid);
|
|
120
|
+
return 'Generated polyrhythm';
|
|
121
|
+
}
|
|
122
|
+
async function doDrums(args, ctx, sid) {
|
|
123
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
124
|
+
if (args.complexity !== undefined)
|
|
125
|
+
InputValidator.validateNormalizedValue(args.complexity, 'complexity');
|
|
126
|
+
const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
|
|
127
|
+
await appendOrSet(drums, ctx, sid);
|
|
128
|
+
return `Generated ${args.style} drums`;
|
|
129
|
+
}
|
|
130
|
+
async function doBassline(args, ctx, sid) {
|
|
131
|
+
InputValidator.validateRootNote(args.key);
|
|
132
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
133
|
+
const bass = ctx.generator.generateBassline(args.key, args.style);
|
|
134
|
+
await appendOrSet(bass, ctx, sid);
|
|
135
|
+
return `Generated ${args.style} bassline in ${args.key}`;
|
|
136
|
+
}
|
|
137
|
+
async function doMelody(args, ctx, sid) {
|
|
138
|
+
InputValidator.validateRootNote(args.root);
|
|
139
|
+
InputValidator.validateScaleName(args.scale);
|
|
140
|
+
if (args.length !== undefined)
|
|
141
|
+
InputValidator.validatePositiveInteger(args.length, 'length');
|
|
142
|
+
const scale = ctx.theory.generateScale(args.root, args.scale);
|
|
143
|
+
const melody = ctx.generator.generateMelody(scale, args.length || 8);
|
|
144
|
+
await appendOrSet(melody, ctx, sid);
|
|
145
|
+
return `Generated melody in ${args.root} ${args.scale}`;
|
|
146
|
+
}
|
|
147
|
+
async function doFill(args, ctx, sid) {
|
|
148
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
149
|
+
if (args.bars !== undefined)
|
|
150
|
+
InputValidator.validatePositiveInteger(args.bars, 'bars');
|
|
151
|
+
const fill = ctx.generator.generateFill(args.style, args.bars || 1);
|
|
152
|
+
await appendOrSet(fill, ctx, sid);
|
|
153
|
+
return `Generated ${args.bars || 1} bar fill`;
|
|
135
154
|
}
|
|
136
155
|
export async function execute(name, args, ctx) {
|
|
156
|
+
const sid = args?.session_id;
|
|
137
157
|
switch (name) {
|
|
138
|
-
case '
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await ctx.controller.play();
|
|
148
|
-
return `Generated ${args.style} pattern. Playing.`;
|
|
158
|
+
case 'generate_part': {
|
|
159
|
+
const role = args?.role;
|
|
160
|
+
switch (role) {
|
|
161
|
+
case 'drums': return await doDrums(args, ctx, sid);
|
|
162
|
+
case 'bass': return await doBassline(args, ctx, sid);
|
|
163
|
+
case 'melody': return await doMelody(args, ctx, sid);
|
|
164
|
+
case 'fill': return await doFill(args, ctx, sid);
|
|
165
|
+
default:
|
|
166
|
+
throw new Error(`Invalid role: ${role}. Must be one of: drums, bass, melody, fill`);
|
|
149
167
|
}
|
|
150
|
-
return `Generated ${args.style} pattern`;
|
|
151
|
-
}
|
|
152
|
-
case 'generate_drums': {
|
|
153
|
-
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
154
|
-
if (args.complexity !== undefined)
|
|
155
|
-
InputValidator.validateNormalizedValue(args.complexity, 'complexity');
|
|
156
|
-
const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
|
|
157
|
-
await appendOrSet(drums, ctx);
|
|
158
|
-
return `Generated ${args.style} drums`;
|
|
159
168
|
}
|
|
160
|
-
case '
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
case 'generate_melody': {
|
|
168
|
-
InputValidator.validateRootNote(args.root);
|
|
169
|
-
InputValidator.validateScaleName(args.scale);
|
|
170
|
-
if (args.length !== undefined)
|
|
171
|
-
InputValidator.validatePositiveInteger(args.length, 'length');
|
|
172
|
-
const scale = ctx.theory.generateScale(args.root, args.scale);
|
|
173
|
-
const melody = ctx.generator.generateMelody(scale, args.length || 8);
|
|
174
|
-
await appendOrSet(melody, ctx);
|
|
175
|
-
return `Generated melody in ${args.root} ${args.scale}`;
|
|
176
|
-
}
|
|
177
|
-
case 'generate_scale': {
|
|
178
|
-
InputValidator.validateRootNote(args.root);
|
|
179
|
-
InputValidator.validateScaleName(args.scale);
|
|
180
|
-
const notes = ctx.theory.generateScale(args.root, args.scale);
|
|
181
|
-
return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
|
|
182
|
-
}
|
|
183
|
-
case 'generate_chord_progression': {
|
|
184
|
-
InputValidator.validateRootNote(args.key);
|
|
185
|
-
InputValidator.validateChordStyle(args.style);
|
|
186
|
-
const progression = ctx.theory.generateChordProgression(args.key, args.style);
|
|
187
|
-
const chordPattern = ctx.generator.generateChords(progression);
|
|
188
|
-
await appendOrSet(chordPattern, ctx);
|
|
189
|
-
return `Generated ${args.style} progression in ${args.key}: ${progression}`;
|
|
190
|
-
}
|
|
191
|
-
case 'generate_euclidean': {
|
|
192
|
-
InputValidator.validateEuclidean(args.hits, args.steps);
|
|
193
|
-
if (args.sound)
|
|
194
|
-
InputValidator.validateStringLength(args.sound, 'sound', 100, false);
|
|
195
|
-
const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
|
|
196
|
-
await appendOrSet(euclidean, ctx);
|
|
197
|
-
return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
|
|
198
|
-
}
|
|
199
|
-
case 'generate_polyrhythm': {
|
|
200
|
-
args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
|
|
201
|
-
args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
|
|
202
|
-
const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
|
|
203
|
-
await appendOrSet(poly, ctx);
|
|
204
|
-
return 'Generated polyrhythm';
|
|
169
|
+
case 'music_theory': {
|
|
170
|
+
const q = args?.query;
|
|
171
|
+
if (q !== 'scale' && q !== 'chord_progression') {
|
|
172
|
+
throw new Error(`Invalid query: ${q}. Must be one of: scale, chord_progression`);
|
|
173
|
+
}
|
|
174
|
+
return q === 'scale' ? doScale(args, ctx) : await doChordProgression(args, ctx, sid);
|
|
205
175
|
}
|
|
206
|
-
case '
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
176
|
+
case 'generate_rhythm': {
|
|
177
|
+
const t = args?.type;
|
|
178
|
+
if (t !== 'euclidean' && t !== 'polyrhythm') {
|
|
179
|
+
throw new Error(`Invalid type: ${t}. Must be one of: euclidean, polyrhythm`);
|
|
180
|
+
}
|
|
181
|
+
return t === 'euclidean'
|
|
182
|
+
? await doEuclidean(args, ctx, sid)
|
|
183
|
+
: await doPolyrhythm(args, ctx, sid);
|
|
213
184
|
}
|
|
214
185
|
default:
|
|
215
186
|
throw new Error(`generate module does not handle tool: ${name}`);
|