@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
@@ -7,6 +7,12 @@
7
7
  * - `gain` scales the voice; velocity and track volume apply as for synths.
8
8
  * - `choke` is Strudel's `cut`: a new hit in the same group stops the sounding
9
9
  * voice with a short fade.
10
+ * - `loopBegin`/`loopEnd` loop a part of the window; `clip` (`legato`) cuts
11
+ * the voice at the note's length times the factor; `unit` "c"/"s" and
12
+ * `fit` turn `speed` into a duration (cycles = bars, seconds, the note);
13
+ * `accelerate` ramps the rate; `squiz` raises pitch per zero-crossing
14
+ * cycle (described in Tidal/SuperDirt docs; implemented here from that
15
+ * description).
10
16
  * - keyed mode repitches from `root` (rate × 2^((pitch − root)/12)) and holds
11
17
  * for the note's length; oneshot mode maps voices to pitch slots from 36 in
12
18
  * voice-name order and plays the whole window unless `loop` is set.
@@ -61,8 +67,34 @@ export type SamplerVoice = {
61
67
  readonly regionEnd: number;
62
68
  readonly gain: number;
63
69
  readonly choke: string | undefined;
70
+ /** Loop part as travel offsets from the playback start (begin, or end reversed). */
71
+ readonly loopStart: number;
72
+ readonly loopSpan: number;
73
+ /** Rate ramp per output frame (`accelerate` / reference length); 0 is off. */
74
+ readonly ramp: number;
75
+ /** `squiz` ratio; 1 is off. */
76
+ readonly squiz: number;
64
77
  };
65
78
 
