@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
@@ -17,6 +17,7 @@ import type { Track } from "../../../core/score.ts";
17
17
  import {
18
18
  CONTROL_SAMPLES,
19
19
  interpolateAutomation,
20
+ tickAtSample,
20
21
  type EffectContext,
21
22
  } from "./common.ts";
22
23
  import { applyDelay, applyReverb } from "./space.ts";
@@ -58,13 +59,13 @@ function send(
58
59
  stemR: Float64Array,
59
60
  base: number,
60
61
  lane: readonly Readonly<{ tick: number; value: number }>[],
61
- samplesPerTick: number,
62
+ context: Pick<EffectContext, "samplesPerTick" | "warp">,
62
63
  ): void {
63
64
  if (base <= 0 && lane.length === 0) return;
64
65
  let level = base;
65
66
  for (let index = 0; index < outL.length; index += 1) {
66
67
  if (lane.length > 0 && index % CONTROL_SAMPLES === 0)
67
- level = interpolateAutomation(lane, index / samplesPerTick, base);
68
+ level = interpolateAutomation(lane, tickAtSample(context, index), base);
68
69
  outL[index]! += stemL[index]! * level;
69
70
  outR[index]! += stemR[index]! * level;
70
71
  }
@@ -89,7 +90,7 @@ export function sendToBus(
89
90
  stemR,
90
91
  track.delay.mix,
91
92
  track.delayMixAutomation ?? [],
92
- context.samplesPerTick,
93
+ context,
93
94
  );
94
95
  }
95
96
  if (track.reverb) {
@@ -101,7 +102,7 @@ export function sendToBus(
101
102
  stemR,
102
103
  track.reverb.mix,
103
104
  track.fxAutomation?.["reverb-mix"] ?? [],
104
- context.samplesPerTick,
105
+ context,
105
106
  );
106
107
  }
107
108
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The fixed per-track effects chain (`FX_CHAIN` in core/fx.ts):
3
3
  *
4
- * filter → djf → autofilter → vowel → crush → distort → tremolo →
4
+ * filter → djf → autofilter → vowel → crush → distort → stomp → head →
5
+ * cab → tremolo →
5
6
  * compressor → pan → phaser → chorus → leslie → postgain → delay → reverb
6
7
  *
7
8
  * Stages before pan run on the mono voice sum; pan spreads it to stereo;
@@ -19,6 +20,7 @@ import {
19
20
  applyVowel,
20
21
  } from "./filter.ts";
21
22
  import { applyChorus, applyLeslie, applyPhaser } from "./modulation.ts";
23
+ import { applyCab, applyHead, applyStomp } from "./rig/index.ts";
22
24
  import { applyDelay, applyReverb } from "./space.ts";
23
25
 
