@hraness/dawg 0.4.1 → 0.6.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 (166) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/DAWG.md +551 -33
  3. package/README.md +4 -4
  4. package/core/chords.ts +288 -7
  5. package/core/diff.ts +41 -12
  6. package/core/expression.ts +1241 -0
  7. package/core/fx.ts +437 -2
  8. package/core/granular.ts +528 -0
  9. package/core/instruments.ts +281 -0
  10. package/core/keys.ts +386 -0
  11. package/core/loop.ts +23 -0
  12. package/core/master.ts +455 -0
  13. package/core/midi.ts +452 -0
  14. package/core/resonators.ts +574 -0
  15. package/core/rhythm.ts +7 -2
  16. package/core/score.ts +717 -62
  17. package/core/sdk/eval-child.ts +40 -3
  18. package/core/sdk/print.ts +614 -27
  19. package/core/sdk/sync-instruments.ts +58 -0
  20. package/core/sdk/v1.ts +2824 -45
  21. package/core/sections.ts +2072 -0
  22. package/core/strings.ts +845 -0
  23. package/core/synth.ts +11 -1
  24. package/core/tempo.ts +1318 -0
  25. package/core/tuning.ts +1180 -0
  26. package/guides/audition.md +26 -0
  27. package/guides/automation.md +26 -0
  28. package/guides/chords.md +28 -0
  29. package/guides/effects.md +29 -0
  30. package/guides/faders.md +29 -0
  31. package/guides/files.md +28 -0
  32. package/guides/getting-started.md +26 -0
  33. package/guides/index.ts +75 -0
  34. package/guides/keys.md +27 -0
  35. package/guides/media.md +27 -0
  36. package/guides/mix.md +23 -0
  37. package/guides/music.md +14 -0
  38. package/guides/notes.md +26 -0
  39. package/guides/performance.md +26 -0
  40. package/guides/play.md +24 -0
  41. package/guides/project.md +13 -0
  42. package/guides/providers.md +25 -0
  43. package/guides/rhythm.md +26 -0
  44. package/guides/sessions.md +21 -0
  45. package/guides/sound.md +15 -0
  46. package/guides/sounds.md +29 -0
  47. package/guides/tempo.md +27 -0
  48. package/guides/tracks.md +25 -0
  49. package/guides/web-search.md +21 -0
  50. package/package.json +3 -1
  51. package/src/agent/agent.ts +9 -0
  52. package/src/agent/brief.ts +77 -3
  53. package/src/agent/chord-tools.ts +9 -1
  54. package/src/agent/expression-tools.ts +336 -0
  55. package/src/agent/granular-tools.ts +138 -0
  56. package/src/agent/master-tools.ts +299 -0
  57. package/src/agent/models.ts +4 -4
  58. package/src/agent/ops.ts +68 -6
  59. package/src/agent/planner.ts +45 -2
  60. package/src/agent/preview-tool.ts +27 -4
  61. package/src/agent/section-tools.ts +411 -0
  62. package/src/agent/time-tools.ts +290 -0
  63. package/src/agent/tools.ts +574 -16
  64. package/src/agent/tuning-tools.ts +301 -0
  65. package/src/agent/xcb-agent.ts +9 -0
  66. package/src/audio/arrange.ts +489 -0
  67. package/src/audio/audition.ts +16 -3
  68. package/src/audio/click.ts +113 -1
  69. package/src/audio/clock.ts +71 -5
  70. package/src/audio/dsp/bank.ts +233 -0
  71. package/src/audio/dsp/fft.ts +6 -0
  72. package/src/audio/dsp/filters.ts +57 -0
  73. package/src/audio/dsp/interp.ts +112 -0
  74. package/src/audio/dsp/modal.ts +684 -0
  75. package/src/audio/dsp/onset.ts +197 -0
  76. package/src/audio/dsp/oversample.ts +202 -0
  77. package/src/audio/dsp/rng.ts +37 -0
  78. package/src/audio/dsp/shape.ts +81 -0
  79. package/src/audio/dsp/stft.ts +73 -0
  80. package/src/audio/dsp/window.ts +77 -0
  81. package/src/audio/effects/bus.ts +5 -4
  82. package/src/audio/effects/chain.ts +6 -1
  83. package/src/audio/effects/common.ts +30 -1
  84. package/src/audio/effects/dynamics.ts +2 -1
  85. package/src/audio/effects/filter.ts +5 -3
  86. package/src/audio/effects/modulation.ts +5 -1
  87. package/src/audio/effects/rig/cab.ts +99 -0
  88. package/src/audio/effects/rig/filters.ts +152 -0
  89. package/src/audio/effects/rig/gate.ts +39 -0
  90. package/src/audio/effects/rig/head.ts +487 -0
  91. package/src/audio/effects/rig/index.ts +170 -0
  92. package/src/audio/effects/rig/section.ts +78 -0
  93. package/src/audio/effects/rig/stomp.ts +238 -0
  94. package/src/audio/effects/space.ts +92 -2
  95. package/src/audio/engine.ts +100 -21
  96. package/src/audio/fit.ts +402 -0
  97. package/src/audio/granular.ts +664 -0
  98. package/src/audio/instrument-check.ts +133 -0
  99. package/src/audio/instruments.ts +111 -0
  100. package/src/audio/keys/dsp.ts +323 -0
  101. package/src/audio/keys/engine.ts +361 -0
  102. package/src/audio/keys/piano.ts +432 -0
  103. package/src/audio/live-worker.ts +54 -0
  104. package/src/audio/live.ts +263 -17
  105. package/src/audio/loudness.ts +596 -0
  106. package/src/audio/master.ts +660 -0
  107. package/src/audio/measure-worker.ts +45 -0
  108. package/src/audio/measure.ts +110 -0
  109. package/src/audio/player.ts +11 -6
  110. package/src/audio/preview.ts +76 -14
  111. package/src/audio/render-worker.ts +6 -1
  112. package/src/audio/renderer.ts +7 -1
  113. package/src/audio/resonators.ts +287 -0
  114. package/src/audio/sampler.ts +269 -34
  115. package/src/audio/samples.ts +30 -7
  116. package/src/audio/strings/body.ts +250 -0
  117. package/src/audio/strings/engine.ts +369 -0
  118. package/src/audio/strings/loop.ts +119 -0
  119. package/src/audio/strings/measure.test-helpers.ts +198 -0
  120. package/src/audio/strings/pluck.ts +354 -0
  121. package/src/audio/synth/voice.ts +60 -13
  122. package/src/audio/synth/zzfx.ts +10 -4
  123. package/src/audio/warp.ts +147 -0
  124. package/src/audio/wav.ts +377 -30
  125. package/src/commands/arrange.ts +949 -0
  126. package/src/commands/expression.ts +941 -0
  127. package/src/commands/fit.ts +135 -0
  128. package/src/commands/fx.ts +31 -0
  129. package/src/commands/granular.ts +427 -0
  130. package/src/commands/help.ts +355 -27
  131. package/src/commands/keys.ts +353 -0
  132. package/src/commands/master.ts +361 -0
  133. package/src/commands/modal.ts +247 -0
  134. package/src/commands/music.ts +6 -1
  135. package/src/commands/rig.ts +260 -0
  136. package/src/commands/sample.ts +3 -0
  137. package/src/commands/string.ts +175 -0
  138. package/src/commands/synth.ts +11 -1
  139. package/src/commands/time.ts +967 -0
  140. package/src/commands/tuning.ts +490 -0
  141. package/src/main.ts +919 -41
  142. package/src/project/check.ts +13 -0
  143. package/src/render.ts +139 -8
  144. package/src/session/daemon.ts +18 -8
  145. package/src/session/naming.ts +8 -1
  146. package/src/session/rebase.ts +21 -0
  147. package/src/tui/arrange-menu.ts +390 -0
  148. package/src/tui/audition.ts +38 -5
  149. package/src/tui/fader.ts +409 -0
  150. package/src/tui/granular-menu.ts +278 -0
  151. package/src/tui/menu-time.ts +401 -0
  152. package/src/tui/menu.ts +967 -23
  153. package/src/tui/modal-menu.ts +118 -0
  154. package/src/tui/performance-menu.ts +235 -0
  155. package/src/tui/play-chords.ts +3 -1
  156. package/src/tui/play-mode.ts +150 -6
  157. package/src/tui/play-session.ts +459 -43
  158. package/tui/app.ts +262 -13
  159. package/tui/arrange-strip.ts +174 -0
  160. package/tui/drawer.ts +478 -0
  161. package/tui/grammar.ts +63 -1
  162. package/tui/guide.ts +351 -0
  163. package/tui/highway.ts +87 -4
  164. package/tui/hits.ts +68 -0
  165. package/tui/input.ts +7 -0
  166. package/tui/keys.ts +72 -0
