@hraness/dawg 0.6.0 → 0.7.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 (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -0,0 +1,492 @@
1
+ /**
2
+ * One blown voice: a digital waveguide bore driven by a reed table
3
+ * (clarinet, `reed`), a conical reed (`sax`, also oboe and bassoon), an
4
+ * air jet (`jet`, flutes; open or stopped) or a lip resonator (`lips`,
5
+ * brass). After Cook and Scavone's STK (Clarinet, Saxofony, Flute, Brass;
6
+ * https://github.com/thestk/stk) and J. O. Smith, Physical Audio Signal
7
+ * Processing (https://ccrma.stanford.edu/~jos/pasp/), ported from the
8
+ * reviewed prototype (.plans/dawg-06/proto/resonators/winds.ts) with the
9
+ * review's fixes:
10
+ *
11
+ * - jet: a two-period loop (bore 1.5 periods, jet 0.5) less the loop
12
+ * low-pass and DC-blocker phase delays, with a jet offset and an edge
13
+ * tap so open pipes get even harmonics; stopped pipes are an inverting
14
+ * half-period bore (odd harmonics).
15
+ * - jet, reed and sax run at twice the rate with first-order ADAA on their
16
+ * nonlinearity and the 47-tap half-band decimator; brass runs at 1x.
17
+ * - Thiran fractional delays, loop cutoff and the trim are retuned every
18
+ * 32-sample control tick (bends, glides, slurs, vibrato).
19
+ * - The oscillation pressure keeps a 0.45 floor for stable pitch; a
20
+ * separate breath gain (30 dB over breath 0..1) makes swells audible.
21
+ *
22
+ * Deterministic: noise from `seededRandom` keyed by the note.
23
+ */
24
+ import type { WindSettings } from "../../../core/resonators.ts";
25
+ import { Decimate2 } from "../dsp/oversample.ts";
26
+ import { seededRandom } from "../dsp/rng.ts";
27
+ import { DcBlock, OnePole } from "../dsp/filters.ts";
28
+ import {
29
+ WindBiquad,
30
+ LoopDelay,
31
+ dcBlockDelay,
32
+ onePoleDelay,
33
+ } from "./filters.ts";
34
+
35
+ /** Control tick in base-rate samples. */
36
+ export const WIND_BLOCK = 32;
37
+
38
+ /** Pitch target over the voice: the first entry at 0, slurs after it. */
39
+ export type WindSegment = Readonly<{ at: number; hz: number }>;
40
+
41
+ export type WindNote = Readonly<{
42
+ /** Pitches in base-rate samples from the voice start; [0] at 0. */
43
+ segments: readonly WindSegment[];
44
+ /** Breath duration (note-off) in base-rate samples. */
45
+ length: number;
46
+ velocity: number;
47
+ seed: string;
48
+ /** Extra breath for accent/marcato (+), less for ghost (-). */
49
+ push?: number;
50
+ /** Pitch offset in cents at t seconds (bends, glides). */
51
+ cents?: (t: number) => number;
52
+ /** Automation at t seconds; returns undefined for a parameter with no lane. */
53
+ lane?: (param: string, t: number) => number | undefined;
54
+ }>;
55
+
56
+ /** Per-voice trims: cents as a function of sounding Hz, output gain. */
57
+ export type WindTrim = Readonly<{
58
+ cents: (hz: number) => number;
59
+ level: number;
60
+ /** Loudness curve over the range: a gain factor at `hz` (default 1). */
61
+ gain?: (hz: number) => number;
62
+ /**
63
+ * Calibration 1 lips (q08): the lip resonance sits on the sounding pitch
64
+ * at Q 40 and the lip opening saturates softly, so the lips lock to the
65
+ * bore's mode instead of beating against it (a 23-35% warble every 8 or
66
+ * 9 periods before). Only the calibration 1 trim rows set it.
67
+ */
68
+ lips?: 1;
69
+ }>;
70
+
71
+ const NO_TRIM: WindTrim = { cents: () => 0, level: 1 };
72
+
73
+ const SLUR_SECONDS = 0.03;
74
+ /** First-order ADAA delays its shaper's output by half a sample. */
75
+ const ADAA_DELAY = 0.5;
76
+ const clamp = (x: number, lo: number, hi: number) =>
77
+ x < lo ? lo : x > hi ? hi : x;
78
+
79
+ // Reed table g(p) = p * clamp(0.7 - s p, -1, 1) and its antiderivative (ADAA).
80
+ function reedG(p: number, s: number): number {
81
+ return p * clamp(0.7 - s * p, -1, 1);
82
+ }
83
+ function reedF(p: number, s: number): number {
84
+ const p1 = -0.3 / s;
85
+ const p2 = 1.7 / s;
86
+ const mid = (x: number) => 0.35 * x * x - (s * x * x * x) / 3;
87
+ if (p < p1) return 0.5 * p * p + mid(p1) - 0.5 * p1 * p1;
88
+ if (p > p2) return -0.5 * p * p + mid(p2) + 0.5 * p2 * p2;
89
+ return mid(p);
90
+ }
91
+ // Jet table j(x) = clamp(x^3 - x, -1, 1) and its antiderivative.
92
+ const JET_EDGE = 1.3247179572447458; // real root of x^3 - x - 1
93
+ function jetG(x: number): number {
94
+ return clamp(x * x * x - x, -1, 1);
95
+ }
96
+ function jetF(x: number): number {
97
+ const a = Math.abs(x);
98
+ const poly = (v: number) => 0.25 * v ** 4 - 0.5 * v * v;
99
+ if (a <= JET_EDGE) return poly(x);
100
+ return poly(JET_EDGE) + (a - JET_EDGE);
101
+ }
102
+
103
+ /**
104
+ * STK's lip filter: a two-pole resonance at the lip frequency with no
105
+ * zeros (BiQuad::setResonance, unnormalised), so the mouth pressure's DC
106
+ * sets the lips' operating point and the squared area mapping has gain.
107
+ */
108
+ class LipResonator {
109
+ private a1 = 0;
110
+ private a2 = 0;
111
+ private y1 = 0;
112
+ private y2 = 0;
113
+ private g = 1;
114
+ set(hz: number, q: number, gain: number, sampleRate: number): void {
115
+ const radius = Math.exp((-Math.PI * hz) / (q * sampleRate));
116
+ this.a1 = -2 * radius * Math.cos((2 * Math.PI * hz) / sampleRate);
117
+ this.a2 = radius * radius;
118
+ this.g = gain * (1 + this.a1 + this.a2);
119
+ }
120
+ process(x: number): number {
121
+ const y = this.g * x - this.a1 * this.y1 - this.a2 * this.y2;
122
+ this.y2 = this.y1;
123
+ this.y1 = y;
124
+ return y;
125
+ }
126
+ }
127
+
128
+ /** First-order ADAA around a shaper with a parameter. */
129
+ class Adaa {
130
+ private x1 = 0;
131
+ private F1: number;
132
+ constructor(
133
+ private readonly f: (x: number) => number,
134
+ private readonly F: (x: number) => number,
135
+ ) {
136
+ this.F1 = F(0);
137
+ }
138
+ process(x: number): number {
139
+ const Fx = this.F(x);
140
+ const dx = x - this.x1;
141
+ const y =
142
+ Math.abs(dx) < 1e-6 ? this.f(0.5 * (x + this.x1)) : (Fx - this.F1) / dx;
143
+ this.x1 = x;
144
+ this.F1 = Fx;
145
+ return y;
146
+ }
147
+ }
148
+
149
+ export class WindVoice {
150
+ /** Base-rate sample after which the voice is silent. */
151
+ readonly endAt: number;
152
+ private readonly rate: number;
153
+ private readonly os: boolean;
154
+ private readonly random: () => number;
155
+ private readonly d0: LoopDelay;
156
+ private readonly d1: LoopDelay;
157
+ private readonly loopLp: OnePole;
158
+ private readonly dc: DcBlock;
159
+ private readonly outDc: DcBlock;
160
+ private readonly noiseLp: OnePole;
161
+ private readonly lip = new LipResonator();
162
+ private readonly shelf = new WindBiquad();
163
+ /** Reed, sax and lips brightness: a pitch-tracked tilt outside the loop. */
164
+ private readonly tone = new WindBiquad();
165
+ private readonly mutes: WindBiquad[] = [];
166
+ private readonly wahFilter = new WindBiquad();
167
+ private readonly decimate: Decimate2 | undefined;
168
+ private readonly shaper: Adaa | undefined;
169
+ private readonly reedSlope: number;
170
+ private settings: WindSettings;
171
+ private n = 0;
172
+ private envelope = 0;
173
+ private oz1 = 0;
174
+ private hz: number;
175
+ private slurFrom = 0;
176
+ private slurAt = -1;
177
+ private segment = 0;
178
+ private pressure = 0;
179
+ private breathGain = 1;
180
+ private wah = 1;
181
+ /** Output gain the loudness curve asks for, and its smoothed value. */
182
+ private gainTarget = 1;
183
+ private gainNow = -1;
184
+ private readonly growlHz: number;
185
+
186
+ constructor(
187
+ base: WindSettings,
188
+ private readonly note: WindNote,
189
+ private readonly sampleRate: number,
190
+ private readonly trim: WindTrim = NO_TRIM,
191
+ ) {
192
+ this.settings = base;
193
+ const p = base;
194
+ this.os = p.model !== "lips";
195
+ this.rate = this.os ? 2 * sampleRate : sampleRate;
196
+ this.random = seededRandom(`${note.seed}:wind`);
197
+ let lowest = Infinity;
198
+ for (const s of note.segments) lowest = Math.min(lowest, s.hz);
199
+ lowest = Math.max(15, lowest) * 0.5; // room for a -1 octave bend
200
+ const maxDelay = (this.rate / lowest) * 2.2 + 16;
201
+ this.d0 = new LoopDelay(maxDelay);
202
+ this.d1 = new LoopDelay(maxDelay);
203
+ this.loopLp = new OnePole(4000, this.rate);
204
+ this.dc = new DcBlock(this.rate, 20);
205
+ this.outDc = new DcBlock(sampleRate, 20);
206
+ this.noiseLp = new OnePole(2500, this.rate);
207
+ this.decimate = this.os ? new Decimate2() : undefined;
208
+ this.reedSlope = 0.3 * (0.6 + 0.8 * p.reed);
209
+ const s = this.reedSlope;
210
+ if (p.model === "reed")
211
+ this.shaper = new Adaa(
212
+ (x) => reedG(x, s),
213
+ (x) => reedF(x, s),
214
+ );
215
+ else if (p.model === "jet") this.shaper = new Adaa(jetG, jetF);
216
+ this.hz = note.segments[0]!.hz;
217
+ this.growlHz = 70 + 50 * this.random();
218
+ // Dynamics: a 1.5 kHz shelf of (v - 0.6) k dB; brass blares, flutes barely change.
219
+ const k = { jet: 8, reed: 10, sax: 14, lips: 22 }[p.model];
220
+ const v = clamp(note.velocity + (note.push ?? 0), 0, 1);
221
+ this.shelf.set("highshelf", 1500, 0.7, (v - 0.6) * k, sampleRate);
222
+ this.setupMute(sampleRate);
223
+ this.endAt = note.length + Math.round((p.release * 3 + 0.05) * sampleRate);
224
+ this.control(0);
225
+ }
226
+
227
+ private setupMute(sr: number): void {
228
+ const add = (
229
+ type: Parameters<WindBiquad["set"]>[0],
230
+ hz: number,
231
+ q: number,
232
+ db: number,
233
+ ) => this.mutes.push(new WindBiquad().set(type, hz, q, db, sr));
234
+ switch (this.settings.mute) {
235
+ case "straight":
236
+ add("hpf", 450, 0.7, 0);
237
+ add("peak", 1800, 2, 9);
238
+ add("lpf", 7000, 0.7, 0);
239
+ break;
240
+ case "cup":
241
+ add("hpf", 220, 0.7, 0);
242
+ add("peak", 850, 1.5, 5);
243
+ add("lpf", 2200, 0.8, 0);
244
+ break;
245
+ case "harmon":
246
+ add("hpf", 700, 0.8, 0);
247
+ add("peak", 1650, 4, 14);
248
+ add("lpf", 4200, 1, 0);
249
+ break;
250
+ default:
251
+ break;
252
+ }
253
+ }
254
+
255
+ /** Control tick: lanes, pitch (slur, cents, vibrato, trim), delays, breath. */
256
+ private control(n: number): void {
257
+ const { note, sampleRate: sr, rate } = this;
258
+ const t = n / sr;
259
+ let p = this.settings;
260
+ if (note.lane) {
261
+ const next: Record<string, unknown> = { ...p };
262
+ let changed = false;
263
+ for (const key of ["breath", "noise", "wah", "growl", "flutter"]) {
264
+ const value = note.lane(key, t);
265
+ if (value !== undefined) {
266
+ next[key] = clamp(value, 0, 1);
267
+ changed = true;
268
+ }
269
+ }
270
+ if (changed) p = this.settings = next as WindSettings;
271
+ }
272
+ // Slurs: a short log-frequency glide into each later segment.
273
+ while (
274
+ this.segment + 1 < note.segments.length &&
275
+ note.segments[this.segment + 1]!.at <= n
276
+ ) {
277
+ this.segment += 1;
278
+ this.slurFrom = this.hz;
279
+ this.slurAt = note.segments[this.segment]!.at;
280
+ }
281
+ const target = note.segments[this.segment]!.hz;
282
+ let hz = target;
283
+ if (this.slurAt >= 0) {
284
+ const u = (n - this.slurAt) / (SLUR_SECONDS * sr);
285
+ if (u >= 1) this.slurAt = -1;
286
+ else hz = this.slurFrom * (target / this.slurFrom) ** u;
287
+ }
288
+ this.hz = hz;
289
+ this.gainTarget = this.trim.gain ? this.trim.gain(hz) : 1;
290
+ if (this.gainNow < 0) this.gainNow = this.gainTarget;
291
+ const cents = note.cents ? note.cents(t) : 0;
292
+ const vib =
293
+ p.vibmod > 0
294
+ ? p.vibmod * Math.sin(2 * Math.PI * p.vib * t) * Math.min(1, t / 0.3)
295
+ : 0;
296
+ const f0 = hz * 2 ** (cents / 1200 + vib / 12);
297
+ const f = f0 * 2 ** (this.trim.cents(f0) / 1200);
298
+ const w = (2 * Math.PI * f) / rate;
299
+ const period = rate / f;
300
+ switch (p.model) {
301
+ case "reed":
302
+ // Closed-open cylinder: inverting reflection, round trip = 2 x loop;
303
+ // the one-zero averager adds half a sample.
304
+ this.d0.set(period / 2 - 0.5 - ADAA_DELAY, w);
305
+ break;
306
+ case "sax": {
307
+ // Reed position 0.35 along the bore: measured over every semitone of
308
+ // sax, altosax, barisax, oboe and bassoon at 22.05, 44.1 and 48 kHz
309
+ // it never flips register (STK's 0.2 flips at 6 of 36 sax notes,
310
+ // the spec's register guard).
311
+ const position = 0.35;
312
+ const total = period - 0.5;
313
+ this.d0.set((1 - position) * total, w);
314
+ this.d1.set(position * total, w);
315
+ break;
316
+ }
317
+ case "jet": {
318
+ // Loop low-pass corner tracks the pitch: brighter above.
319
+ const corner = clamp(f * (3 + 9 * p.bright), 800, 0.4 * rate);
320
+ this.loopLp.set(corner, rate);
321
+ const filters =
322
+ onePoleDelay(this.loopLp.p, w) +
323
+ dcBlockDelay(this.dc.r, w) +
324
+ ADAA_DELAY;
325
+ if (p.stopped) {
326
+ this.d0.set(0.5 * period - filters, w);
327
+ this.d1.set(0.5 * period * (0.9 + 0.2 * p.reed), w);
328
+ } else {
329
+ this.d0.set(1.5 * period - filters, w);
330
+ this.d1.set(0.5 * period * (0.9 + 0.2 * p.reed), w);
331
+ }
332
+ break;
333
+ }
334
+ case "lips":
335
+ // STK Brass: a bore of two periods sounding its 2nd mode and a lip
336
+ // resonance a little below f (constant Q 12, unit DC gain x 3 so
337
+ // every register speaks); the trim removes the pull (+15..45 c).
338
+ // Calibration 1 tunes the lips to f at Q 40 (see WindTrim.lips).
339
+ this.d0.set(2 * period - dcBlockDelay(this.dc.r, w), w);
340
+ if (this.trim.lips)
341
+ this.lip.set(f * (1 + 0.04 * (p.reed - 0.5)), 40, 3, rate);
342
+ else this.lip.set(f * (0.95 + 0.04 * (p.reed - 0.5)), 12, 3, rate);
343
+ break;
344
+ }
345
+ // Brightness for the reed, sax and lip bores: a high shelf from about
346
+ // the 4th harmonic, +-9 dB around bright 0.5. It sits outside the loop
347
+ // (after the bore, before the mute), so pitch and the trims stay put;
348
+ // the jet carries bright in its loop low-pass instead.
349
+ if (p.model !== "jet")
350
+ this.tone.set(
351
+ "highshelf",
352
+ clamp(4 * hz, 500, 0.4 * sr),
353
+ 0.7,
354
+ (p.bright - 0.5) * 18,
355
+ sr,
356
+ );
357
+ // Oscillation pressure keeps a floor; breath gain follows breath.
358
+ const v = clamp(note.velocity + (note.push ?? 0), 0, 1);
359
+ const drive = clamp(p.breath * (0.55 + 0.45 * v), 0, 1);
360
+ const floor = 0.45;
361
+ switch (p.model) {
362
+ case "reed":
363
+ this.pressure = floor + 0.45 * drive;
364
+ break;
365
+ case "sax":
366
+ this.pressure = floor + 0.45 * drive;
367
+ break;
368
+ case "jet":
369
+ this.pressure = 0.9 + 0.35 * drive;
370
+ break;
371
+ case "lips":
372
+ this.pressure = 0.6 + 0.5 * drive;
373
+ break;
374
+ }
375
+ this.breathGain = 10 ** ((30 * (p.breath - 0.6)) / 20);
376
+ // Plunger: wah, opened per note by wahenv.
377
+ if (p.mute === "plunger") {
378
+ const open = Math.min(1, t / 0.15);
379
+ this.wah = p.wah + p.wahenv * (1 - p.wah) * open;
380
+ this.wahFilter.set("lpf", 400 + 4600 * this.wah * this.wah, 1.4, 0, sr);
381
+ }
382
+ }
383
+
384
+ /** One internal-rate sample of the bore. */
385
+ private tick(breath: number): number {
386
+ const p = this.settings;
387
+ switch (p.model) {
388
+ case "reed": {
389
+ const bore = this.d0.read();
390
+ const oz = 0.5 * (bore + this.oz1);
391
+ this.oz1 = bore;
392
+ const pdiff = -0.95 * oz - breath;
393
+ this.d0.push(breath + this.shaper!.process(pdiff));
394
+ return bore;
395
+ }
396
+ case "sax": {
397
+ const a = this.d0.read();
398
+ const b = this.d1.read();
399
+ const oz = 0.5 * (a + this.oz1);
400
+ this.oz1 = a;
401
+ const temp = -0.95 * oz;
402
+ const frame = temp - b;
403
+ const pdiff = breath - frame;
404
+ this.d1.push(temp);
405
+ // Plain reed table (no ADAA): measured, ADAA's half-sample average
406
+ // destabilises the conical loop above ~300 Hz; 2x keeps aliasing low.
407
+ this.d0.push(
408
+ breath - pdiff * clamp(0.7 + this.reedSlope * pdiff, -1, 1) - temp,
409
+ );
410
+ return frame;
411
+ }
412
+ case "jet": {
413
+ const boreOut = this.d0.read();
414
+ const temp = this.dc.process(-this.loopLp.process(boreOut));
415
+ const jet = this.d1.read();
416
+ this.d1.push(breath - 0.5 * temp);
417
+ // Jet offset: the jet hits the edge off-centre, giving even harmonics.
418
+ const offset = p.stopped ? 0 : 0.12;
419
+ const jt = this.shaper!.process(jet + offset) - jetG(offset);
420
+ this.d0.push(jt + 0.5 * temp);
421
+ // Edge tap (open pipes): some of the jet itself is heard.
422
+ return 0.3 * boreOut + (p.stopped ? 0 : 0.12 * jt);
423
+ }
424
+ case "lips": {
425
+ const bore = 0.85 * this.d0.read();
426
+ const mouth = 0.3 * breath;
427
+ let delta = this.lip.process(mouth - bore);
428
+ delta *= delta;
429
+ // Calibration 1: a soft opening (x / (1 + x)) in place of the hard
430
+ // clamp, whose corner fed the warble.
431
+ if (this.trim.lips) delta = delta / (1 + delta);
432
+ else if (delta > 1) delta = 1;
433
+ const frame = delta * mouth + (1 - delta) * bore;
434
+ this.d0.push(this.dc.process(frame));
435
+ return bore * 2;
436
+ }
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Adds `count` base-rate samples into `out` from `offset`; false once the
442
+ * voice has finished.
443
+ */
444
+ process(out: Float64Array, offset: number, count: number): boolean {
445
+ const { sampleRate: sr, note } = this;
446
+ let p = this.settings;
447
+ const attack = Math.max(1, p.attack * sr);
448
+ const release = Math.max(1, p.release * sr);
449
+ const level = this.trim.level * p.gain;
450
+ for (let i = 0; i < count; i += 1) {
451
+ const n = this.n;
452
+ if (n >= this.endAt) return false;
453
+ if (n % WIND_BLOCK === 0 && n > 0) {
454
+ this.control(n);
455
+ p = this.settings;
456
+ }
457
+ if (n < note.length)
458
+ this.envelope = Math.min(1, this.envelope + 1 / attack);
459
+ else this.envelope = Math.max(0, this.envelope - 1 / release);
460
+ const t = n / sr;
461
+ let amp = this.pressure * this.envelope;
462
+ if (p.vibmod > 0) amp *= 1 + 0.04 * Math.sin(2 * Math.PI * p.vib * t);
463
+ if (p.flutter > 0)
464
+ amp *=
465
+ 1 - p.flutter * 0.5 * (0.5 + 0.5 * Math.sin(2 * Math.PI * 26 * t));
466
+ if (p.growl > 0)
467
+ amp *= 1 + p.growl * 0.25 * Math.sin(2 * Math.PI * this.growlHz * t);
468
+ let y: number;
469
+ if (this.os) {
470
+ const a = this.tick(this.breath(amp, p));
471
+ const b = this.tick(this.breath(amp, p));
472
+ y = this.decimate!.process(a, b);
473
+ } else y = this.tick(this.breath(amp, p));
474
+ if (p.growl > 0) y = Math.tanh(y * (1 + 2 * p.growl)) / (1 + p.growl);
475
+ if (p.model !== "jet") y = this.tone.process(y);
476
+ for (const filter of this.mutes) y = filter.process(y);
477
+ if (p.mute === "plunger") y = this.wahFilter.process(y);
478
+ y = this.shelf.process(y);
479
+ this.gainNow += (this.gainTarget - this.gainNow) * 0.002;
480
+ out[offset + i]! +=
481
+ this.outDc.process(y) * level * this.gainNow * this.breathGain;
482
+ this.n += 1;
483
+ }
484
+ return true;
485
+ }
486
+
487
+ /** Breath pressure with low-passed noise. */
488
+ private breath(amp: number, p: WindSettings): number {
489
+ const noise = this.noiseLp.process(this.random() * 2 - 1);
490
+ return amp * (1 + p.noise * 1.6 * noise);
491
+ }
492
+ }
package/src/auth/cli.ts CHANGED
@@ -24,19 +24,20 @@ import { selectProvider } from "../agent/provider.ts";
24
24
  import { audioStatusLine } from "../audio/engine.ts";
25
25
 
26
26
  export const AUTH_USAGE = `Usage:
27
- dawg login find existing setups and pick a provider (reuses a saved one)
28
- dawg login gateway Vercel AI Gateway: existing key, or create one with the Vercel CLI
29
- dawg login openrouter OpenRouter: sign in through your browser (OAuth PKCE)
30
- dawg login codex ChatGPT/Codex subscription through xcb
31
- dawg login claude Claude subscription through xcb
32
- dawg login --pick show the picker even when a provider is saved
33
- dawg login --key paste an AI Gateway key (hidden input)
34
- dawg login --xcb any xcb account (Devin included)
35
- dawg login gateway --budget <dollars> spend limit for a created key
36
- dawg login codex|claude [--account <id>] [--model <key>]
27
+ dawg model key find existing setups and pick a provider (reuses a saved one)
28
+ dawg model key gateway Vercel AI Gateway: existing key, or create one with the Vercel CLI
29
+ dawg model key openrouter OpenRouter: authorize in your browser (OAuth PKCE)
30
+ dawg model key codex ChatGPT/Codex subscription through xcb
31
+ dawg model key claude Claude subscription through xcb
32
+ dawg model key --pick show the picker even when a provider is saved
33
+ dawg model key --key paste an AI Gateway key (hidden input)
34
+ dawg model key --xcb any xcb account (Devin included)
35
+ dawg model key gateway --budget <dollars> spend limit for a created key
36
+ dawg model key codex|claude [--account <id>] [--model <key>]
37
37
  dawg model [<alias>|<vendor/model>] pick the model (cost per prompt shown)
38
38
  dawg logout [gateway|openrouter|codex|claude|xcb] forget keys and the saved choice
39
- dawg auth status [--check] every provider: detected, active, validated`;
39
+ dawg auth status [--check] every provider: detected, active, validated
40
+ The older spelling of \`model key\` still works as an alias.`;
40
41
 
41
42
  /** Entry for `dawg login|logout|auth|model`. Returns the process exit code. */
42
43
  export async function runAuthCommand(argv: readonly string[]): Promise<number> {
@@ -82,40 +83,41 @@ export async function runAuthCommand(argv: readonly string[]): Promise<number> {
82
83
  return login(parsed.target, deps, parsed.options);
83
84
  }
84
85
 
86
+ /** A one-time card the TUI shows at launch about the agent. */
87
+ export type FirstRunCard = Readonly<{
88
+ text: string;
89
+ tone: "info" | "warning";
90
+ }>;
91
+
85
92
  /**
86
- * The first run of `dawg` in a terminal: when nothing is configured (and the
87
- * picker was never dismissed), or a saved sign-in stopped working, show the
88
- * sign-in picker before the TUI starts. Dismissing it is remembered.
93
+ * The launch note about the agent. dawg opens straight into the TUI: music
94
+ * first, sign-in optional. The first session with no provider gets one
95
+ * faint card, `/model key adds an agent · optional`, and records that it was
96
+ * shown so later sessions stay quiet; a saved sign-in that stopped working
97
+ * says so. `dawg login` and `/login` keep the picker.
89
98
  */
90
- export async function runFirstRunLogin(
99
+ export async function firstRunCard(
91
100
  runner: CommandRunner = systemRunner,
92
- ): Promise<void> {
101
+ ): Promise<FirstRunCard | undefined> {
93
102
  const auth = defaultAuthEnv(runner);
94
103
  const config = await readConfig(auth).catch(() => ({}) as DawgConfig);
95
- if (auth.env.DAWG_PROVIDER) return;
96
- if (!config.provider && config.setup === "skipped") return;
104
+ if (auth.env.DAWG_PROVIDER) return undefined;
105
+ if (!config.provider && config.setup === "skipped") return undefined;
97
106
  const selection = await selectProvider(auth).catch(() => undefined);
98
- if (!selection || selection.kind !== "offline") return;
99
- if (config.provider && !selection.invalidSaved) return;
100
- const io = terminalIO();
101
- if (!io.interactive) return;
107
+ if (!selection || selection.kind !== "offline") return undefined;
102
108
  if (selection.invalidSaved)
103
- io.print(`Your saved sign-in stopped working: ${selection.reason}.`);
104
- else io.print("Welcome to dawg. Sign in to enable the agent (Esc to skip).");
105
- const code = await login("pick", {
106
- auth,
107
- io,
108
- hostname: hostname(),
109
- }).catch((error: unknown) => {
110
- io.print(
111
- `sign-in failed: ${error instanceof Error ? error.message : String(error)}`,
112
- );
113
- return 1;
114
- });
115
- if (code !== 0 && !config.provider)
116
- await writeConfig(auth, { setup: "skipped" }).catch(() => undefined);
109
+ return {
110
+ text: `saved sign-in stopped working: ${selection.reason} · /login`,
111
+ tone: "warning",
112
+ };
113
+ if (config.provider) return undefined;
114
+ await writeConfig(auth, { setup: "skipped" }).catch(() => undefined);
115
+ return { text: FIRST_RUN_CARD, tone: "info" };
117
116
  }
118
117
 
118
+ /** The first session's agent note. */
119
+ export const FIRST_RUN_CARD = "/model key adds an agent · optional";
120
+
119
121
  /** `/login` in the TUI, run while the screen is handed back to the shell. */
120
122
  export async function runTuiLogin(
121
123
  target: LoginTarget,
@@ -83,11 +83,29 @@ export type DawgConfig = Readonly<{
83
83
  openrouterModel?: string;
84
84
  /** Set when the first-run picker was dismissed, so it does not nag. */
85
85
  setup?: "skipped";
86
+ /** Show-me level of agent turns (`/showme`); absent is on. */
87
+ showMe?: "on" | "quiet" | "off";
86
88
  }>;
87
89
 
88
90
  /** `vendor/model` IDs as the AI Gateway and OpenRouter spell them. */
89
91
  export const PROVIDER_MODEL_ID_PATTERN =
90
92
  /^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}$/i;
93
+ /**
94
+ * OpenRouter also takes a routing variant after a colon: `:nitro`
95
+ * (throughput first), `:floor` (price first), `:free`, `:online` and so on.
96
+ */
97
+ export const OPENROUTER_MODEL_ID_PATTERN =
98
+ /^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}(?::[a-z0-9-]{1,32})?$/i;
99
+
100
+ /** True for an ID the given provider accepts; any provider when omitted. */
101
+ export function isProviderModelId(
102
+ id: string,
103
+ provider?: "gateway" | "openrouter",
104
+ ): boolean {
105
+ return provider === "gateway"
106
+ ? PROVIDER_MODEL_ID_PATTERN.test(id)
107
+ : OPENROUTER_MODEL_ID_PATTERN.test(id);
108
+ }
91
109
 
92
110
  export type CredentialSource = "env" | "keychain" | "file" | "oidc";
93
111
  export type ResolvedKey = Readonly<{ key: string; source: CredentialSource }>;
@@ -314,6 +332,7 @@ export function parseConfig(value: unknown): DawgConfig {
314
332
  gatewayModel?: string;
315
333
  openrouterModel?: string;
316
334
  setup?: "skipped";
335
+ showMe?: "on" | "quiet" | "off";
317
336
  } = {};
318
337
  if (PROVIDER_CHOICES.includes(record.provider as ProviderChoice))
319
338
  config.provider = record.provider as ProviderChoice;
@@ -323,10 +342,19 @@ export function parseConfig(value: unknown): DawgConfig {
323
342
  }
324
343
  for (const field of ["gatewayModel", "openrouterModel"] as const) {
325
344
  const id = record[field];
326
- if (typeof id === "string" && PROVIDER_MODEL_ID_PATTERN.test(id))
345
+ if (
346
+ typeof id === "string" &&
347
+ isProviderModelId(id, field === "gatewayModel" ? "gateway" : "openrouter")
348
+ )
327
349
  config[field] = id;
328
350
  }
329
351
  if (record.setup === "skipped") config.setup = "skipped";
352
+ if (
353
+ record.showMe === "on" ||
354
+ record.showMe === "quiet" ||
355
+ record.showMe === "off"
356
+ )
357
+ config.showMe = record.showMe;
330
358
  return config;
331
359
  }
332
360
 
@@ -350,6 +378,7 @@ export type ConfigPatch = Partial<{
350
378
  gatewayModel: string | undefined;
351
379
  openrouterModel: string | undefined;
352
380
  setup: "skipped" | undefined;
381
+ showMe: "on" | "quiet" | "off" | undefined;
353
382
  }>;
354
383
 
355
384
  export async function writeConfig(