24
26
  export {
@@ -52,6 +54,9 @@ const MONO: Readonly<Partial<Record<FxName, MonoStage>>> = Object.freeze({
52
54
  vowel: applyVowel,
53
55
  crush: applyCrush,
54
56
  distort: applyDistort,
57
+ stomp: applyStomp,
58
+ head: applyHead,
59
+ cab: applyCab,
55
60
  tremolo: applyTremolo,
56
61
  compressor: applyCompressor,
57
62
  });
@@ -12,6 +12,7 @@
12
12
  import type { AutomationPoint, Track } from "../../../core/score.ts";
13
13
  import type { FxLane, FxValues } from "../../../core/fx.ts";
14
14
  import type { DecodedSample } from "../samples.ts";
15
+ import type { SampleWarp } from "../warp.ts";
15
16
 
16
17
  /** Effect parameters are refreshed at this sample interval. */
17
18
  export const CONTROL_SAMPLES = 32;
@@ -22,8 +23,34 @@ export type EffectContext = Readonly<{
22
23
  tempoBpm: number;
23
24
  /** Decoded `reverb.ir` samples by track id (built-in impulses need none). */
24
25
  irs?: ReadonlyMap<string, DecodedSample>;
26
+ /** The song's tempo map in samples; absent: constant tempo. */
27
+ warp?: SampleWarp;
25
28
  }>;
26
29
 
30
+ /** Score tick at sample `index`, through the tempo map when there is one. */
31
+ export function tickAtSample(
32
+ context: Pick<EffectContext, "samplesPerTick" | "warp">,
33
+ index: number,
34
+ ): number {
35
+ return context.warp
36
+ ? context.warp.tick(index)
37
+ : index / context.samplesPerTick;
38
+ }
39
+
40
+ /**
41
+ * Tempo at sample `index` for tempo-synced LFOs and delays: through the
42
+ * tempo map when there is one (ramps, steps and fermata holds included),
43
+ * else the song's constant tempo.
44
+ */
45
+ export function tempoAtSample(
46
+ context: Pick<EffectContext, "tempoBpm" | "warp">,
47
+ index: number,
48
+ ): number {
49
+ return context.warp
50
+ ? context.warp.bpm(context.warp.tick(index))
51
+ : context.tempoBpm;
52
+ }
53
+
27
54
  /** Resolve a piecewise-linear automation lane, holding the static value before its first point. */
28
55
  export function interpolateAutomation(
29
56
  points: readonly AutomationPoint[],
@@ -58,6 +85,7 @@ export class Param {
58
85
  readonly fallback: number,
59
86
  points: readonly AutomationPoint[] | undefined,
60
87
  private readonly samplesPerTick: number,
88
+ private readonly warp?: SampleWarp,
61
89
  ) {
62
90
  this.points = points ?? [];
63
91
  this.automated = this.points.length > 0;
@@ -67,7 +95,7 @@ export class Param {
67
95
  return this.automated
68
96
  ? interpolateAutomation(
69
97
  this.points,
70
- index / this.samplesPerTick,
98
+ this.warp ? this.warp.tick(index) : index / this.samplesPerTick,
71
99
  this.fallback,
72
100
  )
73
101
  : this.fallback;
@@ -93,6 +121,7 @@ export function fxReader(
93
121
  values[name] as number,
94
122
  track.fxAutomation?.[`${effect}-${name}` as FxLane],
95
123
  context.samplesPerTick,
124
+ context.warp,
96
125
  );
97
126
  },
98
127
  };
@@ -10,6 +10,7 @@ import {
10
10
  fxReader,
11
11
  lfo,
12
12
  lfoHz,
13
+ tempoAtSample,
13
14
  type EffectContext,
14
15
  } from "./common.ts";
15
16
 
@@ -42,7 +43,7 @@ export function applyTremolo(
42
43
  for (let index = 0; index < buffer.length; index += 1) {
43
44
  if (index % CONTROL_SAMPLES === 0) {
44
45
  phasor.setHz(
45
- lfoHz(sync, rate.at(index), context.tempoBpm),
46
+ lfoHz(sync, rate.at(index), tempoAtSample(context, index)),
46
47
  context.sampleRate,
47
48
  );
48
49
  if (depth.automated) amount = depth.at(index);
@@ -15,7 +15,9 @@ import {
15
15
  interpolateAutomation,
16
16
  lfo,
17
17
  lfoHz,
18
+ tempoAtSample,
18
19
  resonanceQ,
20
+ tickAtSample,
19
21
  type EffectContext,
20
22
  type FilterType,
21
23
  } from "./common.ts";
@@ -99,7 +101,7 @@ export function applyTrackFilter(
99
101
  const resonanceLane = track.resonanceAutomation ?? [];
100
102
  if (!track.filter && automation.length === 0 && resonanceLane.length === 0)
101
103
  return;
102
- const { sampleRate, samplesPerTick } = context;
104
+ const { sampleRate } = context;
103
105
  const staticCutoff = track.filter?.cutoff ?? SCORE_LIMITS.maxFilterCutoff;
104
106
  const staticResonance = track.filter?.resonance ?? 0;
105
107
  const automated = automation.length > 0 || resonanceLane.length > 0;
@@ -111,7 +113,7 @@ export function applyTrackFilter(
111
113
  filter.update(staticCutoff, staticResonance);
112
114
  for (let index = 0; index < buffer.length; index += 1) {
113
115
  if (automated && index % CONTROL_SAMPLES === 0) {
114
- const tick = index / samplesPerTick;
116
+ const tick = tickAtSample(context, index);
115
117
  filter.update(
116
118
  interpolateAutomation(automation, tick, staticCutoff),
117
119
  interpolateAutomation(resonanceLane, tick, staticResonance),
@@ -205,7 +207,7 @@ export function applyAutoFilter(
205
207
  : release * envelope + (1 - release) * level;
206
208
  if (index % CONTROL_SAMPLES === 0) {
207
209
  phasor.setHz(
208
- lfoHz(sync, rate.at(index), context.tempoBpm),
210
+ lfoHz(sync, rate.at(index), tempoAtSample(context, index)),
209
211
  context.sampleRate,
210
212
  );
211
213
  followOctaves = follow.at(index);
@@ -9,6 +9,7 @@ import {
9
9
  clamp,
10
10
  fxReader,
11
11
  lfoHz,
12
+ tempoAtSample,
12
13
  readFractional,
13
14
  type EffectContext,
14
15
  } from "./common.ts";
@@ -57,7 +58,10 @@ export function applyPhaser(
57
58
  };
58
59
  for (let index = 0; index < left.length; index += 1) {
59
60
  if (index % CONTROL_SAMPLES === 0) {
60
- phasor.setHz(lfoHz(sync, rate.at(index), context.tempoBpm), sampleRate);
61
+ phasor.setHz(
62
+ lfoHz(sync, rate.at(index), tempoAtSample(context, index)),
63
+ sampleRate,
64
+ );
61
65
  if (depth.automated) amount = depth.at(index);
62
66
  const frequency = clamp(
63
67
  center + (sweep / 2) * Math.sin(2 * Math.PI * phasor.phase),
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Speaker cabinets (`fx.cab`): a parametric speaker, box and microphone
3
+ * model from cheap biquads (no impulse response, no setup cost). Ported
4
+ * from the 0.6 guitar prototype; `8x10`, `1x15` and `di` are dawg
5
+ * additions in the same model.
6
+ */
7
+ import { Rbj, clamp } from "./filters.ts";
8
+
9
+ export const CAB_TYPES = [
10
+ "1x12",
11
+ "2x12",
12
+ "4x12",
13
+ "1x10",
14
+ "open",
15
+ "8x10",
16
+ "1x15",
17
+ "di",
18
+ ] as const;
19
+ export type CabType = (typeof CAB_TYPES)[number];
20
+
21
+ type CabModel = Readonly<{
22
+ hp: number;
23
+ /** Low resonance: Hz, Q, dB. */
24
+ low: readonly [number, number, number];
25
+ peaks: readonly (readonly [number, number, number])[];
26
+ /** Speaker roll-off (two biquads, Butterworth 4th order); 0 = none. */
27
+ lp: number;
28
+ }>;
29
+
30
+ const CABS: Record<CabType, CabModel> = {
31
+ "1x12": {
32
+ hp: 75,
33
+ low: [115, 1.4, 3],
34
+ peaks: [
35
+ [2100, 2, 3],
36
+ [3600, 3, 2],
37
+ ],
38
+ lp: 5200,
39
+ },
40
+ "2x12": {
41
+ hp: 70,
42
+ low: [105, 1.3, 3.5],
43
+ peaks: [
44
+ [1200, 1.5, -2],
45
+ [2800, 2.2, 4.5],
46
+ ],
47
+ lp: 6200,
48
+ },
49
+ "4x12": {
50
+ hp: 65,
51
+ low: [95, 1.2, 5],
52
+ peaks: [
53
+ [700, 1, -2.5],
54
+ [2400, 2, 4],
55
+ [3900, 3, 2],
56
+ ],
57
+ lp: 4700,
58
+ },
59
+ "1x10": { hp: 120, low: [160, 1.5, 2], peaks: [[1500, 1.5, 3]], lp: 4300 },
60
+ open: { hp: 90, low: [120, 0.9, 1], peaks: [[2500, 1.2, 2]], lp: 7000 },
61
+ // Bass cabinets: deep, rolled off early (10s brighter than a 15).
62
+ "8x10": {
63
+ hp: 45,
64
+ low: [80, 1.0, 3],
65
+ peaks: [
66
+ [800, 1.2, -2],
67
+ [2500, 2, 3],
68
+ ],
69
+ lp: 4500,
70
+ },
71
+ "1x15": {
72
+ hp: 40,
73
+ low: [70, 1.2, 4],
74
+ peaks: [
75
+ [1000, 1, -1.5],
76
+ [2000, 1.5, 2],
77
+ ],
78
+ lp: 3500,
79
+ },
80
+ // Direct in: no speaker, only a subsonic high-pass.
81
+ di: { hp: 20, low: [100, 0.7, 0], peaks: [], lp: 0 },
82
+ };
83
+
84
+ /** The cab's filter chain at `sr` for mic position `mic` (0..1). */
85
+ export function cabFilters(type: string, mic: number, sr: number): Rbj[] {
86
+ const m = CABS[type as CabType] ?? CABS["2x12"];
87
+ if (m.lp === 0) return [new Rbj("hpf", m.hp, 0.707, sr)];
88
+ const edge = clamp(mic, 0, 1);
89
+ return [
90
+ new Rbj("hpf", m.hp, 0.707, sr),
91
+ new Rbj("peak", m.low[0], m.low[1], sr, m.low[2]),
92
+ ...m.peaks.map(
93
+ ([f, q, db]) => new Rbj("peak", f, q, sr, db * (1 - 0.6 * edge)),
94
+ ),
95
+ new Rbj("highshelf", 3000, 0.7, sr, 2 - 8 * edge),
96
+ new Rbj("lpf", m.lp * (1 - 0.3 * edge), 0.54, sr),
97
+ new Rbj("lpf", m.lp * (1 - 0.3 * edge), 1.31, sr),
98
+ ];
99
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Filters for the guitar rig: the full RBJ cookbook biquad (peaks and
3
+ * shelves, which the effects chain's `Biquad` does not offer) and a
4
+ * one-pole high-pass. Ported from the 0.6 guitar prototype; a new class so
5
+ * the shared `Biquad` is never retuned.
6
+ */
7
+
8
+ export function clamp(x: number, lo: number, hi: number): number {
9
+ return x < lo ? lo : x > hi ? hi : x;
10
+ }
11
+
12
+ export function dbToGain(db: number): number {
13
+ return 10 ** (db / 20);
14
+ }
15
+
16
+ export type RbjType = "lpf" | "hpf" | "peak" | "lowshelf" | "highshelf";
17
+
18
+ /** RBJ Audio EQ Cookbook biquad, transposed direct form II. */
19
+ export class Rbj {
20
+ b0 = 1;
21
+ b1 = 0;
22
+ b2 = 0;
23
+ a1 = 0;
24
+ a2 = 0;
25
+ z1 = 0;
26
+ z2 = 0;
27
+
28
+ constructor(type: RbjType, hz: number, q: number, sr: number, db = 0) {
29
+ this.set(type, hz, q, sr, db);
30
+ }
31
+
32
+ set(type: RbjType, hz: number, q: number, sr: number, db = 0): void {
33
+ const w = (2 * Math.PI * clamp(hz, 10, sr * 0.49)) / sr;
34
+ const cw = Math.cos(w);
35
+ const alpha = Math.sin(w) / (2 * q);
36
+ const A = 10 ** (db / 40);
37
+ let b0: number, b1: number, b2: number, a0: number, a1: number, a2: number;
38
+ switch (type) {
39
+ case "lpf":
40
+ b0 = (1 - cw) / 2;
41
+ b1 = 1 - cw;
42
+ b2 = b0;
43
+ a0 = 1 + alpha;
44
+ a1 = -2 * cw;
45
+ a2 = 1 - alpha;
46
+ break;
47
+ case "hpf":
48
+ b0 = (1 + cw) / 2;
49
+ b1 = -(1 + cw);
50
+ b2 = b0;
51
+ a0 = 1 + alpha;
52
+ a1 = -2 * cw;
53
+ a2 = 1 - alpha;
54
+ break;
55
+ case "peak":
56
+ b0 = 1 + alpha * A;
57
+ b1 = -2 * cw;
58
+ b2 = 1 - alpha * A;
59
+ a0 = 1 + alpha / A;
60
+ a1 = -2 * cw;
61
+ a2 = 1 - alpha / A;
62
+ break;
63
+ case "lowshelf": {
64
+ const s = 2 * Math.sqrt(A) * alpha;
65
+ b0 = A * (A + 1 - (A - 1) * cw + s);
66
+ b1 = 2 * A * (A - 1 - (A + 1) * cw);
67
+ b2 = A * (A + 1 - (A - 1) * cw - s);
68
+ a0 = A + 1 + (A - 1) * cw + s;
69
+ a1 = -2 * (A - 1 + (A + 1) * cw);
70
+ a2 = A + 1 + (A - 1) * cw - s;
71
+ break;
72
+ }
73
+ case "highshelf": {
74
+ const s = 2 * Math.sqrt(A) * alpha;
75
+ b0 = A * (A + 1 + (A - 1) * cw + s);
76
+ b1 = -2 * A * (A - 1 + (A + 1) * cw);
77
+ b2 = A * (A + 1 + (A - 1) * cw - s);
78
+ a0 = A + 1 - (A - 1) * cw + s;
79
+ a1 = 2 * (A - 1 - (A + 1) * cw);
80
+ a2 = A + 1 - (A - 1) * cw - s;
81
+ break;
82
+ }
83
+ }
84
+ this.b0 = b0 / a0;
85
+ this.b1 = b1 / a0;
86
+ this.b2 = b2 / a0;
87
+ this.a1 = a1 / a0;
88
+ this.a2 = a2 / a0;
89
+ }
90
+
91
+ process(x: number): number {
92
+ const y = this.b0 * x + this.z1;
93
+ this.z1 = this.b1 * x - this.a1 * y + this.z2;
94
+ this.z2 = this.b2 * x - this.a2 * y;
95
+ return y;
96
+ }
97
+ }
98
+
99
+ /** One-pole low-pass y = x + p (y - x). */
100
+ export class Lp1 {
101
+ p = 0;
102
+ y = 0;
103
+
104
+ constructor(hz: number, sr: number) {
105
+ this.set(hz, sr);
106
+ }
107
+
108
+ set(hz: number, sr: number): void {
109
+ this.p = Math.exp((-2 * Math.PI * clamp(hz, 1, sr * 0.49)) / sr);
110
+ }
111
+
112
+ process(x: number): number {
113
+ this.y = x + this.p * (this.y - x);
114
+ return this.y;
115
+ }
116
+ }
117
+
118
+ /** One-pole high-pass (leaky differentiator). */
119
+ export class Hp1 {
120
+ p = 0;
121
+ x1 = 0;
122
+ y1 = 0;
123
+
124
+ constructor(hz: number, sr: number) {
125
+ this.p = Math.exp((-2 * Math.PI * clamp(hz, 1, sr * 0.49)) / sr);
126
+ }
127
+
128
+ process(x: number): number {
129
+ const y = ((1 + this.p) / 2) * (x - this.x1) + this.p * this.y1;
130
+ this.x1 = x;
131
+ this.y1 = y;
132
+ return y;
133
+ }
134
+ }
135
+
136
+ /** DC blocker y = x - x1 + R y1. */
137
+ export class Dc {
138
+ r: number;
139
+ x1 = 0;
140
+ y1 = 0;
141
+
142
+ constructor(sr: number, hz: number) {
143
+ this.r = Math.exp((-2 * Math.PI * hz) / sr);
144
+ }
145
+
146
+ process(x: number): number {
147
+ const y = x - this.x1 + this.r * this.y1;
148
+ this.x1 = x;
149
+ this.y1 = y;
150
+ return y;
151
+ }
152
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Noise gate before the amp (`fx.head.gate`): a peak follower with
3
+ * hysteresis (opens at the threshold, closes 6 dB below it), a 50 ms hold,
4
+ * 1 ms attack and 80 ms release, so a high-gain head does not amplify the
5
+ * hiss and string noise between notes.
6
+ */
7
+ export const GATE_HYSTERESIS_DB = 6;
8
+
9
+ export function applyGate(
10
+ buffer: Float64Array,
11
+ thresholdDb: number,
12
+ sampleRate: number,
13
+ ): void {
14
+ const open = 10 ** (thresholdDb / 20);
15
+ const close = 10 ** ((thresholdDb - GATE_HYSTERESIS_DB) / 20);
16
+ const peakRelease = Math.exp(-1 / (0.01 * sampleRate));
17
+ const attack = 1 - Math.exp(-1 / (0.001 * sampleRate));
18
+ const release = 1 - Math.exp(-1 / (0.08 * sampleRate));
19
+ const holdSamples = Math.round(0.05 * sampleRate);
20
+ let peak = 0;
21
+ let gain = 0;
22
+ let isOpen = false;
23
+ let hold = 0;
24
+ for (let i = 0; i < buffer.length; i += 1) {
25
+ const x = buffer[i]!;
26
+ const level = Math.abs(x);
27
+ peak = level > peak ? level : peak * peakRelease;
28
+ if (peak >= open) {
29
+ isOpen = true;
30
+ hold = holdSamples;
31
+ } else if (peak < close) {
32
+ if (hold > 0) hold -= 1;
33
+ else isOpen = false;
34
+ }
35
+ const target = isOpen ? 1 : 0;
36
+ gain += (target - gain) * (target > gain ? attack : release);
37
+ buffer[i] = x * gain;
38
+ }
39
+ }