@@ -19,7 +19,9 @@
19
19
  * of the score and the note, so cold, cached and worker renders agree.
20
20
  */
21
21
  import type { Note, Track } from "../../../core/score.ts";
22
+ import type { PerformedNote } from "../../../core/expression.ts";
22
23
  import type { FxLane } from "../../../core/fx.ts";
24
+ import { noteHz, type TuningTable } from "../../../core/tuning.ts";
23
25
  import {
24
26
  LEGACY_SOUNDS,
25
27
  SOUND_ALIASES,
@@ -78,6 +80,10 @@ export type VoiceContext = Readonly<{
78
80
  * (a wavetable track's table); otherwise the sound's resolver is used.
79
81
  */
80
82
  oscillatorFor?: (note: Note) => OscillatorFactory | undefined;
83
+ /** The track's merged tuning (`resolveTuning`); absent is 12-TET. */
84
+ tuning?: TuningTable;
85
+ /** Ticks added to a note's start in its noise seed (`RenderOptions`). */
86
+ seedTick?: number;
81
87
  }>;
82
88
 
83
89
  /** Track gain at a tick (volume × volume lane), supplied by the renderer. */
@@ -258,11 +264,16 @@ function noteFilters(
258
264
  c: Controls,
259
265
  gate: number,
260
266
  sampleRate: number,
267
+ accent = false,
261
268
  ): (() => NoteFilter)[] {
262
269
  const out: (() => NoteFilter)[] = [];
263
270
  for (const { prefix, type, param } of FILTERS) {
264
- const depth = c.number(`${prefix}env`);
265
- if (!c.has(param) && depth === 0) continue;
271
+ const written = c.number(`${prefix}env`);
272
+ if (!c.has(param) && written === 0) continue;
273
+ // TB-303 accent: an accented note opens the low-pass further and its
274
+ // filter envelope decays faster.
275
+ const boost = accent && prefix === "lp" ? c.number("faccent") : 0;
276
+ const depth = written + boost;
266
277
  const cutoff = c.number(param);
267
278
  const q = c.number(`${prefix}q`);
268
279
  const slope = type === "lpf" ? c.text("ftype") : "12db";
@@ -276,7 +287,15 @@ function noteFilters(
276
287
  q,
277
288
  depth,
278
289
  anchor,
279
- envelopeFor(c, prefix, "", gate),
290
+ boost > 0
291
+ ? new Envelope(
292
+ c.number(`${prefix}attack`),
293
+ c.number(`${prefix}decay`) / 2,
294
+ c.number(`${prefix}sustain`),
295
+ c.number(`${prefix}release`),
296
+ gate,
297
+ )
298
+ : envelopeFor(c, prefix, "", gate),
280
299
  sampleRate,
281
300
  ),
282
301
  );
@@ -343,13 +362,25 @@ export function renderSynthNote(
343
362
  start + length + Math.ceil(release * sampleRate),
344
363
  );
345
364
  if (end <= start) return;
346
- const random = seededRandom(`${note.id}:${note.startTick}:synth`);
365
+ const random = seededRandom(
366
+ `${note.id}:${note.startTick + (context.seedTick ?? 0)}:synth`,
367
+ );
347
368
  const amp = envelopeFor(c, "", "", gate);
348
369
  const velocity = clamp(note.velocity, 0, 1);
349
370
  const gain = c.number("gain");
350
-
351
- // Pitch.
352
- const base = 440 * 2 ** ((note.pitch - 69) / 12);
371
+ // Note expression (core/expression.ts): absent for plain notes, which
372
+ // keeps their output byte-identical.
373
+ const performance = (note as PerformedNote).performance;
374
+ const cents = performance?.cents;
375
+ const damp = performance?.damp;
376
+ const dampAt = (t: number): number =>
377
+ damp && t > damp.from ? Math.exp(-(t - damp.from) / damp.tau) : 1;
378
+
379
+ // Pitch: the tuning table's key frequency and the note's cents (exactly
380
+ // 440 · 2^((pitch − 69)/12) without either). A key a keyboard mapping
381
+ // leaves unmapped is silent.
382
+ const base = noteHz(note.pitch, note.cents, context.tuning);
383
+ if (!(base > 0)) return;
353
384
 
354
385
  if (isZzfxSound(sound) && !context.oscillatorFor) {
355
386
  const count = end - start;
@@ -371,28 +402,37 @@ export function renderSynthNote(
371
402
  zdelay: c.number("zdelay"),
372
403
  tremolo: c.number("tremolo"),
373
404
  random,
405
+ ...(cents ? { cents } : {}),
406
+ ...(performance?.replaceSlide ? { slideOff: true } : {}),
374
407
  });
375
- const filters = noteFilters(c, gate, sampleRate).map((make) => make());
408
+ const filters = noteFilters(
409
+ c,
410
+ gate,
411
+ sampleRate,
412
+ performance?.accent === true,
413
+ ).map((make) => make());
376
414
  for (let elapsed = 0; elapsed < count; elapsed += 1) {
377
415
  const t = elapsed / sampleRate;
378
416
  if (elapsed % CONTROL_SAMPLES === 0)
379
417
  for (const filter of filters) filter.update(t);
380
418
  let value = raw[elapsed]!;
381
419
  for (const filter of filters) value = filter.process(value);
382
- const level =
420
+ let level =
383
421
  amp.at(t) *
384
422
  velocity *
385
423
  gain *
386
424
  VOICE_LEVEL *
387
425
  gainAt(note.startTick + elapsed / samplesPerTick);
426
+ if (damp) level *= dampAt(t);
388
427
  left[start + elapsed]! += value * level;
389
428
  if (right) right[start + elapsed]! += value * level;
390
429
  }
391
430
  return;
392
431
  }
393
- const vib = c.number("vib");
432
+ // A note's own vibrato or bend replaces the synth's (note over track).
433
+ const vib = performance?.replaceVibrato ? 0 : c.number("vib");
394
434
  const vibmod = c.number("vibmod");
395
- const penv = c.number("penv");
435
+ const penv = performance?.replacePitchEnvelope ? 0 : c.number("penv");
396
436
  const pitchEnv =
397
437
  penv === 0
398
438
  ? undefined
@@ -463,7 +503,12 @@ export function renderSynthNote(
463
503
 
464
504
  const fm = fmOperators(c, gate);
465
505
  const fmPhases = fm.map(() => new Float64Array(unison));
466
- const filterMakers = noteFilters(c, gate, sampleRate);
506
+ const filterMakers = noteFilters(
507
+ c,
508
+ gate,
509
+ sampleRate,
510
+ performance?.accent === true,
511
+ );
467
512
  const filtersL = filterMakers.map((make) => make());
468
513
  const filtersR = right ? filterMakers.map((make) => make()) : [];
469
514
 
@@ -483,6 +528,7 @@ export function renderSynthNote(
483
528
  if (pcurve >= 1) e = e * e * e;
484
529
  semitones += penv * (e - panchor);
485
530
  }
531
+ if (cents) semitones += cents(t) / 100;
486
532
  const frequency = semitones === 0 ? base : base * 2 ** (semitones / 12);
487
533
  if (pwsweep > 0)
488
534
  width = clamp(
@@ -522,12 +568,13 @@ export function renderSynthNote(
522
568
  }
523
569
  for (const filter of filtersL) sampleL = filter.process(sampleL);
524
570
  for (const filter of filtersR) sampleR = filter.process(sampleR);
525
- const level =
571
+ let level =
526
572
  amp.at(t) *
527
573
  velocity *
528
574
  gain *
529
575
  VOICE_LEVEL *
530
576
  gainAt(note.startTick + elapsed / samplesPerTick);
577
+ if (damp) level *= dampAt(t);
531
578
  left[index]! += sampleL * level;
532
579
  if (right) right[index]! += sampleR * level;
533
580
  }
@@ -53,6 +53,10 @@ export type ZzfxParams = Readonly<{
53
53
  zdelay: number;
54
54
  tremolo: number;
55
55
  random: () => number;
56
+ /** Note expression: a pitch offset in cents at `t` seconds. */
57
+ cents?: (t: number) => number;
58
+ /** The note glides, so `slide` and `deltaSlide` are ignored. */
59
+ slideOff?: boolean;
56
60
  }>;
57
61
 
58
62
  function wave(sound: string, phase: number, held: number): number {
@@ -98,14 +102,16 @@ export function renderZzfx(
98
102
  const t = index / sampleRate;
99
103
  // Slide and pitch jump restart every `lfo` seconds.
100
104
  const local = params.lfo > 0 ? t % params.lfo : t;
101
- let frequency =
102
- base +
103
- 500 * params.slide * local +
104
- 250 * params.deltaSlide * local * local;
105
+ let frequency = params.slideOff
106
+ ? base
107
+ : base +
108
+ 500 * params.slide * local +
109
+ 250 * params.deltaSlide * local * local;
105
110
  if (params.pitchJumpTime > 0 && local >= params.pitchJumpTime)
106
111
  frequency += params.pitchJump;
107
112
  if (params.zmod > 0)
108
113
  frequency *= 1 + 0.5 * Math.sin(2 * Math.PI * params.zmod * t);
114
+ if (params.cents) frequency *= 2 ** (params.cents(t) / 1200);
109
115
  frequency = clamp(frequency, 0, sampleRate / 2);
110
116
  let increment = frequency / sampleRate;
111
117
  if (params.noise > 0) increment *= 1 + params.noise * (random() * 2 - 1);
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Sample positions through a song's tempo map (core/tempo.ts).
3
+ *
4
+ * Renderers keep their 0.4 arithmetic (`tick / samplesPerTick`) whenever
5
+ * `warp` is absent, which is every score without tempo events or
6
+ * fermatas, so those renders stay byte-identical. With a map, every tick
7
+ * to sample conversion goes through it instead.
8
+ */
9
+
10
+ import { timeMapFor, type TimeScore } from "../../core/tempo.ts";
11
+
12
+ export type SampleWarp = Readonly<{
13
+ /** Fractional sample index where `tick` sounds. */
14
+ sample(tick: number): number;
15
+ /** Score tick sounding at sample `index`. */
16
+ tick(index: number): number;
17
+ /** Tempo at `tick`. */
18
+ bpm(tick: number): number;
19
+ }>;
20
+
21
+ export function sampleWarpFor(
22
+ score: TimeScore,
23
+ sampleRate: number,
24
+ ): SampleWarp | undefined {
25
+ const map = timeMapFor(score);
26
+ if (!map) return undefined;
27
+ // Per-sample callers (automation, effects, sampler positions) read
28
+ // `tick` once per sample: evaluate the map once per block and interpolate
29
+ // between block edges. That is exact inside constant-tempo stretches,
30
+ // within a hair inside a ramp, and at most one block's ticks off across
31
+ // a step or fermata edge (about a tick at 48 kHz), for automation and
32
+ // modulation reads only: note onsets use `sample`, which stays exact.
33
+ let grid = new Float64Array(0);
34
+ let known = new Uint8Array(0);
35
+ const edge = (block: number): number => {
36
+ if (block >= grid.length) {
37
+ const size = Math.max(block + 1, grid.length * 2, 1024);
38
+ const nextGrid = new Float64Array(size);
39
+ const nextKnown = new Uint8Array(size);
40
+ nextGrid.set(grid);
41
+ nextKnown.set(known);
42
+ grid = nextGrid;
43
+ known = nextKnown;
44
+ }
45
+ if (known[block] === 0) {
46
+ grid[block] = map.tick((block * WARP_BLOCK) / sampleRate);
47
+ known[block] = 1;
48
+ }
49
+ return grid[block]!;
50
+ };
51
+ return Object.freeze({
52
+ sample: (tick: number) => map.seconds(tick) * sampleRate,
53
+ tick: (index: number) => {
54
+ if (!(index >= 0) || index > MAX_GRID_SAMPLES)
55
+ return map.tick(index / sampleRate);
56
+ const block = Math.floor(index / WARP_BLOCK);
57
+ const into = index - block * WARP_BLOCK;
58
+ const from = edge(block);
59
+ if (into === 0) return from;
60
+ return from + ((edge(block + 1) - from) * into) / WARP_BLOCK;
61
+ },
62
+ bpm: (tick: number) => map.bpm(tick),
63
+ });
64
+ }
65
+
66
+ /** Samples between exact `tick` evaluations; ticks in between interpolate. */
67
+ export const WARP_BLOCK = 64;
68
+ /** Past this sample index (about 3 h at 48 kHz) `tick` evaluates directly. */
69
+ const MAX_GRID_SAMPLES = 2 ** 29;
70
+
71
+ /**
72
+ * Start sample and length of a note through a warp: a note inside a
73
+ * fermata beat stretches with it.
74
+ */
75
+ export function warpedSpan(
76
+ warp: SampleWarp,
77
+ startTick: number,
78
+ durationTicks: number,
79
+ ): { start: number; length: number } {
80
+ const from = warp.sample(startTick);
81
+ const to = warp.sample(startTick + durationTicks);
82
+ return {
83
+ start: Math.max(0, Math.floor(from)),
84
+ length: Math.max(1, Math.floor(to - from)),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * A fitted sample window's position map (0.6 `bpm`/`len`/`fit`): output
90
+ * frames after the note start to window frames and back, both at the
91
+ * render rate. Through a tempo map the window follows ramps and steps;
92
+ * at constant tempo both directions are linear.
93
+ */
94
+ export type FitMap = Readonly<{
95
+ /** Output frames the whole window takes. */
96
+ length: number;
97
+ /** Window frame sounding `e` output frames after the start. */
98
+ at(e: number): number;
99
+ /** Output frame (after the start) where window frame `p` sounds. */
100
+ outAt(p: number): number;
101
+ }>;
102
+
103
+ /**
104
+ * The map for a window of `frames` render-rate frames that advances
105
+ * `perBeat` window frames per song beat from `startTick`. Without a warp the
106
+ * song runs at `tempoBpm` throughout.
107
+ */
108
+ export function beatFitMap(
109
+ warp: SampleWarp | undefined,
110
+ timing: Readonly<{
111
+ tempoBpm: number;
112
+ ticksPerBeat: number;
113
+ sampleRate: number;
114
+ }>,
115
+ startTick: number,
116
+ frames: number,
117
+ perBeat: number,
118
+ ): FitMap {
119
+ const { tempoBpm, ticksPerBeat, sampleRate } = timing;
120
+ if (!warp) {
121
+ const ratio = perBeat / ((sampleRate * 60) / tempoBpm);
122
+ return Object.freeze({
123
+ length: frames / ratio,
124
+ at: (e: number) => e * ratio,
125
+ outAt: (p: number) => p / ratio,
126
+ });
127
+ }
128
+ const origin = warp.sample(startTick);
129
+ const outAt = (p: number) =>
130
+ warp.sample(startTick + (p / perBeat) * ticksPerBeat) - origin;
131
+ return Object.freeze({
132
+ length: outAt(frames),
133
+ at: (e: number) =>
134
+ ((warp.tick(origin + e) - startTick) / ticksPerBeat) * perBeat,
135
+ outAt,
136
+ });
137
+ }
138
+
139
+ /** `fit`: the window fills `held` output frames, linearly. */
140
+ export function spanFitMap(frames: number, held: number): FitMap {
141
+ const ratio = frames / Math.max(1, held);
142
+ return Object.freeze({
143
+ length: held,
144
+ at: (e: number) => e * ratio,
145
+ outAt: (p: number) => p / ratio,
146
+ });
147
+ }