@hraness/dawg 0.3.0 → 0.4.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 (81) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/DAWG.md +605 -39
  3. package/README.md +4 -4
  4. package/core/chords.ts +1724 -0
  5. package/core/diff.ts +13 -1
  6. package/core/euclid.ts +670 -0
  7. package/core/fx.ts +1065 -0
  8. package/core/kits.ts +320 -0
  9. package/core/params.ts +111 -0
  10. package/core/rhythm.ts +287 -0
  11. package/core/score.ts +735 -18
  12. package/core/sdk/eval-child.ts +34 -22
  13. package/core/sdk/eval.ts +4 -1
  14. package/core/sdk/print.ts +304 -22
  15. package/core/sdk/sync-chords.ts +54 -0
  16. package/core/sdk/v1.ts +3226 -21
  17. package/core/synth.ts +1001 -0
  18. package/package.json +1 -1
  19. package/src/agent/agent.ts +38 -3
  20. package/src/agent/brief.ts +29 -4
  21. package/src/agent/chord-tools.ts +357 -0
  22. package/src/agent/drum-tools.ts +135 -0
  23. package/src/agent/models.ts +4 -4
  24. package/src/agent/pack-tools.ts +369 -0
  25. package/src/agent/planner.ts +17 -2
  26. package/src/agent/preview-tool.ts +270 -0
  27. package/src/agent/rhythm-tools.ts +145 -0
  28. package/src/agent/tools.ts +308 -14
  29. package/src/agent/xcb-agent.ts +8 -2
  30. package/src/audio/audition.ts +93 -0
  31. package/src/audio/cache.ts +160 -0
  32. package/src/audio/effects/bus.ts +147 -0
  33. package/src/audio/effects/chain.ts +99 -0
  34. package/src/audio/effects/common.ts +251 -0
  35. package/src/audio/effects/convolution.ts +301 -0
  36. package/src/audio/effects/drive.ts +142 -0
  37. package/src/audio/effects/duck.ts +122 -0
  38. package/src/audio/effects/dynamics.ts +121 -0
  39. package/src/audio/effects/filter.ts +319 -0
  40. package/src/audio/effects/modulation.ts +174 -0
  41. package/src/audio/effects/space.ts +337 -0
  42. package/src/audio/engine.ts +27 -4
  43. package/src/audio/kits.ts +200 -0
  44. package/src/audio/packs.ts +1787 -0
  45. package/src/audio/preview.ts +470 -0
  46. package/src/audio/random.ts +15 -0
  47. package/src/audio/sampler.ts +157 -29
  48. package/src/audio/samples.ts +386 -44
  49. package/src/audio/synth/oscillators.ts +268 -0
  50. package/src/audio/synth/voice.ts +555 -0
  51. package/src/audio/synth/zzfx.ts +137 -0
  52. package/src/audio/wav.ts +280 -278
  53. package/src/audio/wavetable-maker.ts +717 -0
  54. package/src/audio/wavetable.ts +624 -0
  55. package/src/commands/drums.ts +299 -0
  56. package/src/commands/edit.ts +26 -4
  57. package/src/commands/fx.ts +360 -0
  58. package/src/commands/help.ts +298 -10
  59. package/src/commands/music.ts +10 -8
  60. package/src/commands/pack.ts +423 -0
  61. package/src/commands/rhythm.ts +230 -0
  62. package/src/commands/sample.ts +162 -2
  63. package/src/commands/synth.ts +229 -0
  64. package/src/commands/wavetable.ts +370 -0
  65. package/src/main.ts +1119 -30
  66. package/src/media/cli.ts +15 -2
  67. package/src/media/tools.ts +106 -1
  68. package/src/media/wavetable.ts +202 -0
  69. package/src/render.ts +22 -1
  70. package/src/session/naming.ts +3 -1
  71. package/src/tui/audition.ts +501 -0
  72. package/src/tui/euclid.ts +472 -0
  73. package/src/tui/menu.ts +1286 -244
  74. package/src/tui/play-chords.ts +538 -0
  75. package/src/tui/play-session.ts +364 -16
  76. package/src/tui/sketch.ts +108 -0
  77. package/src/web/search.ts +4 -1
  78. package/tui/app.ts +161 -39
  79. package/tui/grammar.ts +281 -0
  80. package/tui/highway.ts +7 -1
  81. package/tui/play-strip.ts +59 -14
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Oscillators of the synth voice, behind one seam: `resolveOscillator`
3
+ * maps a sound name to a factory, and `registerOscillatorResolver` lets
4
+ * another module (the wavetable lane) add sounds without touching the
5
+ * voice. Everything else a voice does (envelopes, unison, FM, filters)
6
+ * lives in voice.ts and applies to every oscillator.
7
+ *
8
+ * Band-limiting: sawtooth, square and pulse use PolyBLEP residuals
9
+ * (Välimäki & Huovilainen, "Antialiasing oscillators in subtractive
10
+ * synthesis", 2007); additive partials above Nyquist are skipped. Noise is
11
+ * seeded per note so a note sounds the same on every render.
12
+ */
13
+
14
+ /** What a factory sees when a note starts. */
15
+ export type OscillatorInit = Readonly<{
16
+ sound: string;
17
+ sampleRate: number;
18
+ /** Per-note PRNG, 0..1; deterministic for a note id and start. */
19
+ random: () => number;
20
+ /** Resolved `partials`/`phases`, when set. */
21
+ partials?: readonly number[];
22
+ phases?: readonly number[];
23
+ /** Current pulse width 0..1, read every sample by pulse-like sounds. */
24
+ width: () => number;
25
+ /** Crackle density 0..1. */
26
+ density: number;
27
+ /**
28
+ * The track's raw `synth` value for a parameter (undefined when unset),
29
+ * so an oscillator registered elsewhere can read its own parameters.
30
+ */
31
+ param: (name: string) => unknown;
32
+ /** Seconds since the note started, advanced per sample by the voice. */
33
+ time: () => number;
34
+ }>;
35
+
36
+ /**
37
+ * One oscillator instance (one unison voice of one note). `phase` is in
38
+ * cycles (any real), `increment` the phase step of this sample; noise
39
+ * sounds ignore both.
40
+ */
41
+ export type Oscillator = (phase: number, increment: number) => number;
42
+
43
+ export type OscillatorFactory = (init: OscillatorInit) => Oscillator;
44
+
45
+ /** `undefined` when the resolver does not know the sound. */
46
+ export type OscillatorResolver = (
47
+ sound: string,
48
+ ) => OscillatorFactory | undefined;
49
+
50
+ const resolvers: OscillatorResolver[] = [];
51
+
52
+ /** Add sounds (checked before the built-ins, newest first). */
53
+ export function registerOscillatorResolver(resolver: OscillatorResolver): void {
54
+ resolvers.unshift(resolver);
55
+ }
56
+
57
+ /** The factory for a sound, or `undefined` for an unknown name. */
58
+ export function resolveOscillator(
59
+ sound: string,
60
+ ): OscillatorFactory | undefined {
61
+ const name = sound.trim().toLowerCase();
62
+ for (const resolver of resolvers) {
63
+ const factory = resolver(name);
64
+ if (factory) return factory;
65
+ }
66
+ return BUILT_IN[name];
67
+ }
68
+
69
+ /** PolyBLEP residual for a unit step at phase 0 (t in cycles 0..1). */
70
+ export function polyBlep(t: number, dt: number): number {
71
+ if (dt <= 0) return 0;
72
+ if (t < dt) {
73
+ const x = t / dt;
74
+ return x + x - x * x - 1;
75
+ }
76
+ if (t > 1 - dt) {
77
+ const x = (t - 1) / dt;
78
+ return x * x + x + x + 1;
79
+ }
80
+ return 0;
81
+ }
82
+
83
+ function frac(value: number): number {
84
+ return value - Math.floor(value);
85
+ }
86
+
87
+ function sawtooth(phase: number, increment: number): number {
88
+ const t = frac(phase);
89
+ return 2 * t - 1 - polyBlep(t, Math.abs(increment));
90
+ }
91
+
92
+ function pulse(phase: number, increment: number, width: number): number {
93
+ const t = frac(phase);
94
+ const dt = Math.abs(increment);
95
+ const w = Math.min(0.99, Math.max(0.01, width));
96
+ let value = t < w ? 1 : -1;
97
+ value += polyBlep(t, dt);
98
+ value -= polyBlep(frac(t - w + 1), dt);
99
+ // Remove the DC a non-50% pulse carries so width sweeps don't thump.
100
+ return value - (2 * w - 1);
101
+ }
102
+
103
+ function triangle(phase: number): number {
104
+ return 1 - 4 * Math.abs(frac(phase) - 0.5);
105
+ }
106
+
107
+ /**
108
+ * dawg's original tone colours (piano, pluck, bass), unchanged so the
109
+ * legacy voice renders byte-identically; also the oscillator for those
110
+ * names when a track sets synth parameters.
111
+ */
112
+ export function legacyWave(instrument: string, phase: number): number {
113
+ const name = instrument.trim().toLowerCase();
114
+ const cycle = phase - Math.floor(phase);
115
+ const sine = Math.sin(2 * Math.PI * phase);
116
+ if (name.includes("square")) return cycle < 0.5 ? 1 : -1;
117
+ if (name.includes("saw")) return 2 * cycle - 1;
118
+ if (name.includes("triangle")) return 1 - 4 * Math.abs(cycle - 0.5);
119
+ if (name.includes("bass")) {
120
+ // A rounded fundamental plus a quiet octave gives bass tracks useful weight.
121
+ return Math.tanh(
122
+ 0.9 * Math.sin(2 * Math.PI * phase) +
123
+ 0.25 * Math.sin(4 * Math.PI * phase),
124
+ );
125
+ }
126
+ if (name.includes("piano") || name.includes("pluck")) {
127
+ // Add stable harmonics; the envelope above supplies the note decay.
128
+ const harmonic =
129
+ Math.sin(4 * Math.PI * phase) * 0.28 +
130
+ Math.sin(6 * Math.PI * phase) * 0.12;
131
+ return Math.tanh(sine + harmonic);
132
+ }
133
+ // Unknown instruments deliberately fall back to the original sine voice.
134
+ return sine;
135
+ }
136
+
137
+ /**
138
+ * Harmonic amplitudes of a basic waveform (Fourier series), scaled by
139
+ * `partials` when given: `sawtooth` 1/h, `square` odd 1/h, `triangle`
140
+ * odd ±1/h², `sine`/`user` the partials alone.
141
+ */
142
+ function harmonicSeries(
143
+ sound: string,
144
+ partials: readonly number[] | undefined,
145
+ ): number[] {
146
+ const count = partials?.length ?? 1;
147
+ const out: number[] = [];
148
+ for (let index = 0; index < count; index += 1) {
149
+ const h = index + 1;
150
+ const user = partials?.[index] ?? 1;
151
+ let base = 1;
152
+ if (sound === "sawtooth" || sound === "saw" || sound === "supersaw")
153
+ base = 1 / h;
154
+ else if (sound === "square" || sound === "pulse")
155
+ base = h % 2 === 1 ? 1 / h : 0;
156
+ else if (sound === "triangle")
157
+ base =
158
+ h % 2 === 1
159
+ ? ((h - 1) / 2) % 2 === 0
160
+ ? 1 / (h * h)
161
+ : -1 / (h * h)
162
+ : 0;
163
+ out.push(base * user);
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /** Additive oscillator: partials above Nyquist are skipped. */
169
+ function additive(init: OscillatorInit): Oscillator {
170
+ const amplitudes = harmonicSeries(init.sound, init.partials ?? [1]);
171
+ const phases = init.phases ?? [];
172
+ const norm = Math.max(
173
+ 1,
174
+ amplitudes.reduce((sum, value) => sum + Math.abs(value), 0),
175
+ );
176
+ const count = amplitudes.length;
177
+ return (phase, increment) => {
178
+ let sum = 0;
179
+ const limit = Math.abs(increment) > 0 ? 0.5 / Math.abs(increment) : count;
180
+ for (let index = 0; index < count && index + 1 < limit; index += 1) {
181
+ const a = amplitudes[index]!;
182
+ if (a === 0) continue;
183
+ sum +=
184
+ a *
185
+ Math.sin(2 * Math.PI * ((index + 1) * phase + (phases[index] ?? 0)));
186
+ }
187
+ return sum / norm;
188
+ };
189
+ }
190
+
191
+ /** Paul Kellet's refined pink filter (public-domain, ±0.05 dB above 9 Hz). */
192
+ function pinkNoise(random: () => number): () => number {
193
+ let b0 = 0;
194
+ let b1 = 0;
195
+ let b2 = 0;
196
+ let b3 = 0;
197
+ let b4 = 0;
198
+ let b5 = 0;
199
+ let b6 = 0;
200
+ return () => {
201
+ const white = random() * 2 - 1;
202
+ b0 = 0.99886 * b0 + white * 0.0555179;
203
+ b1 = 0.99332 * b1 + white * 0.0750759;
204
+ b2 = 0.969 * b2 + white * 0.153852;
205
+ b3 = 0.8665 * b3 + white * 0.3104856;
206
+ b4 = 0.55 * b4 + white * 0.5329522;
207
+ b5 = -0.7616 * b5 - white * 0.016898;
208
+ const value = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
209
+ b6 = white * 0.115926;
210
+ // Scaled to white noise's RMS (≈0.35) so the colours swap evenly.
211
+ return value * 0.2;
212
+ };
213
+ }
214
+
215
+ /** A pink-noise source for the `noise` mix control. */
216
+ export function makePinkNoise(random: () => number): () => number {
217
+ return pinkNoise(random);
218
+ }
219
+
220
+ const BUILT_IN: Readonly<Record<string, OscillatorFactory>> = Object.freeze({
221
+ sine: (init) =>
222
+ init.partials ? additive(init) : (phase) => Math.sin(2 * Math.PI * phase),
223
+ sawtooth: (init) => (init.partials ? additive(init) : sawtooth),
224
+ saw: (init) => (init.partials ? additive(init) : sawtooth),
225
+ supersaw: (init) => (init.partials ? additive(init) : sawtooth),
226
+ square: (init) =>
227
+ init.partials
228
+ ? additive(init)
229
+ : (phase, increment) => pulse(phase, increment, 0.5),
230
+ pulse: (init) => (phase, increment) => pulse(phase, increment, init.width()),
231
+ triangle: (init) => (init.partials ? additive(init) : triangle),
232
+ user: (init) => additive(init),
233
+ piano: () => (phase) => legacyWave("piano", phase),
234
+ pluck: () => (phase) => legacyWave("pluck", phase),
235
+ bass: () => (phase) => legacyWave("bass", phase),
236
+ white: (init) => () => (init.random() * 2 - 1) * 0.6,
237
+ pink: (init) => pinkNoise(init.random),
238
+ brown: (init) => {
239
+ let state = 0;
240
+ return () => {
241
+ // Leaky integration of white noise: a −6 dB/octave slope.
242
+ state = (state + 0.02 * (init.random() * 2 - 1)) / 1.02;
243
+ return state * 6.1;
244
+ };
245
+ },
246
+ crackle: (init) => {
247
+ const chance = Math.min(1, (init.density * 1000) / init.sampleRate);
248
+ return () => {
249
+ const hit = init.random() < chance;
250
+ const value = init.random() * 2 - 1;
251
+ return hit ? value : 0;
252
+ };
253
+ },
254
+ // ZzFX sounds render through their own generator (zzfx.ts); these plain
255
+ // waves only register the names (and serve as their seam fallback).
256
+ z_sine: () => (phase) => Math.sin(2 * Math.PI * phase),
257
+ z_triangle: () => triangle,
258
+ z_sawtooth: () => sawtooth,
259
+ z_square: () => (phase, increment) => pulse(phase, increment, 0.5),
260
+ z_tan: () => (phase) =>
261
+ Math.max(-1, Math.min(1, Math.tan(Math.PI * (phase - Math.floor(phase))))),
262
+ z_noise: (init) => () => init.random() * 2 - 1,
263
+ });
264
+
265
+ /** Every built-in sound name. */
266
+ export const BUILT_IN_SOUNDS: readonly string[] = Object.freeze(
267
+ Object.keys(BUILT_IN),
268
+ );