@williamzujkowski/live-coding-music-mcp 1.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/LICENSE +21 -0
- package/README.md +1735 -0
- package/dist/AudioAnalyzer.js +678 -0
- package/dist/PatternStore.js +169 -0
- package/dist/StrudelController.js +676 -0
- package/dist/__tests__/MusicTheory.test.js +245 -0
- package/dist/__tests__/PatternGenerator.test.js +337 -0
- package/dist/__tests__/PatternStore.test.js +153 -0
- package/dist/__tests__/utils/MockPlaywright.js +130 -0
- package/dist/index.js +21 -0
- package/dist/server/EnhancedMCPServer.js +654 -0
- package/dist/server/EnhancedMCPServerFixed.js +2506 -0
- package/dist/server/server.js +580 -0
- package/dist/server/tools/ai.js +530 -0
- package/dist/server/tools/analysis.js +132 -0
- package/dist/server/tools/capture.js +184 -0
- package/dist/server/tools/diagnostics.js +96 -0
- package/dist/server/tools/editor.js +128 -0
- package/dist/server/tools/generate.js +218 -0
- package/dist/server/tools/history.js +196 -0
- package/dist/server/tools/playback.js +41 -0
- package/dist/server/tools/session.js +115 -0
- package/dist/server/tools/storage.js +77 -0
- package/dist/server/tools/transform.js +399 -0
- package/dist/server/tools/types.js +9 -0
- package/dist/services/AudioCaptureService.js +279 -0
- package/dist/services/GeminiService.js +790 -0
- package/dist/services/MIDIExportService.js +412 -0
- package/dist/services/MusicTheory.js +186 -0
- package/dist/services/PatternGenerator.js +647 -0
- package/dist/services/SessionManager.js +316 -0
- package/dist/services/StrudelEngine.js +374 -0
- package/dist/services/__mocks__/StrudelEngine.js +157 -0
- package/dist/types/AudioAnalysis.js +4 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ErrorRecovery.js +248 -0
- package/dist/utils/InputValidator.js +281 -0
- package/dist/utils/Logger.js +19 -0
- package/dist/utils/PatternValidator.js +241 -0
- package/dist/utils/PerformanceMonitor.js +128 -0
- package/package.json +86 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { MusicTheory } from './MusicTheory.js';
|
|
2
|
+
export class PatternGenerator {
|
|
3
|
+
theory = new MusicTheory();
|
|
4
|
+
// Note interval lookup for chord calculations
|
|
5
|
+
notes = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b'];
|
|
6
|
+
/**
|
|
7
|
+
* Get a note at a specific interval from the root
|
|
8
|
+
*/
|
|
9
|
+
getInterval(root, semitones) {
|
|
10
|
+
const normalizedRoot = root.toLowerCase().replace('#', 'b');
|
|
11
|
+
const sharpToFlat = {
|
|
12
|
+
'c#': 'db', 'd#': 'eb', 'f#': 'gb', 'g#': 'ab', 'a#': 'bb'
|
|
13
|
+
};
|
|
14
|
+
const searchRoot = sharpToFlat[normalizedRoot] || normalizedRoot;
|
|
15
|
+
const idx = this.notes.indexOf(searchRoot);
|
|
16
|
+
if (idx === -1)
|
|
17
|
+
return root;
|
|
18
|
+
return this.notes[(idx + semitones) % 12];
|
|
19
|
+
}
|
|
20
|
+
getFourth(root) { return this.getInterval(root, 5); }
|
|
21
|
+
getFifth(root) { return this.getInterval(root, 7); }
|
|
22
|
+
getMinorThird(root) { return this.getInterval(root, 3); }
|
|
23
|
+
getMinorSeventh(root) { return this.getInterval(root, 10); }
|
|
24
|
+
/**
|
|
25
|
+
* Generates a drum pattern for a given style
|
|
26
|
+
* @param style - Music style (e.g., 'techno', 'house', 'dnb', 'ambient')
|
|
27
|
+
* @param complexity - Pattern complexity from 0 to 1 (default: 1)
|
|
28
|
+
* @returns Strudel drum pattern code
|
|
29
|
+
*/
|
|
30
|
+
generateDrumPattern(style, complexity = 1) {
|
|
31
|
+
const patterns = {
|
|
32
|
+
techno: [
|
|
33
|
+
's("bd*4")',
|
|
34
|
+
's("bd*4, ~ cp ~ cp")',
|
|
35
|
+
's("bd*4, ~ cp ~ cp, hh*8")',
|
|
36
|
+
's("bd*4, ~ cp ~ cp, [~ hh]*4, oh ~ ~ ~").swing(0.05)'
|
|
37
|
+
],
|
|
38
|
+
house: [
|
|
39
|
+
's("bd*4, hh*8")',
|
|
40
|
+
's("bd*4, hh*8, ~ cp ~ cp")',
|
|
41
|
+
's("bd*4, [~ hh]*4, ~ cp ~ cp, oh ~ oh ~")',
|
|
42
|
+
's("bd*4, [~ hh]*4, ~ cp ~ cp").every(4, x => x.fast(2))'
|
|
43
|
+
],
|
|
44
|
+
dnb: [
|
|
45
|
+
's("bd ~ ~ bd ~ ~ bd ~, ~ ~ cp ~ ~ cp ~ ~")',
|
|
46
|
+
's("bd ~ ~ [bd bd] ~ ~ bd ~, ~ ~ cp ~ ~ cp ~ ~, hh*16")',
|
|
47
|
+
's("bd ~ ~ [bd bd] ~ ~ bd ~, ~ ~ cp ~ [~ cp] ~ cp ~ ~, hh*16").fast(2)'
|
|
48
|
+
],
|
|
49
|
+
breakbeat: [
|
|
50
|
+
's("bd ~ ~ bd ~ ~ ~ bd, ~ cp ~ ~ cp ~")',
|
|
51
|
+
's("bd ~ ~ bd ~ [~ bd] ~ bd, ~ cp ~ ~ cp ~, hh*8")',
|
|
52
|
+
's("bd ~ [~ bd] bd ~ [~ bd] ~ bd, ~ cp ~ ~ cp [~ cp], hh*8").swing(0.1)'
|
|
53
|
+
],
|
|
54
|
+
trap: [
|
|
55
|
+
's("bd*2, ~ cp ~ cp")',
|
|
56
|
+
's("bd*2, ~ cp ~ cp, hh*8").every(2, x => x.fast(2))',
|
|
57
|
+
's("bd [bd bd] ~ bd, ~ cp ~ cp, hh*16").swing(0.2)'
|
|
58
|
+
],
|
|
59
|
+
jungle: [
|
|
60
|
+
's("bd ~ [~ bd] bd ~ ~ bd ~, ~ cp ~ ~ cp ~").fast(2)',
|
|
61
|
+
's("bd ~ [~ bd] bd ~ [bd bd] bd ~, ~ cp ~ [~ cp] cp ~, hh*32").fast(2)'
|
|
62
|
+
],
|
|
63
|
+
ambient: [
|
|
64
|
+
's("bd ~ ~ ~")',
|
|
65
|
+
's("bd ~ ~ ~, ~ ~ ~ hh:8").room(0.9)',
|
|
66
|
+
's("bd ~ ~ ~, ~ ~ ~ hh:8, ~ ~ oh:5 ~").room(0.9).gain(0.5)'
|
|
67
|
+
],
|
|
68
|
+
experimental: [
|
|
69
|
+
's("bd").euclid(5, 8)',
|
|
70
|
+
's("bd cp").euclid(7, 16)',
|
|
71
|
+
's("bd cp hh").euclid(choose([3, 5, 7]), 16)'
|
|
72
|
+
],
|
|
73
|
+
// === NEW GENRES ===
|
|
74
|
+
intelligent_dnb: [
|
|
75
|
+
// Minimal - sparse break
|
|
76
|
+
`s("breaks165").fit().slice(8, "0 ~ ~ 3 ~ ~ 6 ~").gain(0.5).room(0.3).lpf(5000)`,
|
|
77
|
+
// Medium - classic rolling break
|
|
78
|
+
`s("breaks165").fit().slice(8, "0 0 6 3 0 2 6 7").gain(0.7).room(0.2)`,
|
|
79
|
+
// Complex - layered breaks
|
|
80
|
+
`stack(
|
|
81
|
+
s("breaks165").fit().slice(8, "0 0 6 3 0 2 6 7").gain(0.65).room(0.2),
|
|
82
|
+
s("breaks165").fit().chop(16).gain(0.12).hpf(3000).room(0.4),
|
|
83
|
+
s("bd ~ ~ ~ [~ bd] ~ bd ~").bank("RolandTR909").gain(0.45).lpf(100)
|
|
84
|
+
)`
|
|
85
|
+
],
|
|
86
|
+
trip_hop: [
|
|
87
|
+
// Minimal - sparse and moody
|
|
88
|
+
`s("bd ~ ~ ~, ~ ~ ~ sd:3").room(0.5).gain(0.7)`,
|
|
89
|
+
// Medium - classic trip hop groove
|
|
90
|
+
`stack(
|
|
91
|
+
s("bd ~ ~ bd ~ ~ bd ~").gain(0.8),
|
|
92
|
+
s("~ ~ ~ ~ sd ~ ~ ~").bank("RolandTR808").gain(0.7).room(0.4),
|
|
93
|
+
s("hh*8").gain(0.25).hpf(6000).pan(sine.range(0.3, 0.7))
|
|
94
|
+
)`,
|
|
95
|
+
// Complex - layered with vinyl texture
|
|
96
|
+
`stack(
|
|
97
|
+
s("bd ~ [~ bd] ~ bd ~ ~ ~").gain(0.8),
|
|
98
|
+
s("~ ~ ~ ~ sd ~ ~ sd:2?").bank("RolandTR808").gain(0.7).room(0.5),
|
|
99
|
+
s("hh*8").gain(perlin.range(0.15, 0.3)).hpf(5000),
|
|
100
|
+
s("~ oh ~ ~ ~ oh ~ ~").bank("RolandTR808").gain(0.2).room(0.6)
|
|
101
|
+
).slow(2)`
|
|
102
|
+
],
|
|
103
|
+
boom_bap: [
|
|
104
|
+
// Minimal - hard kick and snare
|
|
105
|
+
`s("bd ~ ~ ~ sd ~ ~ ~, bd ~ ~ ~ sd ~ bd ~")`,
|
|
106
|
+
// Medium - classic boom bap with hats
|
|
107
|
+
`stack(
|
|
108
|
+
s("bd ~ ~ ~ sd ~ ~ ~, bd ~ ~ bd sd ~ bd ~").gain(0.9),
|
|
109
|
+
s("hh*8").gain(0.4).hpf(5000),
|
|
110
|
+
s("~ ~ oh ~ ~ ~ oh ~").gain(0.3)
|
|
111
|
+
).swing(0.08)`,
|
|
112
|
+
// Complex - layered golden era style
|
|
113
|
+
`stack(
|
|
114
|
+
s("bd ~ ~ [~ bd] sd ~ ~ ~, bd ~ ~ bd sd ~ [bd bd] ~").gain(0.9),
|
|
115
|
+
s("~ ~ ~ ~ sd:3 ~ ~ ~").gain(0.3).room(0.2),
|
|
116
|
+
s("[~ hh]*8").gain(0.45).hpf(4000),
|
|
117
|
+
s("~ ~ oh ~ ~ ~ ~ oh:2").gain(0.25).room(0.3)
|
|
118
|
+
).swing(0.1)`
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
// Handle aliases
|
|
122
|
+
const styleMap = {
|
|
123
|
+
'liquid_dnb': 'intelligent_dnb',
|
|
124
|
+
'atmospheric_dnb': 'intelligent_dnb',
|
|
125
|
+
'intelligent': 'intelligent_dnb',
|
|
126
|
+
'liquid': 'intelligent_dnb',
|
|
127
|
+
'atmospheric': 'intelligent_dnb',
|
|
128
|
+
'bukem': 'intelligent_dnb',
|
|
129
|
+
'triphop': 'trip_hop',
|
|
130
|
+
'portishead': 'trip_hop',
|
|
131
|
+
'massive_attack': 'trip_hop',
|
|
132
|
+
'flying_lotus': 'trip_hop',
|
|
133
|
+
'boombap': 'boom_bap',
|
|
134
|
+
'golden_era': 'boom_bap',
|
|
135
|
+
'premier': 'boom_bap',
|
|
136
|
+
'alchemist': 'boom_bap',
|
|
137
|
+
'daringer': 'boom_bap',
|
|
138
|
+
'hitboy': 'boom_bap'
|
|
139
|
+
};
|
|
140
|
+
const resolvedStyle = styleMap[style.toLowerCase()] || style.toLowerCase();
|
|
141
|
+
const stylePatterns = patterns[resolvedStyle] || patterns.techno;
|
|
142
|
+
const index = Math.min(Math.floor(complexity * stylePatterns.length), stylePatterns.length - 1);
|
|
143
|
+
return stylePatterns[index];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Generates a bassline pattern for a given key and style
|
|
147
|
+
* @param key - Musical key (e.g., 'C', 'D', 'F#')
|
|
148
|
+
* @param style - Music style (e.g., 'techno', 'house', 'acid', 'dub')
|
|
149
|
+
* @returns Strudel bassline pattern code
|
|
150
|
+
*/
|
|
151
|
+
generateBassline(key, style) {
|
|
152
|
+
const safeKey = key.toLowerCase();
|
|
153
|
+
const fourth = this.getFourth(safeKey);
|
|
154
|
+
const fifth = this.getFifth(safeKey);
|
|
155
|
+
const minorSeventh = this.getMinorSeventh(safeKey);
|
|
156
|
+
const patterns = {
|
|
157
|
+
techno: `note("${key}2 ${key}2 ${key}2 ${key}2").s("sawtooth").cutoff(800)`,
|
|
158
|
+
house: `note("${key}2 ~ ${key}2 ~").s("sine").gain(0.8)`,
|
|
159
|
+
dnb: `note("${key}1 ~ ~ ${key}2 ~ ${key}1 ~ ~").s("square").cutoff(400)`,
|
|
160
|
+
acid: `note("${key}2 ${key}3 ${key}2 ${this.theory.getNote(key, 3)}2").s("sawtooth").cutoff(sine.range(200, 2000).slow(4))`,
|
|
161
|
+
dub: `note("${key}1 ~ ~ ~ ${key}1 ~ ${this.theory.getNote(key, 5)}1 ~").s("sine:2").room(0.5)`,
|
|
162
|
+
funk: `note("${key}2 ${key}2 ~ ${this.theory.getNote(key, 5)}2 ~ ${key}2 ${this.theory.getNote(key, 7)}2 ~").s("square").cutoff(1200)`,
|
|
163
|
+
jazz: `note("${key}2 ~ ${this.theory.getNote(key, 4)}2 ~ ${this.theory.getNote(key, 7)}2 ~").s("sine").gain(0.7)`,
|
|
164
|
+
ambient: `note("${key}1").s("sine").attack(2).release(4).gain(0.6)`,
|
|
165
|
+
// === NEW GENRES ===
|
|
166
|
+
intelligent_dnb: `note("<${safeKey}1 ~ ${safeKey}1 ~> <${fourth}1 ~ ~ ${fourth}1> <${minorSeventh}0 ~ ${minorSeventh}0 ~> <${this.getMinorThird(safeKey)}1 ~ ~ ~>")
|
|
167
|
+
.s("sine")
|
|
168
|
+
.gain(0.65)
|
|
169
|
+
.lpf(80)
|
|
170
|
+
.attack(0.01)
|
|
171
|
+
.decay(0.2)
|
|
172
|
+
.sustain(0.5)
|
|
173
|
+
.release(0.4)`,
|
|
174
|
+
trip_hop: `note("<${safeKey}1 ~ ~ ${safeKey}1> <~ ${fourth}1 ~ ~> <${fifth}1 ~ ${safeKey}1 ~> <~ ~ ${fourth}1 ~>")
|
|
175
|
+
.s("sine")
|
|
176
|
+
.gain(0.7)
|
|
177
|
+
.lpf(120)
|
|
178
|
+
.attack(0.02)
|
|
179
|
+
.decay(0.3)
|
|
180
|
+
.sustain(0.6)
|
|
181
|
+
.release(0.8)
|
|
182
|
+
.room(0.3)`,
|
|
183
|
+
boom_bap: `note("<${safeKey}1 ~ ${safeKey}1 ~> <${safeKey}1 ${fourth}1 ~ ~> <${safeKey}1 ~ ${fifth}1 ~> <${fourth}1 ~ ${safeKey}1 ~>")
|
|
184
|
+
.s("sawtooth")
|
|
185
|
+
.gain(0.7)
|
|
186
|
+
.lpf(200)
|
|
187
|
+
.attack(0.01)
|
|
188
|
+
.decay(0.15)
|
|
189
|
+
.sustain(0.4)
|
|
190
|
+
.release(0.3)`
|
|
191
|
+
};
|
|
192
|
+
// Handle aliases
|
|
193
|
+
const styleMap = {
|
|
194
|
+
'liquid_dnb': 'intelligent_dnb',
|
|
195
|
+
'atmospheric_dnb': 'intelligent_dnb',
|
|
196
|
+
'intelligent': 'intelligent_dnb',
|
|
197
|
+
'liquid': 'intelligent_dnb',
|
|
198
|
+
'bukem': 'intelligent_dnb',
|
|
199
|
+
'triphop': 'trip_hop',
|
|
200
|
+
'portishead': 'trip_hop',
|
|
201
|
+
'massive_attack': 'trip_hop',
|
|
202
|
+
'boombap': 'boom_bap',
|
|
203
|
+
'golden_era': 'boom_bap',
|
|
204
|
+
'premier': 'boom_bap'
|
|
205
|
+
};
|
|
206
|
+
const resolvedStyle = styleMap[style.toLowerCase()] || style.toLowerCase();
|
|
207
|
+
return patterns[resolvedStyle] || patterns.techno;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Generates a melodic pattern from a scale
|
|
211
|
+
* @param scale - Array of note names to use
|
|
212
|
+
* @param length - Number of notes in the melody (default: 8)
|
|
213
|
+
* @param octaveRange - Tuple of min and max octave numbers (default: [3, 5])
|
|
214
|
+
* @returns Strudel melody pattern code
|
|
215
|
+
*/
|
|
216
|
+
generateMelody(scale, length = 8, octaveRange = [3, 5]) {
|
|
217
|
+
const notes = [];
|
|
218
|
+
let lastNoteIndex = Math.floor(Math.random() * scale.length);
|
|
219
|
+
for (let i = 0; i < length; i++) {
|
|
220
|
+
// Create more musical intervals (prefer steps over leaps)
|
|
221
|
+
const stepProbability = 0.7;
|
|
222
|
+
const useStep = Math.random() < stepProbability;
|
|
223
|
+
let noteIndex;
|
|
224
|
+
if (useStep) {
|
|
225
|
+
// Move by step (1 or 2 scale degrees)
|
|
226
|
+
const step = Math.random() < 0.5 ? 1 : -1;
|
|
227
|
+
noteIndex = (lastNoteIndex + step + scale.length) % scale.length;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Leap to any note
|
|
231
|
+
noteIndex = Math.floor(Math.random() * scale.length);
|
|
232
|
+
}
|
|
233
|
+
const note = scale[noteIndex];
|
|
234
|
+
const octave = octaveRange[0] + Math.floor(Math.random() * (octaveRange[1] - octaveRange[0] + 1));
|
|
235
|
+
notes.push(`${note.toLowerCase()}${octave}`);
|
|
236
|
+
lastNoteIndex = noteIndex;
|
|
237
|
+
}
|
|
238
|
+
return `note("${notes.join(' ')}").s("triangle")`;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Generates a chord pattern from a progression
|
|
242
|
+
* @param progression - Chord progression string
|
|
243
|
+
* @param voicing - Chord voicing style (default: 'triad')
|
|
244
|
+
* @returns Strudel chord pattern code
|
|
245
|
+
*/
|
|
246
|
+
generateChords(progression, voicing = 'triad') {
|
|
247
|
+
const voicings = {
|
|
248
|
+
triad: '.struct("1 ~ ~ ~")',
|
|
249
|
+
seventh: '.struct("1 ~ ~ ~").add(note("7"))',
|
|
250
|
+
sustained: '.attack(0.5).release(2)',
|
|
251
|
+
stab: '.struct("1 ~ 1 ~").release(0.1)',
|
|
252
|
+
pad: '.attack(2).release(4).room(0.8)'
|
|
253
|
+
};
|
|
254
|
+
return `note("<${progression}>").s("sawtooth")${voicings[voicing] || voicings.triad}`;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Generates a complete multi-layer musical pattern
|
|
258
|
+
* @param style - Music style (e.g., 'techno', 'house', 'jazz', 'ambient')
|
|
259
|
+
* @param key - Musical key (default: 'C')
|
|
260
|
+
* @param bpm - Tempo in beats per minute (default: 120)
|
|
261
|
+
* @returns Complete Strudel pattern with drums, bass, chords, and melody
|
|
262
|
+
*/
|
|
263
|
+
generateCompletePattern(style, key = 'C', bpm = 120) {
|
|
264
|
+
// Handle aliases and special genres
|
|
265
|
+
const styleMap = {
|
|
266
|
+
'liquid_dnb': 'intelligent_dnb',
|
|
267
|
+
'atmospheric_dnb': 'intelligent_dnb',
|
|
268
|
+
'intelligent': 'intelligent_dnb',
|
|
269
|
+
'liquid': 'intelligent_dnb',
|
|
270
|
+
'atmospheric': 'intelligent_dnb',
|
|
271
|
+
'bukem': 'intelligent_dnb',
|
|
272
|
+
'triphop': 'trip_hop',
|
|
273
|
+
'portishead': 'trip_hop',
|
|
274
|
+
'massive_attack': 'trip_hop',
|
|
275
|
+
'flying_lotus': 'trip_hop',
|
|
276
|
+
'boombap': 'boom_bap',
|
|
277
|
+
'golden_era': 'boom_bap',
|
|
278
|
+
'premier': 'boom_bap',
|
|
279
|
+
'alchemist': 'boom_bap',
|
|
280
|
+
'daringer': 'boom_bap',
|
|
281
|
+
'hitboy': 'boom_bap'
|
|
282
|
+
};
|
|
283
|
+
const resolvedStyle = styleMap[style.toLowerCase()] || style.toLowerCase();
|
|
284
|
+
// Use specialized generators for new genres
|
|
285
|
+
switch (resolvedStyle) {
|
|
286
|
+
case 'intelligent_dnb':
|
|
287
|
+
return this.generateIntelligentDnB(key, bpm || 170);
|
|
288
|
+
case 'trip_hop':
|
|
289
|
+
return this.generateTripHop(key, bpm || 90);
|
|
290
|
+
case 'boom_bap':
|
|
291
|
+
return this.generateBoomBap(key, bpm || 92);
|
|
292
|
+
}
|
|
293
|
+
// Default generation for other styles
|
|
294
|
+
const drums = this.generateDrumPattern(resolvedStyle, 0.7);
|
|
295
|
+
const bass = this.generateBassline(key, resolvedStyle);
|
|
296
|
+
const scale = this.theory.generateScale(key, resolvedStyle === 'jazz' ? 'dorian' : 'minor');
|
|
297
|
+
const melody = this.generateMelody(scale);
|
|
298
|
+
const chordStyle = resolvedStyle === 'jazz' ? 'jazz' :
|
|
299
|
+
resolvedStyle === 'house' ? 'pop' :
|
|
300
|
+
resolvedStyle === 'techno' ? 'edm' : 'pop';
|
|
301
|
+
const progression = this.theory.generateChordProgression(key, chordStyle);
|
|
302
|
+
const chords = this.generateChords(progression, resolvedStyle === 'ambient' ? 'pad' : 'stab');
|
|
303
|
+
return `// ${resolvedStyle} pattern in ${key} at ${bpm} BPM
|
|
304
|
+
setcpm(${bpm})
|
|
305
|
+
|
|
306
|
+
stack(
|
|
307
|
+
// Drums
|
|
308
|
+
${drums},
|
|
309
|
+
|
|
310
|
+
// Bass
|
|
311
|
+
${bass},
|
|
312
|
+
|
|
313
|
+
// Chords
|
|
314
|
+
${chords}.gain(0.6),
|
|
315
|
+
|
|
316
|
+
// Melody
|
|
317
|
+
${melody}.struct("~ 1 ~ 1 1 ~ 1 ~").delay(0.25).room(0.3).gain(0.5)
|
|
318
|
+
).gain(0.8)`;
|
|
319
|
+
}
|
|
320
|
+
// ========================================
|
|
321
|
+
// INTELLIGENT DNB GENERATOR
|
|
322
|
+
// Style: LTJ Bukem, Good Looking Records
|
|
323
|
+
// ========================================
|
|
324
|
+
generateIntelligentDnB(key, tempo) {
|
|
325
|
+
const safeKey = key.toLowerCase();
|
|
326
|
+
const fourth = this.getFourth(safeKey);
|
|
327
|
+
const seventh = this.getMinorSeventh(safeKey);
|
|
328
|
+
const third = this.getMinorThird(safeKey);
|
|
329
|
+
return `// Intelligent DnB in ${key} at ${tempo} BPM
|
|
330
|
+
// Style: LTJ Bukem / Good Looking Records
|
|
331
|
+
setcps(${tempo}/60)
|
|
332
|
+
samples('github:tidalcycles/dirt-samples')
|
|
333
|
+
|
|
334
|
+
let chords = chord("<${safeKey}m9 ${fourth}m9 ${seventh}m9 ${third}maj7>/4").dict('ireal')
|
|
335
|
+
|
|
336
|
+
stack(
|
|
337
|
+
// === DRUMS - Rolling break with ghost notes ===
|
|
338
|
+
s("breaks165")
|
|
339
|
+
.fit()
|
|
340
|
+
.slice(8, "0 0 6 3 0 2 6 7")
|
|
341
|
+
.gain(0.7)
|
|
342
|
+
.room(0.2),
|
|
343
|
+
|
|
344
|
+
// Ghost break layer
|
|
345
|
+
s("breaks165")
|
|
346
|
+
.fit()
|
|
347
|
+
.chop(16)
|
|
348
|
+
.gain(0.12)
|
|
349
|
+
.hpf(3000)
|
|
350
|
+
.room(0.4)
|
|
351
|
+
.pan(perlin.range(0.3, 0.7)),
|
|
352
|
+
|
|
353
|
+
// Kick reinforcement
|
|
354
|
+
s("bd ~ ~ ~ [~ bd] ~ bd ~")
|
|
355
|
+
.bank("RolandTR909")
|
|
356
|
+
.gain(0.45)
|
|
357
|
+
.lpf(100),
|
|
358
|
+
|
|
359
|
+
// Open hat accents
|
|
360
|
+
s("~ ~ ~ oh ~ ~ oh ~")
|
|
361
|
+
.bank("RolandTR909")
|
|
362
|
+
.gain(0.28)
|
|
363
|
+
.cut(1)
|
|
364
|
+
.room(0.35),
|
|
365
|
+
|
|
366
|
+
// === BASS - Deep sine sub ===
|
|
367
|
+
note("<${safeKey}1 ~ ${safeKey}1 ~> <${fourth}1 ~ ~ ${fourth}1> <${seventh}0 ~ ${seventh}0 ~> <${third}1 ~ ~ ~>")
|
|
368
|
+
.s("sine")
|
|
369
|
+
.gain(0.65)
|
|
370
|
+
.lpf(80)
|
|
371
|
+
.attack(0.01)
|
|
372
|
+
.decay(0.2)
|
|
373
|
+
.sustain(0.5)
|
|
374
|
+
.release(0.4),
|
|
375
|
+
|
|
376
|
+
// === CHORDS - Jazz voicings ===
|
|
377
|
+
chords.voicing()
|
|
378
|
+
.s("gm_epiano1")
|
|
379
|
+
.gain(0.3)
|
|
380
|
+
.room(0.5)
|
|
381
|
+
.delay(0.3)
|
|
382
|
+
.delaytime(0.375)
|
|
383
|
+
.delayfeedback(0.32)
|
|
384
|
+
.lpf(4500),
|
|
385
|
+
|
|
386
|
+
// === ATMOSPHERE - Ethereal strings ===
|
|
387
|
+
chords.voicing()
|
|
388
|
+
.s("gm_strings")
|
|
389
|
+
.attack(2)
|
|
390
|
+
.sustain(0.7)
|
|
391
|
+
.release(3)
|
|
392
|
+
.gain(0.18)
|
|
393
|
+
.room(0.7)
|
|
394
|
+
.size(0.85)
|
|
395
|
+
.lpf(3000)
|
|
396
|
+
.hpf(300)
|
|
397
|
+
)`;
|
|
398
|
+
}
|
|
399
|
+
// ========================================
|
|
400
|
+
// TRIP HOP GENERATOR
|
|
401
|
+
// Style: Portishead, Massive Attack, Flying Lotus
|
|
402
|
+
// ========================================
|
|
403
|
+
generateTripHop(key, tempo) {
|
|
404
|
+
const safeKey = key.toLowerCase();
|
|
405
|
+
const fourth = this.getFourth(safeKey);
|
|
406
|
+
const fifth = this.getFifth(safeKey);
|
|
407
|
+
const seventh = this.getMinorSeventh(safeKey);
|
|
408
|
+
return `// Trip Hop in ${key} at ${tempo} BPM
|
|
409
|
+
// Style: Portishead / Massive Attack
|
|
410
|
+
setcps(${tempo}/60/2)
|
|
411
|
+
|
|
412
|
+
stack(
|
|
413
|
+
// === DRUMS - Slow, heavy, dusty ===
|
|
414
|
+
|
|
415
|
+
// Main beat - half time feel
|
|
416
|
+
s("bd ~ [~ bd] ~ bd ~ ~ ~")
|
|
417
|
+
.gain(0.85)
|
|
418
|
+
.lpf(150)
|
|
419
|
+
.room(0.3),
|
|
420
|
+
|
|
421
|
+
// Snare - heavy with room
|
|
422
|
+
s("~ ~ ~ ~ sd ~ ~ ~")
|
|
423
|
+
.bank("RolandTR808")
|
|
424
|
+
.gain(0.75)
|
|
425
|
+
.room(0.5)
|
|
426
|
+
.size(0.6),
|
|
427
|
+
|
|
428
|
+
// Ghost snares
|
|
429
|
+
s("~ ~ ~ sd:3? ~ ~ sd:2? ~")
|
|
430
|
+
.bank("RolandTR808")
|
|
431
|
+
.gain(0.2)
|
|
432
|
+
.room(0.4),
|
|
433
|
+
|
|
434
|
+
// Dusty hats
|
|
435
|
+
s("hh*8")
|
|
436
|
+
.gain(perlin.range(0.15, 0.3))
|
|
437
|
+
.hpf(5000)
|
|
438
|
+
.room(0.3)
|
|
439
|
+
.pan(perlin.range(0.3, 0.7).slow(4)),
|
|
440
|
+
|
|
441
|
+
// === BASS - Deep and moody ===
|
|
442
|
+
note("<${safeKey}1 ~ ~ ${safeKey}1> <~ ${fourth}1 ~ ~> <${fifth}1 ~ ${safeKey}1 ~> <~ ~ ${fourth}1 ~>")
|
|
443
|
+
.s("sine")
|
|
444
|
+
.gain(0.7)
|
|
445
|
+
.lpf(120)
|
|
446
|
+
.attack(0.02)
|
|
447
|
+
.decay(0.3)
|
|
448
|
+
.sustain(0.6)
|
|
449
|
+
.release(0.8)
|
|
450
|
+
.room(0.3),
|
|
451
|
+
|
|
452
|
+
// === KEYS - Dark Rhodes ===
|
|
453
|
+
chord("<${safeKey}m7 ${fourth}m7 ${fifth}m7 ${seventh}maj7>/4")
|
|
454
|
+
.dict('ireal')
|
|
455
|
+
.voicing()
|
|
456
|
+
.s("gm_epiano1")
|
|
457
|
+
.gain(0.25)
|
|
458
|
+
.room(0.5)
|
|
459
|
+
.lpf(2500)
|
|
460
|
+
.delay(0.4)
|
|
461
|
+
.delaytime(0.666)
|
|
462
|
+
.delayfeedback(0.35),
|
|
463
|
+
|
|
464
|
+
// === ATMOSPHERE - Dark pad ===
|
|
465
|
+
chord("<${safeKey}m7 ${fourth}m7>/8")
|
|
466
|
+
.dict('ireal')
|
|
467
|
+
.voicing()
|
|
468
|
+
.s("sawtooth")
|
|
469
|
+
.attack(3)
|
|
470
|
+
.sustain(0.6)
|
|
471
|
+
.release(4)
|
|
472
|
+
.lpf(sine.range(300, 800).slow(16))
|
|
473
|
+
.lpq(0.5)
|
|
474
|
+
.gain(0.12)
|
|
475
|
+
.room(0.8)
|
|
476
|
+
.size(0.9),
|
|
477
|
+
|
|
478
|
+
// === TEXTURE - Vinyl crackle vibe ===
|
|
479
|
+
s("hh:8*16")
|
|
480
|
+
.gain(0.03)
|
|
481
|
+
.hpf(8000)
|
|
482
|
+
.room(0.2)
|
|
483
|
+
)`;
|
|
484
|
+
}
|
|
485
|
+
// ========================================
|
|
486
|
+
// BOOM BAP GENERATOR
|
|
487
|
+
// Style: DJ Premier, Alchemist, Daringer, Hit-Boy
|
|
488
|
+
// ========================================
|
|
489
|
+
generateBoomBap(key, tempo) {
|
|
490
|
+
const safeKey = key.toLowerCase();
|
|
491
|
+
const fourth = this.getFourth(safeKey);
|
|
492
|
+
const fifth = this.getFifth(safeKey);
|
|
493
|
+
const seventh = this.getMinorSeventh(safeKey);
|
|
494
|
+
const minorThird = this.getMinorThird(safeKey);
|
|
495
|
+
return `// Boom Bap in ${key} at ${tempo} BPM
|
|
496
|
+
// Style: DJ Premier / Alchemist / Daringer
|
|
497
|
+
setcps(${tempo}/60/2)
|
|
498
|
+
|
|
499
|
+
stack(
|
|
500
|
+
// === DRUMS - Hard hitting, swing ===
|
|
501
|
+
|
|
502
|
+
// Main kick pattern
|
|
503
|
+
s("bd ~ ~ [~ bd] sd ~ ~ ~, bd ~ ~ bd sd ~ [bd bd] ~")
|
|
504
|
+
.gain(0.9)
|
|
505
|
+
.lpf(200)
|
|
506
|
+
.room(0.15),
|
|
507
|
+
|
|
508
|
+
// Layered snare for crack
|
|
509
|
+
s("~ ~ ~ ~ sd:3 ~ ~ ~")
|
|
510
|
+
.gain(0.35)
|
|
511
|
+
.hpf(200)
|
|
512
|
+
.room(0.25),
|
|
513
|
+
|
|
514
|
+
// Crispy hats
|
|
515
|
+
s("[~ hh]*8")
|
|
516
|
+
.gain(0.5)
|
|
517
|
+
.hpf(4000)
|
|
518
|
+
.room(0.2),
|
|
519
|
+
|
|
520
|
+
// Open hat accents
|
|
521
|
+
s("~ ~ oh ~ ~ ~ ~ oh:2")
|
|
522
|
+
.gain(0.3)
|
|
523
|
+
.room(0.35)
|
|
524
|
+
.cut(1),
|
|
525
|
+
|
|
526
|
+
// === BASS - Punchy and warm ===
|
|
527
|
+
note("<${safeKey}1 ~ ${safeKey}1 ~> <${safeKey}1 ${fourth}1 ~ ~> <${safeKey}1 ~ ${fifth}1 ~> <${fourth}1 ~ ${safeKey}1 ~>")
|
|
528
|
+
.s("sawtooth")
|
|
529
|
+
.gain(0.65)
|
|
530
|
+
.lpf(180)
|
|
531
|
+
.attack(0.01)
|
|
532
|
+
.decay(0.15)
|
|
533
|
+
.sustain(0.4)
|
|
534
|
+
.release(0.3),
|
|
535
|
+
|
|
536
|
+
// === CHOPS - Soul sample style ===
|
|
537
|
+
chord("<${safeKey}m7 ~ ${fourth}m7 ~> <${seventh}maj7 ~ ${minorThird}maj7 ~>/2")
|
|
538
|
+
.dict('ireal')
|
|
539
|
+
.voicing()
|
|
540
|
+
.s("gm_epiano1")
|
|
541
|
+
.gain(0.4)
|
|
542
|
+
.room(0.3)
|
|
543
|
+
.lpf(3000)
|
|
544
|
+
.hpf(200),
|
|
545
|
+
|
|
546
|
+
// === STRINGS - Cinematic stabs ===
|
|
547
|
+
chord("<${safeKey}m7 ~ ~ ~> <~ ~ ${fourth}m7 ~>/4")
|
|
548
|
+
.dict('ireal')
|
|
549
|
+
.voicing()
|
|
550
|
+
.s("gm_strings")
|
|
551
|
+
.struct("1 ~ ~ ~ ~ 1? ~ ~")
|
|
552
|
+
.attack(0.1)
|
|
553
|
+
.release(0.8)
|
|
554
|
+
.gain(0.25)
|
|
555
|
+
.room(0.4)
|
|
556
|
+
.lpf(4000)
|
|
557
|
+
.hpf(300),
|
|
558
|
+
|
|
559
|
+
// === HORNS - Optional stab ===
|
|
560
|
+
note("<${safeKey}4 ${minorThird}4 ${fourth}4 ${safeKey}4>/8")
|
|
561
|
+
.s("gm_brass")
|
|
562
|
+
.struct("~ ~ ~ ~ 1 ~ ~ ~")
|
|
563
|
+
.gain(0.2)
|
|
564
|
+
.room(0.3)
|
|
565
|
+
.lpf(3500)
|
|
566
|
+
).swing(0.08)`;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Generates variations on an existing pattern
|
|
570
|
+
* @param pattern - Original pattern code
|
|
571
|
+
* @param variationType - Type of variation ('subtle', 'moderate', 'extreme', 'glitch', 'evolving')
|
|
572
|
+
* @returns Pattern with variation modifiers applied
|
|
573
|
+
*/
|
|
574
|
+
generateVariation(pattern, variationType = 'subtle') {
|
|
575
|
+
const variations = {
|
|
576
|
+
subtle: '.sometimes(x => x.fast(2))',
|
|
577
|
+
moderate: '.every(4, x => x.rev).sometimes(x => x.fast(2))',
|
|
578
|
+
extreme: '.every(2, x => x.jux(rev)).sometimes(x => x.iter(4))',
|
|
579
|
+
glitch: '.sometimes(x => x.chop(8).rev).rarely(x => x.speed(-1))',
|
|
580
|
+
evolving: '.slow(4).every(8, x => x.fast(2)).every(16, x => x.palindrome)'
|
|
581
|
+
};
|
|
582
|
+
return pattern + (variations[variationType] || variations.subtle);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Generates a drum fill for transitions
|
|
586
|
+
* @param style - Music style for the fill
|
|
587
|
+
* @param bars - Length of the fill in bars (default: 1)
|
|
588
|
+
* @returns Strudel fill pattern code
|
|
589
|
+
*/
|
|
590
|
+
generateFill(style, bars = 1) {
|
|
591
|
+
const fills = {
|
|
592
|
+
techno: `s("bd*8, cp*4").fast(${bars})`,
|
|
593
|
+
house: `s("bd*4, cp*2, hh*16").fast(${bars})`,
|
|
594
|
+
dnb: `s("bd*8, sn*8").fast(${bars * 2})`,
|
|
595
|
+
trap: `s("bd*4, hh*32").fast(${bars})`,
|
|
596
|
+
breakbeat: `s("bd cp bd cp, hh*8").iter(4).fast(${bars})`,
|
|
597
|
+
intelligent_dnb: `s("breaks165").fit().slice(16, "0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15").gain(0.7).fast(${bars})`,
|
|
598
|
+
trip_hop: `s("bd ~ sd ~, hh*4").room(0.5).fast(${bars})`,
|
|
599
|
+
boom_bap: `s("bd sd bd sd, hh*8").swing(0.1).fast(${bars})`
|
|
600
|
+
};
|
|
601
|
+
return fills[style] || fills.techno;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Generates a transition between two musical styles
|
|
605
|
+
* @param fromStyle - Starting music style
|
|
606
|
+
* @param toStyle - Target music style
|
|
607
|
+
* @param bars - Length of transition in bars (default: 4)
|
|
608
|
+
* @returns Strudel transition pattern with crossfade
|
|
609
|
+
*/
|
|
610
|
+
generateTransition(fromStyle, toStyle, bars = 4) {
|
|
611
|
+
return `// Transition from ${fromStyle} to ${toStyle}
|
|
612
|
+
stack(
|
|
613
|
+
// Fade out ${fromStyle}
|
|
614
|
+
${this.generateDrumPattern(fromStyle, 0.5)}.gain(perlin.range(0.8, 0).slow(${bars})),
|
|
615
|
+
|
|
616
|
+
// Fade in ${toStyle}
|
|
617
|
+
${this.generateDrumPattern(toStyle, 0.5)}.gain(perlin.range(0, 0.8).slow(${bars}))
|
|
618
|
+
)`;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Generates a Euclidean rhythm pattern
|
|
622
|
+
* @param hits - Number of hits in the pattern
|
|
623
|
+
* @param steps - Total number of steps
|
|
624
|
+
* @param sound - Sound name to use (default: "bd")
|
|
625
|
+
* @returns Strudel Euclidean pattern code
|
|
626
|
+
*/
|
|
627
|
+
generateEuclideanPattern(hits, steps, sound = "bd") {
|
|
628
|
+
const rhythm = this.theory.generateEuclideanRhythm(hits, steps);
|
|
629
|
+
return `s("${sound}").struct("${rhythm}")`;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Generates a polyrhythmic pattern with multiple Euclidean rhythms
|
|
633
|
+
* @param sounds - Array of sound names to use
|
|
634
|
+
* @param patterns - Array of hit counts for each rhythm
|
|
635
|
+
* @returns Strudel polyrhythm pattern code
|
|
636
|
+
* @throws {Error} When sounds and patterns arrays have different lengths
|
|
637
|
+
*/
|
|
638
|
+
generatePolyrhythm(sounds, patterns) {
|
|
639
|
+
if (sounds.length !== patterns.length) {
|
|
640
|
+
throw new Error('Number of sounds must match number of patterns');
|
|
641
|
+
}
|
|
642
|
+
const rhythms = sounds.map((sound, i) => {
|
|
643
|
+
return `s("${sound}").euclid(${patterns[i]}, 16)`;
|
|
644
|
+
});
|
|
645
|
+
return `stack(\n ${rhythms.join(',\n ')}\n)`;
|
|
646
|
+
}
|
|
647
|
+
}
|