chord-synth 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.
@@ -0,0 +1,712 @@
1
+ /**
2
+ * chord-synth engine module
3
+ * Core synthesis engine: MIDI arpeggiator + WAV renderer.
4
+ *
5
+ * const { renderJob, SCHEMA, PRESETS } = require('chord-synth');
6
+ * const wavBuf = renderJob({ preset: "C G Am/E F", bpm: 120 });
7
+ * fs.writeFileSync('out.wav', wavBuf);
8
+ */
9
+ 'use strict';
10
+ const SAMPLE_RATE = 44100;
11
+
12
+ // ════════════════════════════════════════════════════════════════════════
13
+ // SECTION 1: MIDI ARPEGGIATOR ENGINE (ported from Logic Pro Scripter)
14
+ // ════════════════════════════════════════════════════════════════════════
15
+
16
+ const NoteMap = {
17
+ "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3,
18
+ "E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, "G#": 8,
19
+ "Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11
20
+ };
21
+
22
+ const NoteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
23
+
24
+ const ChordPatterns = {
25
+ "": [0, 4, 7], "maj": [0, 4, 7], "M": [0, 4, 7],
26
+ "m": [0, 3, 7], "min": [0, 3, 7], "-": [0, 3, 7],
27
+ "dim": [0, 3, 6], "aug": [0, 4, 8], "+": [0, 4, 8],
28
+ "sus2": [0, 2, 7], "sus4": [0, 5, 7], "sus": [0, 5, 7],
29
+ "7": [0, 4, 7, 10], "dom7": [0, 4, 7, 10],
30
+ "maj7": [0, 4, 7, 11], "M7": [0, 4, 7, 11],
31
+ "m7": [0, 3, 7, 10], "min7": [0, 3, 7, 10], "-7": [0, 3, 7, 10],
32
+ "dim7": [0, 3, 6, 9], "m7b5": [0, 3, 6, 10],
33
+ "add9": [0, 4, 7, 14], "madd9": [0, 3, 7, 14],
34
+ "6": [0, 4, 7, 9], "m6": [0, 3, 7, 9],
35
+ "9": [0, 4, 7, 10, 14], "m9": [0, 3, 7, 10, 14],
36
+ "maj9": [0, 4, 7, 11, 14], "M9": [0, 4, 7, 11, 14],
37
+ "11": [0, 4, 7, 10, 14, 17], "m11": [0, 3, 7, 10, 14, 17],
38
+ "13": [0, 4, 7, 10, 14, 17, 21],
39
+ "5": [0, 7], "power": [0, 7, 12]
40
+ };
41
+
42
+ const RateValues = {
43
+ "1/1": 4, "1/2": 2, "1/2T": 1.333, "1/4": 1, "1/4T": 0.667,
44
+ "1/8": 0.5, "1/8T": 0.333, "1/16": 0.25, "1/16T": 0.167, "1/32": 0.125
45
+ };
46
+
47
+ const ArpPatternNames = [
48
+ "Chord (Block)", "Up", "Down", "Up-Down", "Down-Up",
49
+ "Up-Down (No Repeat)", "Down-Up (No Repeat)", "Random", "Random Walk",
50
+ "Outside-In", "Inside-Out", "Pinky-Thumb", "Thumb-Pinky",
51
+ "1-3-5-3", "1-5-3-5", "1-5-8-5", "1-3-5-8-5-3",
52
+ "Alberti Bass (1-5-3-5)", "Stride (Root-Chord)", "Broken 3rds",
53
+ "Pedal Tone", "Alternating Bass"
54
+ ];
55
+
56
+ const PrebuiltProgressions = [
57
+ { name: "Custom", chords: "", beats: 4 },
58
+ { name: "I-V-vi-IV (Pop)", chords: "C | G | Am | F", beats: 4 },
59
+ { name: "I-vi-IV-V (50s)", chords: "C | Am | F | G", beats: 4 },
60
+ { name: "vi-IV-I-V (Sad)", chords: "Am | F | C | G", beats: 4 },
61
+ { name: "I-IV-V-IV (Rock)", chords: "C | F | G | F", beats: 4 },
62
+ { name: "I-V-vi-iii-IV (Canon)", chords: "C | G | Am | Em | F | C | F | G", beats: 2 },
63
+ { name: "ii-V-I (Jazz)", chords: "Dm7 | G7 | Cmaj7 | Cmaj7", beats: 4 },
64
+ { name: "I-vi-ii-V (Jazz Turnaround)", chords: "Cmaj7 | Am7 | Dm7 | G7", beats: 2 },
65
+ { name: "iii-vi-ii-V (Coltrane)", chords: "Em7 | A7 | Dm7 | G7", beats: 2 },
66
+ { name: "Autumn Leaves", chords: "Am7 | D7 | Gmaj7 | Cmaj7 | F#m7b5 | B7 | Em7 | Em7", beats: 4 },
67
+ { name: "So What", chords: "Dm7 | Dm7 | Dm7 | Dm7 | Ebm7 | Ebm7 | Dm7 | Dm7", beats: 4 },
68
+ { name: "Neo-Soul I", chords: "Fmaj9 | Em7 | Dm9 | Cmaj7", beats: 4 },
69
+ { name: "Neo-Soul II", chords: "Gmaj9 | F#m7 | Bm7 | Am9", beats: 4 },
70
+ { name: "R&B Smooth", chords: "Cmaj7 | Bm7 | Am7 | Gmaj7", beats: 4 },
71
+ { name: "12-Bar Blues", chords: "A7 | A7 | A7 | A7 | D7 | D7 | A7 | A7 | E7 | D7 | A7 | E7", beats: 4 },
72
+ { name: "Minor Blues", chords: "Am7 | Am7 | Dm7 | Dm7 | Am7 | E7 | Am7 | E7", beats: 4 },
73
+ { name: "Bossa Nova", chords: "Cmaj7 | C#dim7 | Dm7 | G7", beats: 4 },
74
+ { name: "Girl from Ipanema", chords: "Fmaj7 | Fmaj7 | G7 | G7 | Gm7 | Gb7 | Fmaj7 | Gb7", beats: 4 },
75
+ { name: "Dorian Vamp", chords: "Dm7 | G7 | Dm7 | G7", beats: 4 },
76
+ { name: "Lydian Float", chords: "Cmaj7 | D | Cmaj7 | D", beats: 4 },
77
+ { name: "Ambient Pads", chords: "Cmaj9 | Am9 | Fmaj9 | Gsus4", beats: 8 },
78
+ { name: "EDM Anthem", chords: "Am | F | C | G", beats: 4 },
79
+ { name: "Trance", chords: "Am | G | F | Em", beats: 4 },
80
+ { name: "House", chords: "Cm | Gm | Bb | F", beats: 4 },
81
+ { name: "Pachelbel Canon", chords: "C | G | Am | Em | F | C | F | G", beats: 2 },
82
+ { name: "Andalusian Cadence", chords: "Am | G | F | E", beats: 4 },
83
+ { name: "Royal Road (JP)", chords: "F | G | Em | Am", beats: 4 },
84
+ { name: "Steely Dan", chords: "Cmaj7 | Bm7b5 E7 | Am7 | Gm7 C7 | Fmaj7 | Fm7 Bb7 | Em7 A7 | Dm7 G7", beats: 2 },
85
+ { name: "Gospel", chords: "C | C7 | F | Fm | C | G7 | C | G7", beats: 4 }
86
+ ];
87
+
88
+ // ── Chord parsing with slash chord support ──
89
+
90
+ function parseChordMidi(str) {
91
+ str = str.trim();
92
+ if (!str || str === "|") return null;
93
+
94
+ // Handle slash chords: "Am/E" → Am chord with E bass note
95
+ let bassNote = null;
96
+ let chordPart = str;
97
+ const slashIdx = str.indexOf('/');
98
+ if (slashIdx > 0) {
99
+ const bassPart = str.substring(slashIdx + 1);
100
+ chordPart = str.substring(0, slashIdx);
101
+ let bassRoot = bassPart.charAt(0).toUpperCase();
102
+ if (bassPart.length > 1 && (bassPart.charAt(1) === '#' || bassPart.charAt(1) === 'b')) {
103
+ bassRoot += bassPart.charAt(1);
104
+ }
105
+ if (NoteMap[bassRoot] !== undefined) bassNote = NoteMap[bassRoot];
106
+ }
107
+
108
+ let root = chordPart.charAt(0).toUpperCase();
109
+ let idx = 1;
110
+ if (chordPart.length > 1 && (chordPart.charAt(1) === "#" || chordPart.charAt(1) === "b")) {
111
+ root += chordPart.charAt(1); idx = 2;
112
+ }
113
+ if (NoteMap[root] === undefined) return null;
114
+ const quality = chordPart.substring(idx);
115
+ const intervals = ChordPatterns[quality] || ChordPatterns[quality.toLowerCase()] || [0, 4, 7];
116
+ return {
117
+ name: str, root: NoteMap[root], intervals: intervals.slice(), quality,
118
+ bassNote: bassNote !== null ? bassNote : NoteMap[root]
119
+ };
120
+ }
121
+
122
+ function parseProgression(str) {
123
+ return str.replace(/\|/g, " | ").split(/\s+/)
124
+ .filter(s => s.length > 0 && s !== "|")
125
+ .map(parseChordMidi).filter(c => c);
126
+ }
127
+
128
+ function applyTranspose(chord, semitones) {
129
+ if (semitones === 0) return chord;
130
+ const newRoot = (chord.root + semitones + 12) % 12;
131
+ const newBass = ((chord.bassNote ?? chord.root) + semitones + 12) % 12;
132
+ // Rebuild display name
133
+ let qualStr = chord.quality || '';
134
+ let displayName = NoteNames[newRoot] + qualStr;
135
+ if ((chord.bassNote ?? chord.root) !== chord.root) {
136
+ // Was a slash chord — show transposed bass
137
+ displayName += '/' + NoteNames[newBass];
138
+ }
139
+ return { ...chord, name: displayName, root: newRoot, bassNote: newBass };
140
+ }
141
+
142
+ // ── Arp pattern builder (all 22 patterns from Logic Pro script) ──
143
+
144
+ function buildArpSequence(chord, octaveRange, baseOctave, pattern) {
145
+ const basePitch = (baseOctave + 1) * 12 + chord.root;
146
+ const notes = [];
147
+ for (let oct = 0; oct < octaveRange; oct++) {
148
+ for (const interval of chord.intervals) {
149
+ const pitch = basePitch + interval + (oct * 12);
150
+ if (pitch >= 0 && pitch <= 127) notes.push(pitch);
151
+ }
152
+ }
153
+ if (notes.length === 0) return [];
154
+
155
+ let sequence = [], temp, mid, left, right;
156
+ switch (pattern) {
157
+ case 0: sequence = [notes.slice()]; break;
158
+ case 1: sequence = notes.slice(); break;
159
+ case 2: sequence = notes.slice().reverse(); break;
160
+ case 3: sequence = notes.slice().concat(notes.slice().reverse()); break;
161
+ case 4: sequence = notes.slice().reverse().concat(notes.slice()); break;
162
+ case 5:
163
+ sequence = notes.slice();
164
+ if (notes.length > 2) sequence = sequence.concat(notes.slice(1, -1).reverse());
165
+ break;
166
+ case 6:
167
+ sequence = notes.slice().reverse();
168
+ if (notes.length > 2) sequence = sequence.concat(notes.slice(1, -1));
169
+ break;
170
+ case 7:
171
+ sequence = notes.slice();
172
+ for (let j = sequence.length - 1; j > 0; j--) {
173
+ const k = Math.floor(Math.random() * (j + 1));
174
+ [sequence[j], sequence[k]] = [sequence[k], sequence[j]];
175
+ }
176
+ break;
177
+ case 8:
178
+ sequence = [];
179
+ let cur = Math.floor(notes.length / 2);
180
+ for (let j = 0; j < notes.length * 2; j++) {
181
+ sequence.push(notes[cur]);
182
+ cur = Math.max(0, Math.min(notes.length - 1, cur + (Math.random() < 0.5 ? -1 : 1)));
183
+ }
184
+ break;
185
+ case 9:
186
+ temp = notes.slice(); sequence = [];
187
+ while (temp.length > 0) { if (temp.length > 0) sequence.push(temp.shift()); if (temp.length > 0) sequence.push(temp.pop()); }
188
+ break;
189
+ case 10:
190
+ temp = notes.slice(); sequence = []; mid = Math.floor(temp.length / 2); left = mid - 1; right = mid;
191
+ while (left >= 0 || right < temp.length) {
192
+ if (right < temp.length) sequence.push(temp[right++]);
193
+ if (left >= 0) sequence.push(temp[left--]);
194
+ }
195
+ break;
196
+ case 11:
197
+ sequence = [];
198
+ for (let j = 0; j < Math.ceil(notes.length / 2); j++) {
199
+ const hi = notes.length - 1 - j, lo = j;
200
+ if (hi >= lo) { sequence.push(notes[hi]); if (hi !== lo) sequence.push(notes[lo]); }
201
+ }
202
+ break;
203
+ case 12:
204
+ sequence = [];
205
+ for (let j = 0; j < Math.ceil(notes.length / 2); j++) {
206
+ const lo = j, hi = notes.length - 1 - j;
207
+ if (lo <= hi) { sequence.push(notes[lo]); if (lo !== hi) sequence.push(notes[hi]); }
208
+ }
209
+ break;
210
+ case 13: sequence = notes.length >= 3 ? [notes[0], notes[1], notes[2], notes[1]] : notes.slice(); break;
211
+ case 14: case 17: sequence = notes.length >= 3 ? [notes[0], notes[2], notes[1], notes[2]] : notes.slice(); break;
212
+ case 15: {
213
+ const octUp = notes[0] + 12;
214
+ sequence = notes.length >= 3 ? [notes[0], notes[2], octUp <= 127 ? octUp : notes[0], notes[2]] : notes.slice();
215
+ break;
216
+ }
217
+ case 16: {
218
+ const oc = Math.min(notes[0] + 12, 127);
219
+ sequence = notes.length >= 3 ? [notes[0], notes[1], notes[2], oc, notes[2], notes[1]] : notes.slice();
220
+ break;
221
+ }
222
+ case 18:
223
+ sequence = notes.length >= 2 ? [notes[0], notes.slice(1)] : notes.slice();
224
+ break;
225
+ case 19:
226
+ sequence = [];
227
+ for (let j = 0; j < notes.length - 1; j++) { sequence.push(notes[j]); sequence.push(notes[j + 1]); }
228
+ break;
229
+ case 20:
230
+ sequence = [];
231
+ for (let j = 1; j < notes.length; j++) { sequence.push(notes[0]); sequence.push(notes[j]); }
232
+ break;
233
+ case 21:
234
+ sequence = notes.length >= 3 ? [notes[0], notes[2], notes[0], notes[2]] : notes.slice();
235
+ break;
236
+ default: sequence = notes.slice();
237
+ }
238
+ return sequence;
239
+ }
240
+
241
+ // ── MIDI event generator ──
242
+
243
+ function generateMidiEvents(config) {
244
+ const {
245
+ progression, beatsPerChord, bpm, transpose = 0,
246
+ pattern = 1, rate = "1/8", octaveRange = 1, baseOctave = 3,
247
+ gatePercent = 0.9, velocity = 90, velRandom = 5,
248
+ accentEvery = 4, accentAmount = 20, firstBeatAccent = true,
249
+ swing = 0.5, loops = 1
250
+ } = config;
251
+
252
+ const rawChords = parseProgression(progression);
253
+ if (rawChords.length === 0) return { events: [], duration: 0, chords: [] };
254
+
255
+ // Apply transpose globally — pad, bass, and display all see transposed chords
256
+ const chords = rawChords.map(c => applyTranspose(c, transpose));
257
+
258
+ const stepLength = RateValues[rate] || 0.5;
259
+ const beatDuration = 60 / bpm;
260
+ const stepDur = stepLength * beatDuration;
261
+
262
+ const totalChordsBeats = chords.length * beatsPerChord;
263
+ const totalBeats = totalChordsBeats * loops;
264
+ const totalDuration = totalBeats * beatDuration;
265
+ const totalSteps = Math.floor(totalBeats / stepLength);
266
+
267
+ const events = [];
268
+ let stepCounter = 0;
269
+
270
+ for (let step = 0; step < totalSteps; step++) {
271
+ const beatPos = step * stepLength;
272
+ const loopBeat = beatPos % totalChordsBeats;
273
+ const chordIdx = Math.floor(loopBeat / beatsPerChord) % chords.length;
274
+ const isFirstBeatOfChord = (Math.floor(loopBeat / stepLength) % Math.floor(beatsPerChord / stepLength)) === 0;
275
+
276
+ const chord = chords[chordIdx];
277
+ const arpSeq = buildArpSequence(chord, octaveRange, baseOctave, pattern);
278
+ if (arpSeq.length === 0) continue;
279
+
280
+ const arpIdx = step % arpSeq.length;
281
+ let pitches = arpSeq[arpIdx];
282
+ if (!Array.isArray(pitches)) pitches = [pitches];
283
+
284
+ let vel = velocity;
285
+ if (velRandom > 0) vel += Math.floor(Math.random() * velRandom * 2) - velRandom;
286
+ if (accentEvery > 0 && stepCounter % accentEvery === 0) vel += accentAmount;
287
+ if (firstBeatAccent && isFirstBeatOfChord) vel += Math.floor(accentAmount * 0.5);
288
+ vel = Math.max(1, Math.min(127, vel));
289
+
290
+ let time = step * stepDur;
291
+ if (step % 2 === 1 && swing > 0.5) time += (swing - 0.5) * 2 * stepDur;
292
+
293
+ events.push({ time, pitches, velocity: vel, duration: stepDur * gatePercent, chordName: chord.name, chordIdx });
294
+ stepCounter++;
295
+ }
296
+
297
+ return { events, duration: totalDuration, chords };
298
+ }
299
+
300
+
301
+ // ════════════════════════════════════════════════════════════════════════
302
+ // SECTION 2: WAV SYNTHESIS ENGINE
303
+ // ════════════════════════════════════════════════════════════════════════
304
+
305
+ function generateWaveform(type, phase) {
306
+ const p = ((phase % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
307
+ switch (type) {
308
+ case 'sine': return Math.sin(p);
309
+ case 'sawtooth': return 2 * (p / (2 * Math.PI)) - 1;
310
+ case 'square': return p < Math.PI ? 1 : -1;
311
+ case 'triangle': { const t = p / (2 * Math.PI); return t < 0.25 ? 4*t : t < 0.75 ? 2-4*t : -4+4*t; }
312
+ default: return Math.sin(p);
313
+ }
314
+ }
315
+
316
+ class BiquadFilter {
317
+ constructor(type, freq, Q, sr, gain = 0) {
318
+ this.x1=0;this.x2=0;this.y1=0;this.y2=0;
319
+ const w0=2*Math.PI*freq/sr, cosw0=Math.cos(w0), sinw0=Math.sin(w0), alpha=sinw0/(2*Q);
320
+ let b0,b1,b2,a0,a1,a2;
321
+ switch(type){
322
+ case'lowpass':b0=(1-cosw0)/2;b1=1-cosw0;b2=(1-cosw0)/2;a0=1+alpha;a1=-2*cosw0;a2=1-alpha;break;
323
+ case'highpass':b0=(1+cosw0)/2;b1=-(1+cosw0);b2=(1+cosw0)/2;a0=1+alpha;a1=-2*cosw0;a2=1-alpha;break;
324
+ case'bandpass':b0=alpha;b1=0;b2=-alpha;a0=1+alpha;a1=-2*cosw0;a2=1-alpha;break;
325
+ case'peaking':{const A=Math.pow(10,gain/40);b0=1+alpha*A;b1=-2*cosw0;b2=1-alpha*A;a0=1+alpha/A;a1=-2*cosw0;a2=1-alpha/A;break;}
326
+ default:b0=1;b1=0;b2=0;a0=1;a1=0;a2=0;
327
+ }
328
+ this.b0=b0/a0;this.b1=b1/a0;this.b2=b2/a0;this.a1=a1/a0;this.a2=a2/a0;
329
+ }
330
+ process(x){const y=this.b0*x+this.b1*this.x1+this.b2*this.x2-this.a1*this.y1-this.a2*this.y2;this.x2=this.x1;this.x1=x;this.y2=this.y1;this.y1=y;return y;}
331
+ processBuffer(buf){for(let i=0;i<buf.length;i++)buf[i]=this.process(buf[i]);return buf;}
332
+ }
333
+
334
+ function compress(buffer, threshold=-24, ratio=4, attack=0.003, release=0.25) {
335
+ const tL=Math.pow(10,threshold/20),aC=Math.exp(-1/(attack*SAMPLE_RATE)),rC=Math.exp(-1/(release*SAMPLE_RATE));
336
+ let env=0;
337
+ for(let i=0;i<buffer.length;i++){
338
+ const a=Math.abs(buffer[i]);env=a>env?aC*env+(1-aC)*a:rC*env+(1-rC)*a;
339
+ if(env>tL)buffer[i]*=tL*Math.pow(env/tL,1/ratio-1)/Math.max(env,0.0001);
340
+ }
341
+ return buffer;
342
+ }
343
+
344
+ function applyAlgorithmicReverb(input, decayTime=2.0, mix=0.18) {
345
+ const out=new Float32Array(input.length);
346
+ const cD=[1557,1617,1491,1422].map(d=>Math.floor(d*SAMPLE_RATE/44100));
347
+ const aD=[225,556].map(d=>Math.floor(d*SAMPLE_RATE/44100));
348
+ const cB=cD.map(d=>({buf:new Float32Array(d),idx:0,len:d}));
349
+ const cF=cD.map(d=>Math.pow(0.001,d/(decayTime*SAMPLE_RATE)));
350
+ const aB=aD.map(d=>({buf:new Float32Array(d),idx:0,len:d}));
351
+ for(let i=0;i<input.length;i++){
352
+ let cs=0;
353
+ for(let c=0;c<cB.length;c++){const cb=cB[c];const del=cb.buf[cb.idx];cb.buf[cb.idx]=input[i]+del*cF[c];cb.idx=(cb.idx+1)%cb.len;cs+=del;}
354
+ cs/=cB.length;let ap=cs;
355
+ for(let a=0;a<aB.length;a++){const ab=aB[a];const del=ab.buf[ab.idx];ab.buf[ab.idx]=ap+del*0.5;ab.idx=(ab.idx+1)%ab.len;ap=del-ap*0.5;}
356
+ out[i]=input[i]*(1-mix)+ap*mix;
357
+ }
358
+ return out;
359
+ }
360
+
361
+ function mixInto(mainBuf, noteBuf, offsetSamples) {
362
+ const start=Math.max(0,offsetSamples), end=Math.min(mainBuf.length,offsetSamples+noteBuf.length);
363
+ for(let i=start;i<end;i++) mainBuf[i]+=noteBuf[i-offsetSamples];
364
+ }
365
+
366
+ function midiToFreq(pitch) { return 440 * Math.pow(2, (pitch - 69) / 12); }
367
+
368
+ // ── Instrument Renderers ──
369
+
370
+ function renderPianoNote(freq, duration, volume=0.20, velScale=1.0) {
371
+ const len=Math.floor(SAMPLE_RATE*(duration+0.3)); const buf=new Float32Array(len); const vol=volume*velScale;
372
+ const sL=Math.floor(SAMPLE_RATE*0.015);
373
+ for(let i=0;i<sL&&i<len;i++) buf[i]+=(Math.random()*2-1)*vol*0.4*Math.exp(-i/(SAMPLE_RATE*0.003));
374
+ const H=[{r:1,a:1.0,t:'triangle'},{r:2,a:0.5,t:'sine'},{r:3,a:0.25,t:'sine'},{r:4,a:0.12,t:'sine'},{r:5,a:0.06,t:'sine'}];
375
+ for(const h of H){let p=0;const inc=2*Math.PI*freq*h.r/SAMPLE_RATE;for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;buf[i]+=generateWaveform(h.t,p)*vol*h.a*Math.min(1,t/0.005)*Math.exp(-t/(duration*0.4));p+=inc;}}
376
+ new BiquadFilter('lowpass',Math.min(freq*8,8000),1.0,SAMPLE_RATE).processBuffer(buf);
377
+ return buf;
378
+ }
379
+
380
+ function renderGuitarNote(freq, duration, volume=0.16, velScale=1.0) {
381
+ const len=Math.floor(SAMPLE_RATE*(duration+0.2)); const buf=new Float32Array(len); const vol=volume*velScale;
382
+ const pL=Math.floor(SAMPLE_RATE*0.02);
383
+ for(let i=0;i<pL&&i<len;i++) buf[i]+=(Math.random()*2-1)*vol*0.5*Math.exp(-i/(SAMPLE_RATE*0.004));
384
+ const H=[{r:1,a:1.0},{r:2,a:0.6},{r:3,a:0.35},{r:4,a:0.15},{r:5,a:0.08}];
385
+ for(const h of H){let p=0;const inc=2*Math.PI*freq*h.r/SAMPLE_RATE;for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;buf[i]+=generateWaveform('triangle',p)*vol*h.a*Math.min(1,t/0.003)*Math.exp(-t/(duration*0.35));p+=inc;}}
386
+ new BiquadFilter('lowpass',Math.min(freq*6,6000),1.5,SAMPLE_RATE).processBuffer(buf);
387
+ return buf;
388
+ }
389
+
390
+ function renderBassNote(freq, duration, volume=0.30, velScale=1.0) {
391
+ const len=Math.floor(SAMPLE_RATE*(duration+0.2)); const buf=new Float32Array(len); const vol=volume*velScale;
392
+ const bf=freq<150?freq:freq/2; let p1=0,p2=0;
393
+ const i1=2*Math.PI*bf/SAMPLE_RATE, i2=2*Math.PI*bf*2/SAMPLE_RATE;
394
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;const e=Math.min(1,t/0.01)*Math.exp(-t/(duration*0.5));buf[i]=(Math.sin(p1)*0.7+generateWaveform('triangle',p2)*0.3)*vol*e;p1+=i1;p2+=i2;}
395
+ new BiquadFilter('lowpass',Math.min(bf*4,800),2.0,SAMPLE_RATE).processBuffer(buf);
396
+ new BiquadFilter('peaking',bf*1.5,2.0,SAMPLE_RATE,4).processBuffer(buf);
397
+ return buf;
398
+ }
399
+
400
+ function renderPadChord(frequencies, duration, volume=0.12, fadeIn=0.4, fadeOut=0.4) {
401
+ const len=Math.floor(SAMPLE_RATE*duration); const buf=new Float32Array(len);
402
+ for(const freq of frequencies){
403
+ for(let j=0;j<4;j++){
404
+ const df=freq*Math.pow(2,(j-1.5)*3/1200);
405
+ const types=['sawtooth','sine','triangle','sine'];
406
+ let phase=Math.random()*2*Math.PI; const inc=2*Math.PI*df/SAMPLE_RATE;
407
+ const oscVol=volume*0.25/frequencies.length;
408
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;let env=1;if(t<fadeIn)env=t/fadeIn;if(t>duration-fadeOut)env=Math.max(0,(duration-t)/fadeOut);buf[i]+=generateWaveform(types[j],phase)*oscVol*env;phase+=inc;}
409
+ }
410
+ }
411
+ new BiquadFilter('lowpass',2000,0.7,SAMPLE_RATE).processBuffer(buf);
412
+ return buf;
413
+ }
414
+
415
+ function renderViolin(freq, duration, volume=0.18, velScale=1.0) {
416
+ const len=Math.floor(SAMPLE_RATE*(duration+0.1)); const buf=new Float32Array(len); const vol=volume*velScale;
417
+ let phase=0,vibP=0;
418
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;const vib=Math.sin(vibP)*3*Math.min(1,t/0.3);const f=freq+vib;const inc=2*Math.PI*f/SAMPLE_RATE;const att=Math.min(1,t/0.08);const rel=t>duration-0.05?Math.max(0,(duration-t)/0.05):1;buf[i]+=(generateWaveform('sawtooth',phase)*0.5+Math.sin(phase*2)*0.3+Math.sin(phase*3)*0.15)*vol*att*rel;phase+=inc;vibP+=2*Math.PI*5.5/SAMPLE_RATE;}
419
+ new BiquadFilter('lowpass',Math.min(freq*5,6000),1.2,SAMPLE_RATE).processBuffer(buf);
420
+ for(let i=0;i<Math.min(len,SAMPLE_RATE*0.03);i++) buf[i]+=(Math.random()*2-1)*vol*0.15*Math.exp(-i/(SAMPLE_RATE*0.008));
421
+ return buf;
422
+ }
423
+
424
+ function renderFlute(freq, duration, volume=0.14, velScale=1.0) {
425
+ const len=Math.floor(SAMPLE_RATE*(duration+0.1)); const buf=new Float32Array(len); const vol=volume*velScale;
426
+ let phase=0,vibP=0;
427
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;const vib=Math.sin(vibP)*2*Math.min(1,t/0.4);const f=freq+vib;const inc=2*Math.PI*f/SAMPLE_RATE;const att=Math.min(1,t/0.06);const rel=t>duration-0.04?Math.max(0,(duration-t)/0.04):1;buf[i]+=(Math.sin(phase)*0.8+Math.sin(phase*2)*0.15)*vol*att*rel;phase+=inc;vibP+=2*Math.PI*5/SAMPLE_RATE;}
428
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;buf[i]+=(Math.random()*2-1)*vol*0.08*Math.min(1,t/0.02)*Math.exp(-t/(duration*0.8));}
429
+ new BiquadFilter('lowpass',Math.min(freq*4,5000),0.8,SAMPLE_RATE).processBuffer(buf);
430
+ return buf;
431
+ }
432
+
433
+ function renderClarinet(freq, duration, volume=0.16, velScale=1.0) {
434
+ const len=Math.floor(SAMPLE_RATE*(duration+0.1)); const buf=new Float32Array(len); const vol=volume*velScale;
435
+ let phase=0;
436
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;const att=Math.min(1,t/0.04);const rel=t>duration-0.04?Math.max(0,(duration-t)/0.04):1;buf[i]+=(generateWaveform('square',phase)*0.5+Math.sin(phase*3)*0.25+Math.sin(phase*5)*0.1)*vol*att*rel;phase+=2*Math.PI*freq/SAMPLE_RATE;}
437
+ for(let i=0;i<Math.min(len,SAMPLE_RATE*0.02);i++) buf[i]+=(Math.random()*2-1)*vol*0.2*Math.exp(-i/(SAMPLE_RATE*0.005));
438
+ new BiquadFilter('lowpass',Math.min(freq*5,4500),1.5,SAMPLE_RATE).processBuffer(buf);
439
+ return buf;
440
+ }
441
+
442
+ function renderCello(freq, duration, volume=0.20, velScale=1.0) {
443
+ const len=Math.floor(SAMPLE_RATE*(duration+0.15)); const buf=new Float32Array(len); const vol=volume*velScale;
444
+ let phase=0,vibP=0;
445
+ for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;const vib=Math.sin(vibP)*3*Math.min(1,t/0.4);const f=freq+vib;const inc=2*Math.PI*f/SAMPLE_RATE;const att=Math.min(1,t/0.1);const rel=t>duration-0.08?Math.max(0,(duration-t)/0.08):1;buf[i]+=(generateWaveform('sawtooth',phase)*0.6+Math.sin(phase*2)*0.25+Math.sin(phase*3)*0.1)*vol*att*rel;phase+=inc;vibP+=2*Math.PI*4.5/SAMPLE_RATE;}
446
+ for(let i=0;i<Math.min(len,SAMPLE_RATE*0.04);i++) buf[i]+=(Math.random()*2-1)*vol*0.12*Math.exp(-i/(SAMPLE_RATE*0.01));
447
+ new BiquadFilter('lowpass',Math.min(freq*4,3500),1.0,SAMPLE_RATE).processBuffer(buf);
448
+ return buf;
449
+ }
450
+
451
+ // ── Percussion ──
452
+ function renderKick(vol=0.4,acc=false){const len=Math.floor(SAMPLE_RATE*0.25);const buf=new Float32Array(len);const v=acc?vol*1.3:vol;let ph=0;for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;buf[i]=Math.sin(ph)*v*Math.exp(-t*12);ph+=2*Math.PI*(50+100*Math.exp(-t*30))/SAMPLE_RATE;}for(let i=0;i<Math.min(Math.floor(SAMPLE_RATE*0.008),len);i++)buf[i]+=(Math.random()*2-1)*v*0.6*Math.exp(-i/(SAMPLE_RATE*0.002));return buf;}
453
+ function renderSnare(vol=0.35,acc=false){const len=Math.floor(SAMPLE_RATE*0.2);const buf=new Float32Array(len);const v=acc?vol*1.3:vol;let ph=0;for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;buf[i]=(Math.random()*2-1)*v*0.7*Math.exp(-t*18)+Math.sin(ph)*v*0.5*Math.exp(-t*25);ph+=2*Math.PI*200/SAMPLE_RATE;}for(let i=0;i<Math.min(Math.floor(SAMPLE_RATE*0.01),len);i++)buf[i]+=(Math.random()*2-1)*v*0.5*Math.exp(-i/(SAMPLE_RATE*0.001));return buf;}
454
+ function renderHiHat(vol=0.2,acc=false){const len=Math.floor(SAMPLE_RATE*0.08);const buf=new Float32Array(len);const v=acc?vol*1.2:vol;for(let i=0;i<len;i++)buf[i]=(Math.random()*2-1)*v*Math.exp(-i/SAMPLE_RATE*50);new BiquadFilter('highpass',7000,1.0,SAMPLE_RATE).processBuffer(buf);return buf;}
455
+ function renderTom(vol=0.3,acc=false){const len=Math.floor(SAMPLE_RATE*0.2);const buf=new Float32Array(len);const v=acc?vol*1.2:vol;let ph=0;for(let i=0;i<len;i++){const t=i/SAMPLE_RATE;buf[i]=Math.sin(ph)*v*Math.exp(-t*10);ph+=2*Math.PI*(120+60*Math.exp(-t*15))/SAMPLE_RATE;}return buf;}
456
+
457
+ const InstrumentRenderers = {
458
+ piano: renderPianoNote, guitar: renderGuitarNote, bass: renderBassNote,
459
+ violin: renderViolin, flute: renderFlute, clarinet: renderClarinet, cello: renderCello
460
+ };
461
+
462
+
463
+ // ════════════════════════════════════════════════════════════════════════
464
+ // SECTION 3: INTEGRATED RENDERER
465
+ // ════════════════════════════════════════════════════════════════════════
466
+
467
+ const BeatPatterns = {
468
+ "8beat": "| K! H S H K H S H |",
469
+ "16beat": "| K! H H S H H K H H S H H K H H S H |",
470
+ "poprock": "| K! H S! H K . K H S! H |",
471
+ "acousticpop": "| K! . H S! . H K . H S! . H |",
472
+ "bossanova": "| K . . S . K . S . . K . S . . . |",
473
+ "swing": "| K . T . S . T . K . T . S . T . |",
474
+ "funk": "| K! . S! . K . S! . K! . S . K . |",
475
+ "rnb": "| K! . H S! H . K H H S! . H |",
476
+ "waltz": "| K! T T | K! T T |",
477
+ "arpeggio": "| K . . . S . . . K . . . S . . . |",
478
+ "none": ""
479
+ };
480
+
481
+ function parseURN(text) { return text.replace(/\|/g, "").trim().split(/\s+/).filter(t => t); }
482
+
483
+ /**
484
+ * Main render function.
485
+ *
486
+ * config.preset can be:
487
+ * - A named preset: "I-V-vi-IV (Pop)", "Autumn Leaves", etc.
488
+ * - A custom string: "C G Am/E F", "Dm7 | G7 | Cmaj7", etc.
489
+ *
490
+ * config.transpose: semitones (-11 to +11), applied to ALL tracks
491
+ *
492
+ * New pad controls:
493
+ * config.padOctave: MIDI octave for pad (default 4 = middle C region)
494
+ * config.padVolume: pad volume 0.0-1.0 (default 0.12)
495
+ * config.enablePad: true/false (default true)
496
+ */
497
+ function renderJob(config) {
498
+ // ── Resolve preset: named OR custom chord string ──
499
+ let progression = config.preset || "C | G | Am | F";
500
+ let beatsPerChord = config.beatsPerChord || 4;
501
+
502
+ const found = PrebuiltProgressions.find(p => p.name === progression);
503
+ if (found) {
504
+ progression = found.chords;
505
+ beatsPerChord = config.beatsPerChord || found.beats;
506
+ }
507
+ // Otherwise, preset IS the chord string (custom mode)
508
+
509
+ const bpm = config.bpm || 120;
510
+ const loops = config.loops || 2;
511
+ const transpose = config.transpose || 0;
512
+ const instrument = config.instrument || 'piano';
513
+ const enableBass = config.enableBass !== false;
514
+ const enablePad = config.enablePad !== false;
515
+ const padOctave = config.padOctave ?? 4;
516
+ const padVolume = config.padVolume ?? 0.12;
517
+ const enableDrums = config.enableDrums !== false;
518
+ const drumPattern = config.drumPattern || 'poprock';
519
+ const reverbMix = config.reverbMix ?? 0.18;
520
+ const masterVol = config.masterVolume ?? 0.85;
521
+
522
+ // ── Generate MIDI events (chords returned are already transposed) ──
523
+ const { events, duration, chords } = generateMidiEvents({
524
+ progression, beatsPerChord, bpm, transpose,
525
+ pattern: config.pattern ?? 1,
526
+ rate: config.rate || "1/8",
527
+ octaveRange: config.octaveRange || 1,
528
+ baseOctave: config.baseOctave || 3,
529
+ gatePercent: config.gatePercent || 0.9,
530
+ velocity: config.velocity || 90,
531
+ velRandom: config.velRandom ?? 5,
532
+ accentEvery: config.accentEvery ?? 4,
533
+ accentAmount: config.accentAmount ?? 20,
534
+ firstBeatAccent: config.firstBeatAccent !== false,
535
+ swing: config.swing ?? 0.5,
536
+ loops
537
+ });
538
+
539
+ if (events.length === 0) { console.log(' ⚠ No events generated!'); return Buffer.alloc(44); }
540
+
541
+ const totalSamples = Math.floor(SAMPLE_RATE * (duration + 3));
542
+ const dryBuf = new Float32Array(totalSamples);
543
+ const wetBuf = new Float32Array(totalSamples);
544
+ const beatDur = 60 / bpm;
545
+ const renderInstr = InstrumentRenderers[instrument] || renderPianoNote;
546
+
547
+ const txLabel = transpose !== 0 ? ` [transpose ${transpose > 0 ? '+' : ''}${transpose}]` : '';
548
+ console.log(` Chords: ${chords.map(c => c.name).join(' → ')}${txLabel}`);
549
+ console.log(` Pattern: ${ArpPatternNames[config.pattern ?? 1]}, Rate: ${config.rate || '1/8'}, BPM: ${bpm}`);
550
+ console.log(` Lead: ${instrument} | Pad: ${enablePad ? 'oct'+padOctave+' vol'+padVolume : 'off'} | Bass: ${enableBass ? 'on' : 'off'} | Drums: ${enableDrums ? drumPattern : 'off'}`);
551
+
552
+ // ── PAD TRACK: sustained chord pads, one per chord, properly timed ──
553
+ if (enablePad && chords.length > 0) {
554
+ process.stdout.write(' Pad track...');
555
+ const chordDuration = beatsPerChord * beatDur;
556
+ const totalChordBeats = chords.length * beatsPerChord;
557
+ for (let loop = 0; loop < loops; loop++) {
558
+ chords.forEach((chord, idx) => {
559
+ const chordStart = (loop * totalChordBeats + idx * beatsPerChord) * beatDur;
560
+ if (chordStart >= duration) return;
561
+ // Build frequencies from transposed chord at padOctave
562
+ const padFreqs = chord.intervals.map(iv => midiToFreq(padOctave * 12 + chord.root + iv));
563
+ const padDur = Math.min(chordDuration, duration - chordStart);
564
+ const fadeTime = Math.min(0.4, padDur * 0.15);
565
+ const padBuf = renderPadChord(padFreqs, padDur, padVolume, fadeTime, fadeTime);
566
+ const offset = Math.floor(chordStart * SAMPLE_RATE);
567
+ for (let i = 0; i < padBuf.length && i + offset < totalSamples; i++) {
568
+ dryBuf[i + offset] += padBuf[i] * 0.6;
569
+ wetBuf[i + offset] += padBuf[i] * 0.4; // pads get more reverb
570
+ }
571
+ });
572
+ }
573
+ process.stdout.write(' ✓\n');
574
+ }
575
+
576
+ // ── ARP NOTES ──
577
+ process.stdout.write(' Arp notes...');
578
+ let prevChordIdx = -1;
579
+ for (let ei = 0; ei < events.length; ei++) {
580
+ const ev = events[ei];
581
+ const offset = Math.floor(ev.time * SAMPLE_RATE);
582
+ const velScale = ev.velocity / 100;
583
+
584
+ for (const pitch of ev.pitches) {
585
+ const noteBuf = renderInstr(midiToFreq(pitch), ev.duration, undefined, velScale);
586
+ for (let i = 0; i < noteBuf.length && i + offset < totalSamples; i++) {
587
+ dryBuf[i + offset] += noteBuf[i] * 0.8;
588
+ wetBuf[i + offset] += noteBuf[i] * 0.2;
589
+ }
590
+ }
591
+
592
+ // ── BASS: on chord changes, use slash bass note (Am/E → E bass) ──
593
+ if (enableBass && ev.chordIdx !== undefined && ev.chordIdx !== prevChordIdx) {
594
+ const chord = chords[ev.chordIdx % chords.length];
595
+ const bassNoteNum = chord.bassNote ?? chord.root;
596
+ const bassFreq = midiToFreq(3 * 12 + bassNoteNum); // octave 2
597
+ const bassDur = beatsPerChord * beatDur * 0.8;
598
+ const bassBuf = renderBassNote(bassFreq, bassDur, 0.28, velScale);
599
+ for (let i = 0; i < bassBuf.length && i + offset < totalSamples; i++) {
600
+ dryBuf[i + offset] += bassBuf[i] * 0.92;
601
+ wetBuf[i + offset] += bassBuf[i] * 0.08;
602
+ }
603
+ prevChordIdx = ev.chordIdx;
604
+ }
605
+ }
606
+ process.stdout.write(' ✓\n');
607
+
608
+ // ── DRUMS ──
609
+ if (enableDrums && drumPattern !== 'none' && BeatPatterns[drumPattern]) {
610
+ process.stdout.write(' Drums...');
611
+ const tokens = parseURN(BeatPatterns[drumPattern]);
612
+ if (tokens.length > 0) {
613
+ const totalDrumSteps = Math.ceil(duration / beatDur);
614
+ for (let step = 0; step < totalDrumSteps; step++) {
615
+ const token = tokens[step % tokens.length];
616
+ const time = step * beatDur;
617
+ if (time >= duration) break;
618
+ const offset = Math.floor(time * SAMPLE_RATE);
619
+ const isAcc = token.includes('!');
620
+ const base = token.replace('!', '');
621
+ if (base.includes('K')) mixInto(dryBuf, renderKick(0.35, isAcc), offset);
622
+ if (base.includes('S')) { const s=renderSnare(0.30,isAcc); for(let i=0;i<s.length&&i+offset<totalSamples;i++){dryBuf[i+offset]+=s[i]*0.85;wetBuf[i+offset]+=s[i]*0.15;} }
623
+ if (base.includes('H')) mixInto(dryBuf, renderHiHat(0.18, isAcc), offset);
624
+ if (base.includes('T')) mixInto(dryBuf, renderTom(0.25, isAcc), offset);
625
+ }
626
+ }
627
+ process.stdout.write(' ✓\n');
628
+ }
629
+
630
+ // ── EFFECTS & MIXDOWN ──
631
+ process.stdout.write(' Reverb + compress...');
632
+ const reverbbed = applyAlgorithmicReverb(wetBuf, 2.2, 1.0);
633
+ const mixed = new Float32Array(totalSamples);
634
+ for (let i = 0; i < totalSamples; i++) mixed[i] = dryBuf[i] * (1 - reverbMix) + reverbbed[i] * reverbMix;
635
+ compress(mixed, -24, 4, 0.003, 0.25);
636
+ for (let i = 0; i < mixed.length; i++) mixed[i] *= masterVol;
637
+
638
+ const targetSamples = Math.floor(duration * SAMPLE_RATE);
639
+ const output = mixed.slice(0, targetSamples);
640
+ let peak = 0;
641
+ for (let i = 0; i < output.length; i++) peak = Math.max(peak, Math.abs(output[i]));
642
+ if (peak > 0.95) { const sc = 0.92 / peak; for (let i = 0; i < output.length; i++) output[i] *= sc; }
643
+ process.stdout.write(' ✓\n');
644
+
645
+ return encodeWAV(output);
646
+ }
647
+
648
+ // ── WAV Encoder ──
649
+
650
+ function encodeWAV(samples) {
651
+ const buf = Buffer.alloc(samples.length * 2 + 44); let o = 0;
652
+ const ws = s => { for (let i = 0; i < s.length; i++) buf[o++] = s.charCodeAt(i); };
653
+ ws("RIFF"); buf.writeUInt32LE(36 + samples.length * 2, o); o += 4;
654
+ ws("WAVE"); ws("fmt ");
655
+ buf.writeUInt32LE(16, o); o += 4;
656
+ buf.writeUInt16LE(1, o); o += 2; buf.writeUInt16LE(1, o); o += 2;
657
+ buf.writeUInt32LE(SAMPLE_RATE, o); o += 4; buf.writeUInt32LE(SAMPLE_RATE * 2, o); o += 4;
658
+ buf.writeUInt16LE(2, o); o += 2; buf.writeUInt16LE(16, o); o += 2;
659
+ ws("data"); buf.writeUInt32LE(samples.length * 2, o); o += 4;
660
+ for (let i = 0; i < samples.length; i++, o += 2) {
661
+ const s = Math.max(-1, Math.min(1, samples[i]));
662
+ buf.writeInt16LE(Math.floor(s < 0 ? s * 0x8000 : s * 0x7fff), o);
663
+ }
664
+ return buf;
665
+ }
666
+
667
+ // ════════════════════════════════════════════════════════════════════════
668
+ // MODULE EXPORTS
669
+ // ════════════════════════════════════════════════════════════════════════
670
+
671
+ const PRESETS = PrebuiltProgressions.slice(1).map(p => ({ name: p.name, chords: p.chords, beats: p.beats }));
672
+ const PATTERNS = ArpPatternNames.map((name, index) => ({ index, name }));
673
+ const INSTRUMENTS = ['piano', 'guitar', 'bass', 'violin', 'flute', 'clarinet', 'cello'];
674
+ const DRUMS = Object.keys(BeatPatterns);
675
+ const RATES = Object.keys(RateValues);
676
+
677
+ const SCHEMA = {
678
+ type: "object",
679
+ required: ["preset"],
680
+ properties: {
681
+ preset: { type: "string", description: "Named preset (e.g. 'I-V-vi-IV (Pop)') OR custom chords (e.g. 'C G Am/E F', 'Dm7 | G7 | Cmaj7'). Slash chords set bass note." },
682
+ beatsPerChord: { type: "integer", description: "Beats per chord. Auto-set from named presets, required for custom.", default: 4, minimum: 1, maximum: 16 },
683
+ bpm: { type: "integer", description: "Tempo in BPM.", default: 120, minimum: 30, maximum: 300 },
684
+ transpose: { type: "integer", description: "Transpose all tracks by N semitones.", default: 0, minimum: -11, maximum: 11 },
685
+ pattern: { type: "integer", description: "Arp pattern index 0-21. 0=Block 1=Up 2=Down 3=UpDown 5=UpDownNR 7=Random 13=1-3-5-3 17=Alberti 18=Stride 20=Pedal.", default: 1, minimum: 0, maximum: 21 },
686
+ rate: { type: "string", description: "Arp note rate.", default: "1/8", enum: ["1/1","1/2","1/2T","1/4","1/4T","1/8","1/8T","1/16","1/16T","1/32"] },
687
+ instrument: { type: "string", description: "Lead instrument.", default: "piano", enum: ["piano","guitar","bass","violin","flute","clarinet","cello"] },
688
+ octaveRange: { type: "integer", description: "Octave span for arp.", default: 1, minimum: 1, maximum: 4 },
689
+ baseOctave: { type: "integer", description: "Starting MIDI octave.", default: 3, minimum: 1, maximum: 5 },
690
+ gatePercent: { type: "number", description: "Note gate as fraction of step.", default: 0.9, minimum: 0.1, maximum: 1.0 },
691
+ velocity: { type: "integer", description: "Base MIDI velocity.", default: 90, minimum: 1, maximum: 127 },
692
+ velRandom: { type: "integer", description: "Velocity randomization +/-.", default: 5, minimum: 0, maximum: 30 },
693
+ accentEvery: { type: "integer", description: "Accent every N steps. 0=off.", default: 4, minimum: 0, maximum: 16 },
694
+ accentAmount: { type: "integer", description: "Accent velocity boost.", default: 20, minimum: 0, maximum: 50 },
695
+ firstBeatAccent:{ type: "boolean", description: "Accent first beat of each chord.", default: true },
696
+ swing: { type: "number", description: "Swing. 0.5=straight, 0.66=medium, 0.75=heavy.", default: 0.5, minimum: 0.5, maximum: 0.75 },
697
+ loops: { type: "integer", description: "Repetitions of progression.", default: 2, minimum: 1, maximum: 16 },
698
+ enablePad: { type: "boolean", description: "Sustained pad chord track.", default: true },
699
+ padOctave: { type: "integer", description: "Pad octave.", default: 4, minimum: 2, maximum: 5 },
700
+ padVolume: { type: "number", description: "Pad volume.", default: 0.12, minimum: 0.0, maximum: 0.3 },
701
+ enableBass: { type: "boolean", description: "Auto bass (uses slash chord bass).", default: true },
702
+ enableDrums: { type: "boolean", description: "Drum track.", default: true },
703
+ drumPattern: { type: "string", description: "Drum pattern.", default: "poprock", enum: ["8beat","16beat","poprock","acousticpop","bossanova","swing","funk","rnb","waltz","arpeggio","none"] },
704
+ reverbMix: { type: "number", description: "Reverb wet/dry.", default: 0.18, minimum: 0.0, maximum: 0.5 },
705
+ masterVolume: { type: "number", description: "Master volume.", default: 0.85, minimum: 0.0, maximum: 1.0 }
706
+ }
707
+ };
708
+
709
+ module.exports = {
710
+ renderJob, encodeWAV, PRESETS, PATTERNS, INSTRUMENTS, DRUMS, RATES, SCHEMA, SAMPLE_RATE,
711
+ parseProgression, generateMidiEvents, midiToFreq, PrebuiltProgressions, ArpPatternNames, BeatPatterns, RateValues
712
+ };