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.
Files changed (50) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +272 -0
  3. package/package.json +30 -0
  4. package/src/arrangement.js +282 -0
  5. package/src/arrangement.ts +317 -0
  6. package/src/assets/cover.png +0 -0
  7. package/src/cli.js +1398 -0
  8. package/src/cli.ts +1709 -0
  9. package/src/exportAudio.js +459 -0
  10. package/src/exportAudio.ts +499 -0
  11. package/src/exportMidi.js +85 -0
  12. package/src/exportMidi.ts +103 -0
  13. package/src/ffmpeg-static.d.ts +5 -0
  14. package/src/fx.js +28 -0
  15. package/src/fx.ts +49 -0
  16. package/src/generator.js +15 -0
  17. package/src/generator.ts +29 -0
  18. package/src/index.js +511 -0
  19. package/src/index.ts +642 -0
  20. package/src/instrument.js +35 -0
  21. package/src/instrument.ts +51 -0
  22. package/src/midi.js +167 -0
  23. package/src/midi.ts +218 -0
  24. package/src/openExport.js +22 -0
  25. package/src/openExport.ts +24 -0
  26. package/src/prompt.js +22 -0
  27. package/src/prompt.ts +25 -0
  28. package/src/providers/auth.js +23 -0
  29. package/src/providers/auth.ts +30 -0
  30. package/src/providers/claudeProvider.js +46 -0
  31. package/src/providers/claudeProvider.ts +50 -0
  32. package/src/providers/factory.js +39 -0
  33. package/src/providers/factory.ts +43 -0
  34. package/src/providers/geminiProvider.js +55 -0
  35. package/src/providers/geminiProvider.ts +71 -0
  36. package/src/providers/grokProvider.js +57 -0
  37. package/src/providers/grokProvider.ts +69 -0
  38. package/src/providers/groqProvider.js +57 -0
  39. package/src/providers/groqProvider.ts +69 -0
  40. package/src/providers/mockProvider.js +13 -0
  41. package/src/providers/mockProvider.ts +15 -0
  42. package/src/providers/openaiProvider.js +45 -0
  43. package/src/providers/openaiProvider.ts +49 -0
  44. package/src/providers/retry.js +46 -0
  45. package/src/providers/retry.ts +54 -0
  46. package/src/types.js +1 -0
  47. package/src/types.ts +17 -0
  48. package/src/validate.js +10 -0
  49. package/src/validate.ts +13 -0
  50. 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