@williamzujkowski/live-coding-music-mcp 4.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 +202 -705
- 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 +384 -76
- package/dist/server/tools/ai.js +124 -91
- package/dist/server/tools/analysis.js +168 -59
- package/dist/server/tools/capture.js +135 -24
- package/dist/server/tools/compose.js +26 -26
- package/dist/server/tools/diagnostics.js +20 -3
- package/dist/server/tools/editor.js +60 -6
- package/dist/server/tools/generate.js +38 -15
- package/dist/server/tools/history.js +30 -8
- package/dist/server/tools/playback.js +2 -1
- package/dist/server/tools/session.js +38 -6
- package/dist/server/tools/storage.js +179 -8
- package/dist/server/tools/transform.js +127 -43
- 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
package/dist/server/tools/ai.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* pattern (tempo/key/style detection, layer detection, merge logic)
|
|
9
9
|
* and that are used only here. They live in this module.
|
|
10
10
|
*/
|
|
11
|
+
import { empty, err, withStashField } from './types.js';
|
|
12
|
+
import { AiRateLimitError } from '../../services/ai/AiTransport.js';
|
|
11
13
|
import { Logger } from '../../utils/Logger.js';
|
|
14
|
+
import { lookup } from '../../utils/TableLookup.js';
|
|
15
|
+
import { declaredBpm } from '../../utils/Tempo.js';
|
|
12
16
|
const logger = new Logger();
|
|
13
17
|
const SESSION_ID_PROP = {
|
|
14
18
|
session_id: {
|
|
@@ -25,7 +29,8 @@ export const tools = [
|
|
|
25
29
|
'task=jam generates a fresh layer (drums/bass/melody/pad/texture) and merges it into the current pattern, then auto-plays. ' +
|
|
26
30
|
'All three share Gemini auth + rate limiting. ' +
|
|
27
31
|
'Example: ai_assist({ task: "jam", layer: "bass" }). ' +
|
|
28
|
-
'
|
|
32
|
+
'Needs GEMINI_API_KEY, or a logged-in claude/agy/codex CLI on PATH (#252). ' +
|
|
33
|
+
'For non-AI pattern generation use generate_part; for full compositions use compose.',
|
|
29
34
|
inputSchema: {
|
|
30
35
|
type: 'object',
|
|
31
36
|
properties: {
|
|
@@ -78,7 +83,8 @@ async function getPatternFeedback(includeAudio, style, ctx, sid) {
|
|
|
78
83
|
if (!ctx.geminiService.isAvailable()) {
|
|
79
84
|
return {
|
|
80
85
|
gemini_available: false,
|
|
81
|
-
error: '
|
|
86
|
+
error: 'No AI transport available. Set GEMINI_API_KEY, or log in to one of the '
|
|
87
|
+
+ 'claude/agy/codex CLIs on PATH (#252).',
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
@@ -92,19 +98,40 @@ async function getPatternFeedback(includeAudio, style, ctx, sid) {
|
|
|
92
98
|
catch (error) {
|
|
93
99
|
const message = error instanceof Error ? error.message : String(error);
|
|
94
100
|
logger.error('Pattern feedback failed', { error: message });
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
// By type, not by phrase. `AiRateLimitError` is what the limiter
|
|
102
|
+
// throws; matching the words meant a reworded message would fall
|
|
103
|
+
// through to "Pattern analysis failed" and lose the one thing a
|
|
104
|
+
// caller can act on — that waiting works (#380, #392).
|
|
105
|
+
if (error instanceof AiRateLimitError) {
|
|
106
|
+
return err('transient', message, { isRetryable: true });
|
|
97
107
|
}
|
|
98
108
|
result.error = `Pattern analysis failed: ${message}`;
|
|
99
109
|
}
|
|
100
110
|
if (includeAudio && (sid || ctx.isInitialized())) {
|
|
101
111
|
try {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
|
|
112
|
+
const sample = await captureAudioSampleForFeedback(ctx, sid);
|
|
113
|
+
if (sample === null) {
|
|
114
|
+
logger.warn('Audio capture returned no data');
|
|
115
|
+
if (!result.error)
|
|
116
|
+
result.error = 'Audio capture returned no data.';
|
|
117
|
+
}
|
|
118
|
+
else if (sample.silent) {
|
|
119
|
+
// Sending silence to Gemini does not fail — it returns a confident
|
|
120
|
+
// mood/style/energy for audio that is not there. Feedback silently
|
|
121
|
+
// decoupled from the audio is worse than no feedback, so refuse.
|
|
122
|
+
result.audio_analysis_skipped =
|
|
123
|
+
'Captured audio was silent, so no feedback was requested. ' +
|
|
124
|
+
'Is a pattern playing? Call playback({ action: "play" }) first.';
|
|
125
|
+
logger.warn('Skipping Gemini audio analysis: capture was silent', {
|
|
126
|
+
peak: sample.peak,
|
|
127
|
+
});
|
|
105
128
|
}
|
|
106
129
|
else {
|
|
107
|
-
|
|
130
|
+
// Measurements, not the waveform. Every installed CLI answers
|
|
131
|
+
// "CANNOT DECODE AUDIO", and agy will confabulate detailed
|
|
132
|
+
// analysis of audio it never examined rather than admit it.
|
|
133
|
+
result.audio_analysis = await ctx.geminiService.analyzeAudioMeasurements(sample.measurements, pattern, { style, duration: FEEDBACK_SAMPLE_MS / 1000 });
|
|
134
|
+
result.audio_levels = { peak: sample.peak, rms: sample.rms };
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
catch (error) {
|
|
@@ -121,6 +148,8 @@ async function getPatternFeedback(includeAudio, style, ctx, sid) {
|
|
|
121
148
|
}
|
|
122
149
|
return result;
|
|
123
150
|
}
|
|
151
|
+
/** Seconds of audio sent to Gemini for feedback. */
|
|
152
|
+
const FEEDBACK_SAMPLE_MS = 5000;
|
|
124
153
|
/**
|
|
125
154
|
* Captures a brief audio sample for Gemini analysis. Different from the
|
|
126
155
|
* `capture_audio_sample` tool — this one connects directly to the
|
|
@@ -133,62 +162,31 @@ async function captureAudioSampleForFeedback(ctx, sid) {
|
|
|
133
162
|
logger.warn('Cannot capture audio: controller page not available');
|
|
134
163
|
return null;
|
|
135
164
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const audioCtx = analyzer.analyser.context;
|
|
146
|
-
const destination = audioCtx.createMediaStreamDestination();
|
|
147
|
-
analyzer.analyser.connect(destination);
|
|
148
|
-
const mediaRecorder = new MediaRecorder(destination.stream, { mimeType: 'audio/webm;codecs=opus' });
|
|
149
|
-
const chunks = [];
|
|
150
|
-
mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0)
|
|
151
|
-
chunks.push(e.data); };
|
|
152
|
-
mediaRecorder.onstop = async () => {
|
|
153
|
-
try {
|
|
154
|
-
analyzer.analyser.disconnect(destination);
|
|
155
|
-
}
|
|
156
|
-
catch { /* already disconnected */ }
|
|
157
|
-
if (chunks.length === 0) {
|
|
158
|
-
resolve(null);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const blob = new Blob(chunks, { type: 'audio/webm' });
|
|
162
|
-
const reader = new FileReader();
|
|
163
|
-
reader.onloadend = () => {
|
|
164
|
-
const base64 = reader.result;
|
|
165
|
-
resolve(base64.split(',')[1] || null);
|
|
166
|
-
};
|
|
167
|
-
reader.onerror = () => resolve(null);
|
|
168
|
-
reader.readAsDataURL(blob);
|
|
169
|
-
};
|
|
170
|
-
mediaRecorder.start();
|
|
171
|
-
setTimeout(() => { if (mediaRecorder.state === 'recording')
|
|
172
|
-
mediaRecorder.stop(); }, 5000);
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
resolve(null);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
if (!audioData)
|
|
180
|
-
return null;
|
|
181
|
-
const binaryString = atob(audioData);
|
|
182
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
183
|
-
for (let i = 0; i < binaryString.length; i++)
|
|
184
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
185
|
-
return new Blob([bytes], { type: 'audio/webm' });
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
-
logger.error('Audio capture failed', { error: message });
|
|
165
|
+
// Ensures the recorder is injected for this session before exporting.
|
|
166
|
+
await ctx.getAudioCaptureService(sid);
|
|
167
|
+
const result = await ctx.audioExportService.exportAudio(page, {
|
|
168
|
+
duration: FEEDBACK_SAMPLE_MS,
|
|
169
|
+
format: 'wav',
|
|
170
|
+
output: 'base64',
|
|
171
|
+
});
|
|
172
|
+
if (!result.success || result.audio === undefined) {
|
|
173
|
+
logger.warn('Audio capture returned no data', { error: result.error });
|
|
190
174
|
return null;
|
|
191
175
|
}
|
|
176
|
+
// The bytes are deliberately discarded. No model in play can decode
|
|
177
|
+
// audio, so what gets sent is the measurement set, not the waveform.
|
|
178
|
+
return {
|
|
179
|
+
measurements: {
|
|
180
|
+
durationMs: result.duration,
|
|
181
|
+
peak: result.peak,
|
|
182
|
+
rms: result.rms,
|
|
183
|
+
sampleRate: result.sampleRate,
|
|
184
|
+
channels: result.channels,
|
|
185
|
+
},
|
|
186
|
+
peak: result.peak,
|
|
187
|
+
rms: result.rms,
|
|
188
|
+
silent: result.silent === true,
|
|
189
|
+
};
|
|
192
190
|
}
|
|
193
191
|
// ---------------------------------------------------------------------------
|
|
194
192
|
// suggest_pattern_from_audio
|
|
@@ -198,7 +196,10 @@ async function suggestPatternFromAudio(style, role, ctx, sid) {
|
|
|
198
196
|
return { error: 'Browser not initialized. Run init and play a pattern first.' };
|
|
199
197
|
}
|
|
200
198
|
if (!ctx.geminiService.isAvailable()) {
|
|
201
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
error: 'No AI transport available. Set GEMINI_API_KEY, or log in to one of the '
|
|
201
|
+
+ 'claude/agy/codex CLIs on PATH (#252).',
|
|
202
|
+
};
|
|
202
203
|
}
|
|
203
204
|
const controller = ctx.getController(sid);
|
|
204
205
|
let bpm = 0, key = 'C', scale = 'major';
|
|
@@ -222,7 +223,7 @@ async function suggestPatternFromAudio(style, role, ctx, sid) {
|
|
|
222
223
|
melody: 'a melodic line that harmonizes with the key',
|
|
223
224
|
percussion: 'a percussion layer that adds rhythmic interest',
|
|
224
225
|
};
|
|
225
|
-
const roleText = roleDesc
|
|
226
|
+
const roleText = lookup(roleDesc, role, roleDesc['complement']);
|
|
226
227
|
const styleText = style ? ` in a ${style} style` : '';
|
|
227
228
|
const tempoText = bpm > 0 ? `Detected tempo: ${bpm} BPM. ` : '';
|
|
228
229
|
const keyText = `Detected key: ${key} ${scale}. `;
|
|
@@ -240,10 +241,17 @@ Example patterns:
|
|
|
240
241
|
try {
|
|
241
242
|
const geminiResponse = await ctx.geminiService.suggestVariations(prompt, style);
|
|
242
243
|
if (!geminiResponse || geminiResponse.length === 0) {
|
|
243
|
-
|
|
244
|
+
// The model answered; it just had nothing to offer. Returning a
|
|
245
|
+
// bare `error` key made `isFailureShaped` convert this into an
|
|
246
|
+
// err() envelope, so a working call was reported as a failure —
|
|
247
|
+
// the same bug as #274, pointing the other way (#288).
|
|
248
|
+
return empty({
|
|
249
|
+
suggestions: [],
|
|
250
|
+
message: 'Gemini returned no pattern suggestions.',
|
|
251
|
+
});
|
|
244
252
|
}
|
|
245
253
|
const suggestedPattern = geminiResponse[0].code;
|
|
246
|
-
const validation = ctx.strudelEngine.validate(suggestedPattern);
|
|
254
|
+
const validation = await ctx.strudelEngine.validate(suggestedPattern);
|
|
247
255
|
return {
|
|
248
256
|
suggested_pattern: suggestedPattern,
|
|
249
257
|
analysis: { bpm, key, scale },
|
|
@@ -293,7 +301,7 @@ async function jamWith(layer, styleHint, autoPlay = true, ctx, sid) {
|
|
|
293
301
|
}
|
|
294
302
|
let newLayer;
|
|
295
303
|
try {
|
|
296
|
-
newLayer = generateComplementaryLayer(layer, key,
|
|
304
|
+
newLayer = generateComplementaryLayer(layer, key, detectedStyle, existingLayers, ctx);
|
|
297
305
|
}
|
|
298
306
|
catch (error) {
|
|
299
307
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -308,18 +316,18 @@ async function jamWith(layer, styleHint, autoPlay = true, ctx, sid) {
|
|
|
308
316
|
}
|
|
309
317
|
const mergedPattern = mergeLayerIntoPattern(currentPattern, newLayer, layer);
|
|
310
318
|
try {
|
|
311
|
-
await ctx.writePatternSafe(mergedPattern, sid);
|
|
319
|
+
const written = await ctx.writePatternSafe(mergedPattern, sid);
|
|
312
320
|
if (autoPlay && (sid || ctx.isInitialized())) {
|
|
313
321
|
await ctx.getController(sid).play();
|
|
314
322
|
}
|
|
315
|
-
return {
|
|
323
|
+
return withStashField({
|
|
316
324
|
success: true,
|
|
317
325
|
message: `Added ${layer} layer${styleHint ? ` (${styleHint} style)` : ''} to jam with your pattern`,
|
|
318
326
|
layer,
|
|
319
327
|
detected: { tempo, key, existingLayers },
|
|
320
328
|
newLayer,
|
|
321
329
|
pattern: mergedPattern.substring(0, 300) + (mergedPattern.length > 300 ? '...' : ''),
|
|
322
|
-
};
|
|
330
|
+
}, written);
|
|
323
331
|
}
|
|
324
332
|
catch (error) {
|
|
325
333
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -333,20 +341,29 @@ async function jamWith(layer, styleHint, autoPlay = true, ctx, sid) {
|
|
|
333
341
|
};
|
|
334
342
|
}
|
|
335
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* The tempo a pattern's author meant, for generating a layer to sit on
|
|
346
|
+
* top of it.
|
|
347
|
+
*
|
|
348
|
+
* Uses the canonical parser rather than a regex of its own. The regex it
|
|
349
|
+
* had required a bare `setcpm(<n>)`, so it matched nothing the generator
|
|
350
|
+
* writes — every generated pattern is `setcpm(130/4)` — and fell through
|
|
351
|
+
* to guessing from words in the text, which is how `jam_with` came to
|
|
352
|
+
* read a tempo off a comment (#397).
|
|
353
|
+
*
|
|
354
|
+
* `declaredBpm`, not `impliedBpm`: this reads patterns written by someone
|
|
355
|
+
* else, and only the number they wrote is knowable. Working out what one
|
|
356
|
+
* will actually sound like takes an assumption about beats per cycle
|
|
357
|
+
* that holds for what this project generates and not for an arbitrary
|
|
358
|
+
* hand-written pattern.
|
|
359
|
+
*/
|
|
336
360
|
function detectTempoFromPattern(pattern) {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
return Math.round(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const cpsMatch = pattern.match(/setcps\s*\(\s*(\d+(?:\.\d+)?)\s*(?:\/\s*60)?\s*\)/i);
|
|
344
|
-
if (cpsMatch) {
|
|
345
|
-
const cps = parseFloat(cpsMatch[1]);
|
|
346
|
-
if (pattern.includes(`setcps(${cpsMatch[1]}/60`))
|
|
347
|
-
return Math.round(cps);
|
|
348
|
-
return Math.round(cps * 60);
|
|
349
|
-
}
|
|
361
|
+
const declared = declaredBpm(pattern);
|
|
362
|
+
if (declared !== undefined)
|
|
363
|
+
return Math.round(declared);
|
|
364
|
+
// No tempo call at all. Guessing from genre words is a poor answer,
|
|
365
|
+
// but it beats silently assuming 120 for a dnb pattern — and a real
|
|
366
|
+
// parse always wins over it now, which is the part that was broken.
|
|
350
367
|
if (pattern.toLowerCase().includes('dnb'))
|
|
351
368
|
return 174;
|
|
352
369
|
if (pattern.toLowerCase().includes('techno'))
|
|
@@ -375,10 +392,13 @@ function detectKeyFromPattern(pattern) {
|
|
|
375
392
|
}
|
|
376
393
|
if (allNotes.length === 0)
|
|
377
394
|
return 'C';
|
|
378
|
-
|
|
395
|
+
// Object.create(null): the keys come from pattern text, and both
|
|
396
|
+
// reading and writing 'constructor'/'__proto__' on a plain literal
|
|
397
|
+
// misbehave (#318).
|
|
398
|
+
const noteCounts = Object.create(null);
|
|
379
399
|
for (const note of allNotes) {
|
|
380
400
|
const normalizedNote = note.charAt(0).toUpperCase() + note.slice(1);
|
|
381
|
-
noteCounts[normalizedNote] = (noteCounts
|
|
401
|
+
noteCounts[normalizedNote] = lookup(noteCounts, normalizedNote, 0) + 1;
|
|
382
402
|
}
|
|
383
403
|
let mostCommonNote = 'C';
|
|
384
404
|
let maxCount = 0;
|
|
@@ -430,7 +450,14 @@ function detectStyleFromPattern(pattern, styleHint) {
|
|
|
430
450
|
return 'trap';
|
|
431
451
|
return 'techno';
|
|
432
452
|
}
|
|
433
|
-
function generateComplementaryLayer(layer, key,
|
|
453
|
+
function generateComplementaryLayer(layer, key, style, existingLayers, ctx) {
|
|
454
|
+
// No `tempo` parameter: it was declared, passed, and never read. The
|
|
455
|
+
// merged layer goes inside the host pattern's own stack and inherits
|
|
456
|
+
// its tempo call (see `mergeLayerIntoPattern`), so there is nothing
|
|
457
|
+
// here for a tempo to change. A cross-model reviewer read the dead
|
|
458
|
+
// parameter as evidence that this function retimes the layer, and
|
|
459
|
+
// concluded jam_with would desync by 4x; it cannot. Dead arguments
|
|
460
|
+
// are not free.
|
|
434
461
|
switch (layer) {
|
|
435
462
|
case 'drums':
|
|
436
463
|
if (existingLayers.includes('drums')) {
|
|
@@ -443,7 +470,7 @@ function generateComplementaryLayer(layer, key, tempo, style, existingLayers, ct
|
|
|
443
470
|
'jungle': 's("hh*32").gain(perlin.range(0.2, 0.4)).hpf(4000)',
|
|
444
471
|
'jazz': 's("~ ride ~ ride, ~ ~ ~ hh").gain(0.3).room(0.3)',
|
|
445
472
|
};
|
|
446
|
-
return percOptions
|
|
473
|
+
return lookup(percOptions, style, percOptions['techno']);
|
|
447
474
|
}
|
|
448
475
|
return ctx.generator.generateDrumPattern(style, 0.6);
|
|
449
476
|
case 'bass':
|
|
@@ -451,8 +478,14 @@ function generateComplementaryLayer(layer, key, tempo, style, existingLayers, ct
|
|
|
451
478
|
case 'melody': {
|
|
452
479
|
let scaleName = 'minor';
|
|
453
480
|
let octaveRange = [4, 5];
|
|
481
|
+
// `major`, not `dorian`. The convention names the mode of the ii
|
|
482
|
+
// CHORD; rooted on the key it is the wrong collection, and over
|
|
483
|
+
// the jazz progression this server generates (Dm7 G7 Cmaj7 in C)
|
|
484
|
+
// it puts Eb against Cmaj7's E (#449). `jam_with` cannot see the
|
|
485
|
+
// host pattern's chords, so it follows the generator's choice
|
|
486
|
+
// rather than disagreeing with it.
|
|
454
487
|
if (style === 'jazz') {
|
|
455
|
-
scaleName = '
|
|
488
|
+
scaleName = 'major';
|
|
456
489
|
octaveRange = [3, 5];
|
|
457
490
|
}
|
|
458
491
|
if (style === 'ambient') {
|
|
@@ -468,7 +501,7 @@ function generateComplementaryLayer(layer, key, tempo, style, existingLayers, ct
|
|
|
468
501
|
'trap': '.gain(0.5).room(0.15)', 'jungle': '.delay(0.125).room(0.25).gain(0.55)',
|
|
469
502
|
'jazz': '.room(0.4).gain(0.5)',
|
|
470
503
|
};
|
|
471
|
-
return ctx.generator.generateMelody(scale, 8, octaveRange) + (effects
|
|
504
|
+
return ctx.generator.generateMelody(scale, 8, octaveRange) + lookup(effects, style, '.room(0.3).gain(0.5)');
|
|
472
505
|
}
|
|
473
506
|
case 'pad': {
|
|
474
507
|
const safeKey = key.toLowerCase();
|
|
@@ -483,7 +516,7 @@ function generateComplementaryLayer(layer, key, tempo, style, existingLayers, ct
|
|
|
483
516
|
'jungle': `chord("<${safeKey}m9 ${fourth}m9>/8").dict('ireal').voicing().s("gm_epiano1").gain(0.25).room(0.4).delay(0.25)`,
|
|
484
517
|
'jazz': `chord("<${safeKey}m9 ${fourth}m9 ${fifth}7>/4").dict('ireal').voicing().s("gm_epiano1").gain(0.3).room(0.5)`,
|
|
485
518
|
};
|
|
486
|
-
return padPatterns
|
|
519
|
+
return lookup(padPatterns, style, padPatterns['techno']);
|
|
487
520
|
}
|
|
488
521
|
case 'texture': {
|
|
489
522
|
const texturePatterns = {
|
|
@@ -495,7 +528,7 @@ function generateComplementaryLayer(layer, key, tempo, style, existingLayers, ct
|
|
|
495
528
|
'jungle': `s("breaks125:8").fit().chop(32).gain(0.05).hpf(5000).room(0.4).pan(perlin.range(0.2, 0.8))`,
|
|
496
529
|
'jazz': `s("brush:1").struct("~ 1 ~ 1 ~ 1 ~ ~").gain(0.1).room(0.5)`,
|
|
497
530
|
};
|
|
498
|
-
return texturePatterns
|
|
531
|
+
return lookup(texturePatterns, style, texturePatterns['techno']);
|
|
499
532
|
}
|
|
500
533
|
default:
|
|
501
534
|
throw new Error(`Unknown layer type: ${layer}`);
|