opennote-cli 1.3.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 +22 -0
- package/README.md +272 -0
- package/package.json +30 -0
- package/src/arrangement.js +282 -0
- package/src/arrangement.ts +317 -0
- package/src/assets/cover.png +0 -0
- package/src/cli.js +1398 -0
- package/src/cli.ts +1709 -0
- package/src/exportAudio.js +459 -0
- package/src/exportAudio.ts +499 -0
- package/src/exportMidi.js +85 -0
- package/src/exportMidi.ts +103 -0
- package/src/ffmpeg-static.d.ts +5 -0
- package/src/fx.js +28 -0
- package/src/fx.ts +49 -0
- package/src/generator.js +15 -0
- package/src/generator.ts +29 -0
- package/src/index.js +511 -0
- package/src/index.ts +642 -0
- package/src/instrument.js +35 -0
- package/src/instrument.ts +51 -0
- package/src/midi.js +167 -0
- package/src/midi.ts +218 -0
- package/src/openExport.js +22 -0
- package/src/openExport.ts +24 -0
- package/src/prompt.js +22 -0
- package/src/prompt.ts +25 -0
- package/src/providers/auth.js +23 -0
- package/src/providers/auth.ts +30 -0
- package/src/providers/claudeProvider.js +46 -0
- package/src/providers/claudeProvider.ts +50 -0
- package/src/providers/factory.js +39 -0
- package/src/providers/factory.ts +43 -0
- package/src/providers/geminiProvider.js +55 -0
- package/src/providers/geminiProvider.ts +71 -0
- package/src/providers/grokProvider.js +57 -0
- package/src/providers/grokProvider.ts +69 -0
- package/src/providers/groqProvider.js +57 -0
- package/src/providers/groqProvider.ts +69 -0
- package/src/providers/mockProvider.js +13 -0
- package/src/providers/mockProvider.ts +15 -0
- package/src/providers/openaiProvider.js +45 -0
- package/src/providers/openaiProvider.ts +49 -0
- package/src/providers/retry.js +46 -0
- package/src/providers/retry.ts +54 -0
- package/src/types.js +1 -0
- package/src/types.ts +17 -0
- package/src/validate.js +10 -0
- package/src/validate.ts +13 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import type { GeneratedNote } from './types';
|
|
2
|
+
|
|
3
|
+
export type GenerationMode = 'single' | 'backing';
|
|
4
|
+
export type PitchRange = 'low' | 'mid' | 'high';
|
|
5
|
+
export type GateStyle = 'tight' | 'balanced' | 'long';
|
|
6
|
+
export type ModRate = 'off' | 'slow' | 'med' | 'fast';
|
|
7
|
+
export type ModTarget = 'velocity' | 'duration' | 'pitch';
|
|
8
|
+
export type MetronomeMode = 'off' | 'count-in' | 'always';
|
|
9
|
+
export type GrowthStyle = 'flat' | 'build';
|
|
10
|
+
export type TimingFeel = 'tight' | 'human' | 'offbeat' | 'loose';
|
|
11
|
+
|
|
12
|
+
export type NoteEvent = {
|
|
13
|
+
pitch: number;
|
|
14
|
+
velocity: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
startMs: number;
|
|
17
|
+
channel: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BackingControls = {
|
|
21
|
+
drums: boolean;
|
|
22
|
+
bass: boolean;
|
|
23
|
+
clap: boolean;
|
|
24
|
+
openHat: boolean;
|
|
25
|
+
perc: boolean;
|
|
26
|
+
metronome: MetronomeMode;
|
|
27
|
+
swing: number; // 0..100
|
|
28
|
+
gate: GateStyle;
|
|
29
|
+
mutate: number; // 0..100
|
|
30
|
+
deviate: number; // 0..100
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PitchControls = {
|
|
34
|
+
transpose: number; // -12..12
|
|
35
|
+
range: PitchRange;
|
|
36
|
+
snapScale: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ModulateControls = {
|
|
40
|
+
rate: ModRate;
|
|
41
|
+
depth: number; // 0..100
|
|
42
|
+
target: ModTarget;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
|
|
46
|
+
|
|
47
|
+
function clamp(n: number, min: number, max: number): number {
|
|
48
|
+
return Math.max(min, Math.min(max, n));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rangeBounds(range: PitchRange): [number, number] {
|
|
52
|
+
if (range === 'low') return [28, 60];
|
|
53
|
+
if (range === 'high') return [67, 108];
|
|
54
|
+
return [48, 84];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function closestScalePitch(p: number): number {
|
|
58
|
+
let best = p;
|
|
59
|
+
let bestDist = Infinity;
|
|
60
|
+
for (let candidate = p - 6; candidate <= p + 6; candidate++) {
|
|
61
|
+
const pc = ((candidate % 12) + 12) % 12;
|
|
62
|
+
if (!MAJOR_SCALE.includes(pc)) continue;
|
|
63
|
+
const d = Math.abs(candidate - p);
|
|
64
|
+
if (d < bestDist) {
|
|
65
|
+
best = candidate;
|
|
66
|
+
bestDist = d;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function gateMultiplier(gate: GateStyle): number {
|
|
73
|
+
if (gate === 'tight') return 0.7;
|
|
74
|
+
if (gate === 'long') return 1.25;
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modRateCycles(rate: ModRate, n: number): number {
|
|
79
|
+
if (rate === 'off') return 0;
|
|
80
|
+
if (rate === 'slow') return Math.max(1, n / 16);
|
|
81
|
+
if (rate === 'fast') return Math.max(2, n / 4);
|
|
82
|
+
return Math.max(1.5, n / 8);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function lfo(i: number, n: number, rate: ModRate): number {
|
|
86
|
+
const cycles = modRateCycles(rate, n);
|
|
87
|
+
if (cycles === 0) return 0;
|
|
88
|
+
const phase = (i / Math.max(1, n - 1)) * Math.PI * 2 * cycles;
|
|
89
|
+
return Math.sin(phase);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function swingify(sequence: GeneratedNote[], swing: number): GeneratedNote[] {
|
|
93
|
+
if (swing <= 0) return sequence;
|
|
94
|
+
const amt = clamp(swing / 100, 0, 1) * 0.22;
|
|
95
|
+
return sequence.map((n, i) => ({
|
|
96
|
+
...n,
|
|
97
|
+
durationMs: Math.max(30, Math.round(n.durationMs * (i % 2 === 0 ? 1 + amt : 1 - amt))),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mutDeviate(sequence: GeneratedNote[], mutate: number, deviate: number): GeneratedNote[] {
|
|
102
|
+
const mutateChance = clamp(mutate, 0, 100) / 100;
|
|
103
|
+
const dev = clamp(deviate, 0, 100) / 100;
|
|
104
|
+
return sequence.map((n) => {
|
|
105
|
+
let pitch = n.pitch;
|
|
106
|
+
let velocity = n.velocity;
|
|
107
|
+
let durationMs = n.durationMs;
|
|
108
|
+
if (Math.random() < mutateChance) {
|
|
109
|
+
pitch += Math.round((Math.random() * 2 - 1) * (2 + dev * 8));
|
|
110
|
+
velocity += Math.round((Math.random() * 2 - 1) * (6 + dev * 18));
|
|
111
|
+
durationMs += Math.round((Math.random() * 2 - 1) * (25 + dev * 180));
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
pitch: clamp(pitch, 0, 127),
|
|
115
|
+
velocity: clamp(velocity, 1, 127),
|
|
116
|
+
durationMs: clamp(durationMs, 35, 5000),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function transformMelody(
|
|
122
|
+
sequence: GeneratedNote[],
|
|
123
|
+
pitch: PitchControls,
|
|
124
|
+
mod: ModulateControls,
|
|
125
|
+
backing: BackingControls,
|
|
126
|
+
): GeneratedNote[] {
|
|
127
|
+
const [minP, maxP] = rangeBounds(pitch.range);
|
|
128
|
+
const gMul = gateMultiplier(backing.gate);
|
|
129
|
+
const depth = clamp(mod.depth, 0, 100) / 100;
|
|
130
|
+
const base = sequence.map((n, i, arr) => {
|
|
131
|
+
let p = clamp(n.pitch + clamp(pitch.transpose, -12, 12), minP, maxP);
|
|
132
|
+
if (pitch.snapScale) p = closestScalePitch(p);
|
|
133
|
+
let v = n.velocity;
|
|
134
|
+
let d = Math.max(35, Math.round(n.durationMs * gMul));
|
|
135
|
+
const modv = lfo(i, arr.length, mod.rate) * depth;
|
|
136
|
+
if (mod.target === 'pitch') p = clamp(Math.round(p + modv * 2.5), minP, maxP);
|
|
137
|
+
if (mod.target === 'velocity') v = clamp(Math.round(v + modv * 22), 1, 127);
|
|
138
|
+
if (mod.target === 'duration') d = clamp(Math.round(d * (1 + modv * 0.35)), 35, 6000);
|
|
139
|
+
return { pitch: p, velocity: v, durationMs: d };
|
|
140
|
+
});
|
|
141
|
+
const swung = swingify(base, backing.swing);
|
|
142
|
+
return mutDeviate(swung, backing.mutate, backing.deviate);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function applyGrowthAndDuration(
|
|
146
|
+
sequence: GeneratedNote[],
|
|
147
|
+
growth: GrowthStyle,
|
|
148
|
+
durationStretch: number,
|
|
149
|
+
): GeneratedNote[] {
|
|
150
|
+
const stretch = clamp(durationStretch, 1, 4);
|
|
151
|
+
if (growth === 'flat' && Math.abs(stretch - 1) < 0.001) return sequence;
|
|
152
|
+
|
|
153
|
+
return sequence.map((n, i, arr) => {
|
|
154
|
+
const progress = arr.length <= 1 ? 1 : i / (arr.length - 1);
|
|
155
|
+
const complexity = Math.pow(progress, 1.35);
|
|
156
|
+
let pitch = n.pitch;
|
|
157
|
+
let velocity = n.velocity;
|
|
158
|
+
let durationMs = Math.round(n.durationMs * stretch);
|
|
159
|
+
|
|
160
|
+
if (growth === 'build') {
|
|
161
|
+
velocity += Math.round(complexity * 20);
|
|
162
|
+
durationMs = Math.round(durationMs * (0.95 + complexity * 0.35));
|
|
163
|
+
if (complexity > 0.55 && i % 5 === 4) pitch += 1;
|
|
164
|
+
if (complexity > 0.78 && i % 7 === 6) pitch += 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
pitch: clamp(pitch, 0, 127),
|
|
169
|
+
velocity: clamp(velocity, 1, 127),
|
|
170
|
+
durationMs: clamp(durationMs, 35, 8000),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function sequenceToEvents(sequence: GeneratedNote[], channel: number): NoteEvent[] {
|
|
176
|
+
const out: NoteEvent[] = [];
|
|
177
|
+
let t = 0;
|
|
178
|
+
for (const n of sequence) {
|
|
179
|
+
out.push({
|
|
180
|
+
pitch: clamp(Math.round(n.pitch), 0, 127),
|
|
181
|
+
velocity: clamp(Math.round(n.velocity), 1, 127),
|
|
182
|
+
durationMs: Math.max(35, Math.round(n.durationMs)),
|
|
183
|
+
startMs: t,
|
|
184
|
+
channel: clamp(channel, 0, 15),
|
|
185
|
+
});
|
|
186
|
+
t += Math.max(35, Math.round(n.durationMs));
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function applyTimingFeel(
|
|
192
|
+
events: NoteEvent[],
|
|
193
|
+
bpm: number,
|
|
194
|
+
feel: TimingFeel,
|
|
195
|
+
amount: number,
|
|
196
|
+
): NoteEvent[] {
|
|
197
|
+
if (feel === 'tight' || amount <= 0 || events.length === 0) return events;
|
|
198
|
+
const beatMs = 60000 / Math.max(1, bpm);
|
|
199
|
+
const clampedAmount = clamp(amount, 0, 100) / 100;
|
|
200
|
+
const maxShiftMs = feel === 'human'
|
|
201
|
+
? beatMs * 0.05 * clampedAmount
|
|
202
|
+
: feel === 'offbeat'
|
|
203
|
+
? beatMs * 0.18 * clampedAmount
|
|
204
|
+
: beatMs * 0.12 * clampedAmount;
|
|
205
|
+
|
|
206
|
+
const shifted = events.map((e, i) => {
|
|
207
|
+
if (e.channel === 9 && feel === 'offbeat') {
|
|
208
|
+
// Keep downbeat kicks in place; move hats/perc around them.
|
|
209
|
+
const isKick = e.pitch === 36 || e.pitch === 35;
|
|
210
|
+
if (isKick) return { ...e };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let delta = 0;
|
|
214
|
+
if (feel === 'human') {
|
|
215
|
+
delta = (Math.random() * 2 - 1) * maxShiftMs;
|
|
216
|
+
} else if (feel === 'offbeat') {
|
|
217
|
+
const dir = i % 2 === 0 ? 1 : -1;
|
|
218
|
+
delta = dir * maxShiftMs;
|
|
219
|
+
} else {
|
|
220
|
+
// loose: mixed push/pull with slightly stronger random.
|
|
221
|
+
const dir = i % 3 === 0 ? 1 : -1;
|
|
222
|
+
delta = dir * maxShiftMs * 0.6 + (Math.random() * 2 - 1) * maxShiftMs * 0.6;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...e,
|
|
227
|
+
startMs: Math.max(0, Math.round(e.startMs + delta)),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return shifted.sort((a, b) => a.startMs - b.startMs);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function pickStyle(theme: string): 'techno' | 'trap' | 'ambient' | 'other' {
|
|
235
|
+
const t = theme.toLowerCase();
|
|
236
|
+
if (t.includes('techno') || t.includes('house')) return 'techno';
|
|
237
|
+
if (t.includes('trap') || t.includes('hip')) return 'trap';
|
|
238
|
+
if (t.includes('ambient') || t.includes('cinematic')) return 'ambient';
|
|
239
|
+
return 'other';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildBackingEvents(
|
|
243
|
+
melodyEvents: NoteEvent[],
|
|
244
|
+
theme: string,
|
|
245
|
+
bpm: number,
|
|
246
|
+
controls: BackingControls,
|
|
247
|
+
growth: GrowthStyle = 'flat',
|
|
248
|
+
): NoteEvent[] {
|
|
249
|
+
if (!controls.drums && !controls.bass && !controls.clap && !controls.openHat && !controls.perc) return [];
|
|
250
|
+
const out: NoteEvent[] = [];
|
|
251
|
+
const style = pickStyle(theme);
|
|
252
|
+
const beatMs = 60000 / Math.max(1, bpm);
|
|
253
|
+
const endMs = melodyEvents.length
|
|
254
|
+
? Math.max(...melodyEvents.map((e) => e.startMs + e.durationMs))
|
|
255
|
+
: beatMs * 8;
|
|
256
|
+
const bars = Math.max(1, Math.ceil(endMs / (beatMs * 4)));
|
|
257
|
+
|
|
258
|
+
if (controls.drums) {
|
|
259
|
+
for (let bar = 0; bar < bars; bar++) {
|
|
260
|
+
const buildProgress = bars <= 1 ? 1 : bar / (bars - 1);
|
|
261
|
+
const allowSnare = growth === 'build' ? buildProgress > 0.28 : true;
|
|
262
|
+
const allowHat = growth === 'build' ? buildProgress > 0.52 : true;
|
|
263
|
+
const allowPerc = growth === 'build' ? buildProgress > 0.65 : true;
|
|
264
|
+
const barStart = bar * beatMs * 4;
|
|
265
|
+
for (let step = 0; step < 16; step++) {
|
|
266
|
+
const stepMs = beatMs / 4;
|
|
267
|
+
const t = barStart + step * stepMs;
|
|
268
|
+
const beat = step % 4 === 0;
|
|
269
|
+
const off = step % 2 === 0;
|
|
270
|
+
if (style === 'techno') {
|
|
271
|
+
if (beat) out.push({ pitch: 36, velocity: 104, durationMs: 90, startMs: t, channel: 9 }); // kick
|
|
272
|
+
if (allowSnare && step % 4 === 2) out.push({ pitch: 38, velocity: 95, durationMs: 90, startMs: t, channel: 9 }); // snare
|
|
273
|
+
if (allowHat && off) out.push({ pitch: 42, velocity: 92, durationMs: 45, startMs: t, channel: 9 }); // hihat
|
|
274
|
+
if (controls.clap && allowSnare && step % 8 === 4) out.push({ pitch: 39, velocity: 84, durationMs: 80, startMs: t, channel: 9 }); // clap
|
|
275
|
+
if (controls.openHat && allowHat && step % 8 === 6) out.push({ pitch: 46, velocity: 80, durationMs: 110, startMs: t, channel: 9 }); // open hat
|
|
276
|
+
if (controls.perc && allowPerc && step % 16 === 11) out.push({ pitch: 45, velocity: 76, durationMs: 90, startMs: t, channel: 9 }); // perc tom
|
|
277
|
+
} else if (style === 'trap') {
|
|
278
|
+
if (step === 0 || step === 10) out.push({ pitch: 36, velocity: 105, durationMs: 90, startMs: t, channel: 9 });
|
|
279
|
+
if (allowSnare && (step === 4 || step === 12)) out.push({ pitch: 38, velocity: 95, durationMs: 90, startMs: t, channel: 9 });
|
|
280
|
+
if (allowHat && step % 2 === 0) out.push({ pitch: 42, velocity: 88, durationMs: 35, startMs: t, channel: 9 });
|
|
281
|
+
if (controls.clap && allowSnare && (step === 4 || step === 12)) out.push({ pitch: 39, velocity: 80, durationMs: 70, startMs: t, channel: 9 });
|
|
282
|
+
if (controls.openHat && allowHat && (step === 7 || step === 15)) out.push({ pitch: 46, velocity: 78, durationMs: 95, startMs: t, channel: 9 });
|
|
283
|
+
if (controls.perc && allowPerc && step % 16 === 14) out.push({ pitch: 50, velocity: 72, durationMs: 90, startMs: t, channel: 9 });
|
|
284
|
+
} else if (style === 'ambient') {
|
|
285
|
+
if (step === 0) out.push({ pitch: 36, velocity: 70, durationMs: 120, startMs: t, channel: 9 });
|
|
286
|
+
if (allowSnare && step === 8) out.push({ pitch: 38, velocity: 64, durationMs: 120, startMs: t, channel: 9 });
|
|
287
|
+
if (allowHat && step % 4 === 0) out.push({ pitch: 42, velocity: 70, durationMs: 40, startMs: t, channel: 9 });
|
|
288
|
+
if (controls.clap && allowSnare && step === 12) out.push({ pitch: 39, velocity: 56, durationMs: 90, startMs: t, channel: 9 });
|
|
289
|
+
if (controls.openHat && allowHat && step === 10) out.push({ pitch: 46, velocity: 62, durationMs: 110, startMs: t, channel: 9 });
|
|
290
|
+
if (controls.perc && allowPerc && step === 14) out.push({ pitch: 75, velocity: 52, durationMs: 80, startMs: t, channel: 9 });
|
|
291
|
+
} else {
|
|
292
|
+
if (beat) out.push({ pitch: 36, velocity: 98, durationMs: 90, startMs: t, channel: 9 });
|
|
293
|
+
if (allowSnare && step % 8 === 4) out.push({ pitch: 38, velocity: 88, durationMs: 90, startMs: t, channel: 9 });
|
|
294
|
+
if (allowHat && off) out.push({ pitch: 42, velocity: 84, durationMs: 45, startMs: t, channel: 9 });
|
|
295
|
+
if (controls.clap && allowSnare && step % 8 === 4) out.push({ pitch: 39, velocity: 76, durationMs: 80, startMs: t, channel: 9 });
|
|
296
|
+
if (controls.openHat && allowHat && step % 8 === 6) out.push({ pitch: 46, velocity: 74, durationMs: 100, startMs: t, channel: 9 });
|
|
297
|
+
if (controls.perc && allowPerc && step % 16 === 13) out.push({ pitch: 50, velocity: 70, durationMs: 85, startMs: t, channel: 9 });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (controls.bass) {
|
|
304
|
+
for (const e of melodyEvents) {
|
|
305
|
+
if (e.startMs % Math.round(beatMs * 2) !== 0) continue;
|
|
306
|
+
out.push({
|
|
307
|
+
pitch: clamp(e.pitch - 24, 28, 60),
|
|
308
|
+
velocity: clamp(e.velocity - 10, 45, 110),
|
|
309
|
+
durationMs: clamp(Math.round(e.durationMs * 1.2), 80, 600),
|
|
310
|
+
startMs: e.startMs,
|
|
311
|
+
channel: 1,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return out.sort((a, b) => a.startMs - b.startMs);
|
|
317
|
+
}
|
|
Binary file
|