79
+ /**
80
+ * Source frames travelled after `elapsed` output frames: `step·(e + k·e²/2)`
81
+ * with the rate held at zero once a negative ramp reaches it.
82
+ */
83
+ function travelAt(elapsed: number, step: number, ramp: number): number {
84
+ if (ramp === 0) return elapsed * step;
85
+ const e = ramp < 0 ? Math.min(elapsed, -1 / ramp) : elapsed;
86
+ return step * (e + (ramp * e * e) / 2);
87
+ }
88
+
89
+ /** Output frames until `distance` source frames are travelled (Infinity if never). */
90
+ function framesToTravel(distance: number, step: number, ramp: number): number {
91
+ if (ramp === 0) return distance / step;
92
+ const d = distance / step;
93
+ const disc = 1 + 2 * ramp * d;
94
+ if (disc < 0) return Infinity;
95
+ return (Math.sqrt(disc) - 1) / ramp;
96
+ }
97
+
66
98
  function noteStartFrame(note: Note, timing: SamplerTiming): number {
67
99
  const { score, sampleRate } = timing;
68
100
  return Math.max(
@@ -148,26 +180,64 @@ export function planSamplerVoices(
148
180
  const sample = bank.voices.get(sampleKey(track.id, target.voice));
149
181
  if (!ref || !sample) continue;
150
182
  const speed = ref.speed ?? 1;
151
- const step =
152
- ((Math.abs(speed) * sample.sampleRate) / sampleRate) * target.ratio;
153
183
  const regionStart = (ref.begin ?? 0) * sample.frames;
154
184
  const regionEnd = (ref.end ?? 1) * sample.frames;
155
- const natural = Math.max(1, Math.floor((regionEnd - regionStart) / step));
156
185
  const held = noteLengthFrames(note, timing);
186
+ // `fit`, `unit: "c"` and `unit: "s"` give the window a duration; the
187
+ // keyed ratio still repitches on top.
188
+ const unit = ref.unit ?? "r";
189
+ const seconds = ref.fit
190
+ ? held / sampleRate
191
+ : unit === "c"
192
+ ? (timing.score.beatsPerBar * 60) /
193
+ timing.score.tempoBpm /
194
+ Math.abs(speed)
195
+ : unit === "s"
196
+ ? Math.abs(speed)
197
+ : undefined;
198
+ const step =
199
+ seconds === undefined
200
+ ? ((Math.abs(speed) * sample.sampleRate) / sampleRate) * target.ratio
201
+ : ((regionEnd - regionStart) / (seconds * sampleRate)) * target.ratio;
202
+ const natural = Math.max(1, Math.floor((regionEnd - regionStart) / step));
157
203
  const release = Math.max(1, Math.round(RELEASE_SECONDS * sampleRate));
158
204
  const loop = ref.loop === true;
205
+ const clipped =
206
+ ref.clip === undefined ? held : Math.max(1, Math.floor(held * ref.clip));
159
207
  let length: number;
160
208
  let fade: number;
161
209
  if (loop) {
162
- length = held + release;
210
+ length = clipped + release;
163
211
  fade = release;
164
- } else if (sampler.mode === "keyed" && held + release < natural) {
165
- length = held + release;
212
+ } else if (
213
+ (sampler.mode === "keyed" || ref.clip !== undefined) &&
214
+ clipped + release < natural
215
+ ) {
216
+ length = clipped + release;
166
217
  fade = release;
167
218
  } else {
168
219
  length = natural;
169
220
  fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
170
221
  }
222
+ // accelerate: the rate ramps by `accelerate`× over the planned length;
223
+ // a non-looping voice ends where the ramped read runs out (or stops).
224
+ const accelerate = ref.accelerate ?? 0;
225
+ const ramp = accelerate === 0 ? 0 : accelerate / length;
226
+ if (ramp !== 0 && !loop) {
227
+ const reach = Math.min(
228
+ framesToTravel(regionEnd - regionStart, step, ramp),
229
+ ramp < 0 ? -1 / ramp : Infinity,
230
+ );
231
+ const until = Math.max(1, Math.floor(reach));
232
+ if (until < length) {
233
+ length = until;
234
+ fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
235
+ }
236
+ }
237
+ const loopFrom =
238
+ ref.loopBegin === undefined ? regionStart : ref.loopBegin * sample.frames;
239
+ const loopTo =
240
+ ref.loopEnd === undefined ? regionEnd : ref.loopEnd * sample.frames;
171
241
  const start = noteStartFrame(note, timing);
172
242
  const voice: SamplerVoice = {
173
243
  voice: target.voice,
@@ -183,6 +253,10 @@ export function planSamplerVoices(
183
253
  regionEnd,
184
254
  gain: (ref.gain ?? 1) * Math.max(0, Math.min(1, note.velocity)),
185
255
  choke: ref.choke,
256
+ loopStart: speed < 0 ? regionEnd - loopTo : loopFrom - regionStart,
257
+ loopSpan: loopTo - loopFrom,
258
+ ramp,
259
+ squiz: ref.squiz ?? 1,
186
260
  };
187
261
  if (voice.choke !== undefined) {
188
262
  const previous = lastInGroup.get(voice.choke);
@@ -225,38 +299,44 @@ export function renderSamplerVoices(
225
299
  const attack = Math.max(1, ATTACK_SECONDS * sampleRate);
226
300
  for (const voice of voices) {
227
301
  const { sample, step, regionStart, regionEnd, reverse, loop } = voice;
302
+ const { loopStart, loopSpan, ramp } = voice;
228
303
  const mono = sample.mono;
229
304
  const span = regionEnd - regionStart;
230
305
  const crossfade = loop
231
- ? Math.min(LOOP_CROSSFADE_SECONDS * sample.sampleRate, span / 4)
306
+ ? Math.min(LOOP_CROSSFADE_SECONDS * sample.sampleRate, loopSpan / 4)
232
307
  : 0;
233
- const period = span - crossfade;
308
+ const period = loopSpan - crossfade;
234
309
  const end = Math.min(target.length, voice.end);
235
310
  const fadeFrom = voice.end - voice.fade;
236
311
  const level = SAMPLE_LEVEL * voice.gain;
312
+ const at = (offset: number) =>
313
+ readAt(mono, reverse ? regionEnd - 1 - offset : regionStart + offset);
314
+ // The raw read at an elapsed frame, or undefined past a one-shot's end.
315
+ const read = (elapsed: number): number | undefined => {
316
+ const travelled =
317
+ ramp === 0 ? elapsed * step : travelAt(elapsed, step, ramp);
318
+ if (!loop) return travelled >= span ? undefined : at(travelled);
319
+ if (travelled < loopStart) return at(travelled);
320
+ const into = travelled - loopStart;
321
+ const phase =
322
+ into < crossfade || period <= 0
323
+ ? into % Math.max(loopSpan, 1e-9)
324
+ : crossfade + ((into - crossfade) % period);
325
+ let value = at(loopStart + phase);
326
+ if (crossfade > 0 && phase >= period) {
327
+ const mix = (phase - period) / crossfade;
328
+ value = value * (1 - mix) + at(loopStart + phase - period) * mix;
329
+ }
330
+ return value;
331
+ };
332
+ const squizzed =
333
+ voice.squiz > 1
334
+ ? squizBuffer(read, end - voice.start, voice.squiz)
335
+ : undefined;
237
336
  for (let index = voice.start; index < end; index += 1) {
238
337
  const elapsed = index - voice.start;
239
- const travelled = elapsed * step;
240
- let value: number;
241
- if (!loop) {
242
- if (travelled >= span) break;
243
- value = readAt(
244
- mono,
245
- reverse ? regionEnd - 1 - travelled : regionStart + travelled,
246
- );
247
- } else {
248
- const phase =
249
- travelled < crossfade || period <= 0
250
- ? travelled % Math.max(span, 1e-9)
251
- : crossfade + ((travelled - crossfade) % period);
252
- const at = (offset: number) =>
253
- readAt(mono, reverse ? regionEnd - 1 - offset : regionStart + offset);
254
- value = at(phase);
255
- if (crossfade > 0 && phase >= period) {
256
- const mix = (phase - period) / crossfade;
257
- value = value * (1 - mix) + at(phase - period) * mix;
258
- }
259
- }
338
+ const value = squizzed ? squizzed[elapsed]! : read(elapsed);
339
+ if (value === undefined) break;
260
340
  let envelope = Math.min(1, elapsed / attack);
261
341
  if (index >= fadeFrom) envelope *= (voice.end - index) / voice.fade;
262
342
  target[index]! +=
@@ -268,6 +348,54 @@ export function renderSamplerVoices(
268
348
  }
269
349
  }
270
350
 
351
+ /** Longest zero-crossing cycle `squiz` treats as one (longer runs split). */
352
+ const SQUIZ_MAX_CYCLE = 4096;
353
+
354
+ /**
355
+ * Squiz (Tidal/SuperDirt's description: a simplistic pitch raiser): cut the
356
+ * voice at its upward zero crossings and play each cycle `ratio`× faster,
357
+ * repeating it to fill the cycle's original length. Length is kept, the
358
+ * pitch rises, and the timbre turns buzzy, as documented.
359
+ */
360
+ export function squizBuffer(
361
+ read: (elapsed: number) => number | undefined,
362
+ length: number,
363
+ ratio: number,
364
+ ): Float64Array {
365
+ const raw = new Float64Array(Math.max(0, length));
366
+ let filled = raw.length;
367
+ for (let i = 0; i < raw.length; i += 1) {
368
+ const value = read(i);
369
+ if (value === undefined) {
370
+ filled = i;
371
+ break;
372
+ }
373
+ raw[i] = value;
374
+ }
375
+ const out = new Float64Array(raw.length);
376
+ let start = 0;
377
+ while (start < filled) {
378
+ let stop = start + 1;
379
+ while (
380
+ stop < filled &&
381
+ stop - start < SQUIZ_MAX_CYCLE &&
382
+ !(raw[stop - 1]! < 0 && raw[stop]! >= 0)
383
+ )
384
+ stop += 1;
385
+ const size = stop - start;
386
+ for (let i = 0; i < size; i += 1) {
387
+ const position = (i * ratio) % size;
388
+ const base = Math.floor(position);
389
+ const next = base + 1 < size ? base + 1 : 0;
390
+ const frac = position - base;
391
+ out[start + i] =
392
+ raw[start + base]! + (raw[start + next]! - raw[start + base]!) * frac;
393
+ }
394
+ start = stop;
395
+ }
396
+ return out;
397
+ }
398
+
271
399
  /**
272
400
  * Seconds a sampler track rings past the loop end (bounded by the caller),
273
401
  * so loop renders fold whole samples onto the start.