@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
package/src/audio/live.ts CHANGED
@@ -19,10 +19,18 @@ import {
19
19
  import { bpmAtTick } from "../../core/tempo.ts";
20
20
  import { EMPTY_SAMPLE_BANK, sampleKey, type SampleBank } from "./samples.ts";
21
21
  import { RENDER_CHANNELS, renderScorePcm } from "./wav.ts";
22
- import { engineFor } from "./instruments.ts";
22
+ import { engineFor, RING_OUT_FADE_SECONDS } from "./instruments.ts";
23
+ import { chordDigest, chordTimeline } from "./granular.ts";
24
+ import { resolveGranular } from "../../core/granular.ts";
23
25
  import { noteHz, resolveTuning } from "../../core/tuning.ts";
24
26
  import { liveFitPending, withLiveFit } from "./fit.ts";
27
+ import { isOrganFamily } from "../../core/keys.ts";
25
28
  import type { LiveFullReply, LiveFullRequest } from "./live-worker.ts";
29
+ import { isBowed, liveStringTrack } from "./strings/engine.ts";
30
+ import { resolveString } from "../../core/strings.ts";
31
+ import { singTrack } from "../../core/sing.ts";
32
+ import { warmGlottal } from "./dsp/glottal.ts";
33
+ import { withSingHorizon } from "./sing/engine.ts";
26
34
 
27
35
  /** A rendered live note: interleaved stereo 16-bit PCM. */
28
36
  export type LiveNotePcm = Readonly<{
@@ -37,11 +45,22 @@ export type LiveNotePcm = Readonly<{
37
45
  */
38
46
  fitting?: true;
39
47
  /**
40
- * True when this is only the first window of a guitar-rig note
48
+ * True when this is only the first window of a guitar-rig or bowed note
41
49
  * (`LIVE_RIG_WINDOW_SECONDS`): call `LiveSynth.render` again with
42
50
  * `full: true` off the key path and swap the result in by voice id.
43
51
  */
44
52
  partial?: boolean;
53
+ /**
54
+ * The organ clock tick a first window rendered at: pass it back as the
55
+ * request's `clock` for the full pass so the wheels and rotors continue.
56
+ */
57
+ clock?: number;
58
+ /**
59
+ * The key's release does not fade the voice: it rings to the end of its
60
+ * rendered tail (0.6.1 modal `damp 0`), which ends in a `RING_OUT_FADE_SECONDS`
61
+ * fade when the length cap cut it.
62
+ */
63
+ ringOut?: true;
45
64
  }>;
46
65
 
47
66
  export type LiveNoteRequest = Readonly<{
@@ -58,6 +77,8 @@ export type LiveNoteRequest = Readonly<{
58
77
  tick?: number;
59
78
  /** Render the whole note even through a guitar rig (the background pass). */
60
79
  full?: boolean;
80
+ /** An organ note's clock tick from its first window (`LiveNotePcm.clock`). */
81
+ clock?: number;
61
82
  }>;
62
83
 
63
84
  /**
@@ -78,8 +99,21 @@ const CACHE_ENTRIES = 96;
78
99
  */
79
100
  export class LiveSynth {
80
101
  private readonly cache = new Map<string, LiveNotePcm>();
102
+ /** f061-organ: the live organ clock's origin (performance.now ms). */
103
+ private readonly organEpoch = performance.now();
81
104
 
82
- public constructor(private readonly sampleRate: number) {}
105
+ /**
106
+ * `fastTail: false` always renders the whole tail window (tests compare
107
+ * the release-sized buffer against it).
108
+ */
109
+ public constructor(
110
+ private readonly sampleRate: number,
111
+ private readonly options: Readonly<{
112
+ fastTail?: boolean;
113
+ /** Called with each one-note render's buffer length, in frames. */
114
+ onRender?: (frames: number) => void;
115
+ }> = {},
116
+ ) {}
83
117
 
84
118
  public get rate(): number {
85
119
  return this.sampleRate;
@@ -122,6 +156,38 @@ export class LiveSynth {
122
156
  );
123
157
  const velocity = Math.max(0, Math.min(1, request.velocity));
124
158
  const pitch = Math.max(0, Math.min(127, Math.round(request.pitch)));
159
+ // f061-organ: one rotating speaker and one set of free-running wheels
160
+ // per organ track. Each key renders from the song tick it sounds at
161
+ // (or the synth's own clock when stopped), so successive notes share
162
+ // the track's rotor and wheel phases instead of restarting them.
163
+ const organTick =
164
+ track.keys && isOrganFamily(track.instrument)
165
+ ? Math.max(
166
+ 0,
167
+ Math.round(
168
+ request.clock ??
169
+ request.tick ??
170
+ ((performance.now() - this.organEpoch) / 1000) * ticksPerSecond,
171
+ ),
172
+ )
173
+ : undefined;
174
+ // Granular `quant chord` (0.6.1): a live note snaps to the song's
175
+ // chords at the played tick, as the offline render of the same note.
176
+ const quantChord =
177
+ track.granular !== undefined &&
178
+ liveEngine?.id === "granular" &&
179
+ resolveGranular(track.granular).quant === "chord"
180
+ ? { score, tick: request.tick ?? 0 }
181
+ : undefined;
182
+ const chord = quantChord
183
+ ? chordDigest(
184
+ chordTimeline(score),
185
+ quantChord.tick,
186
+ quantChord.tick +
187
+ durationTicks +
188
+ Math.ceil(liveEngine!.tailSeconds(track, pitch) * ticksPerSecond),
189
+ )
190
+ : undefined;
125
191
  const key = JSON.stringify([
126
192
  liveTrack(track),
127
193
  pitch,
@@ -137,6 +203,7 @@ export class LiveSynth {
137
203
  ...(score.tuning || track.tuning
138
204
  ? [score.tuning ?? null, score.key]
139
205
  : []),
206
+ ...(organTick !== undefined ? [`organ:${organTick}`] : []),
140
207
  // A 0.6 engine's assets, and the key it may tune to.
141
208
  ...(liveEngine
142
209
  ? [
@@ -147,21 +214,48 @@ export class LiveSynth {
147
214
  score.key ?? null,
148
215
  ]
149
216
  : []),
217
+ ...(chord ? [chord] : []),
150
218
  ]);
151
- return { track, score, bpm, seconds, durationTicks, velocity, pitch, key };
219
+ return {
220
+ track,
221
+ score,
222
+ bpm,
223
+ seconds,
224
+ durationTicks,
225
+ velocity,
226
+ pitch,
227
+ key,
228
+ organTick,
229
+ quantChord,
230
+ };
152
231
  }
153
232
 
154
233
  public render(request: LiveNoteRequest): LiveNotePcm | undefined {
155
234
  const plan = this.plan(request);
156
235
  if (!plan) return undefined;
157
- const { track, score, bpm, seconds, durationTicks, velocity, pitch, key } =
158
- plan;
236
+ const {
237
+ track,
238
+ score,
239
+ bpm,
240
+ seconds,
241
+ durationTicks,
242
+ velocity,
243
+ pitch,
244
+ key,
245
+ organTick,
246
+ quantChord,
247
+ } = plan;
159
248
  const liveEngine = engineFor(track);
160
249
  // The first window of a rig note: its own cache entry, swapped for the
161
250
  // whole note once the background pass has rendered it.
251
+ // Organs too: wheels, rotors and drive cost 4-8 ms per note-second.
162
252
  const windowed =
163
253
  request.full !== true &&
164
- hasRig(track) &&
254
+ (hasRig(track) ||
255
+ organTick !== undefined ||
256
+ bowedTrack(track) ||
257
+ hasFormant(track) ||
258
+ singTrack(track)) &&
165
259
  seconds > LIVE_RIG_WINDOW_SECONDS &&
166
260
  !this.cache.has(key);
167
261
  if (windowed) {
@@ -206,22 +300,8 @@ export class LiveSynth {
206
300
  : 0;
207
301
  // Fitted sample windows over 8 s fit in the background (silent until
208
302
  // ready, never at the wrong pitch); shorter ones fit synchronously.
209
- const audio = withLiveFit(() =>
210
- renderScorePcm(single, {
211
- sampleRate: this.sampleRate,
212
- // The renderer's shortest one-shot is 1 s; the window is cut below.
213
- maxSeconds: windowed
214
- ? 1
215
- : Math.max(MAX_LIVE_NOTE_SECONDS + 4, seconds + tail),
216
- ...(request.samples ? { samples: request.samples } : {}),
217
- }),
218
- );
219
- const frames = windowed
220
- ? Math.min(
221
- audibleFrames(audio.pcm),
222
- Math.round(LIVE_RIG_WINDOW_SECONDS * this.sampleRate),
223
- )
224
- : audibleFrames(audio.pcm);
303
+ const windowFrames = Math.round(LIVE_RIG_WINDOW_SECONDS * this.sampleRate);
304
+ const horizon = windowed ? windowFrames : Infinity;
225
305
  const release = liveEngine?.releaseSeconds
226
306
  ? liveEngine.releaseSeconds(
227
307
  track,
@@ -233,9 +313,51 @@ export class LiveSynth {
233
313
  ),
234
314
  )
235
315
  : tail;
316
+ const renderFor = (maxSeconds: number) => {
317
+ const out = withLiveFit(() =>
318
+ withSingHorizon(horizon, () =>
319
+ renderScorePcm(single, {
320
+ sampleRate: this.sampleRate,
321
+ maxSeconds,
322
+ ...(request.samples ? { samples: request.samples } : {}),
323
+ ...(organTick ? { seedTick: organTick } : {}),
324
+ ...(quantChord ? { quantChord } : {}),
325
+ }),
326
+ ),
327
+ );
328
+ this.options.onRender?.(out.frames);
329
+ return out;
330
+ };
331
+ const fullSeconds = Math.max(MAX_LIVE_NOTE_SECONDS + 4, seconds + tail);
332
+ // A damped key decays 60 dB per 4.6 x its release (a linear fade of the
333
+ // same energy), so 12 x release is past -150 dB: silent in 16 bits. The
334
+ // renderer is causal, so the short buffer is the full one's prefix; a
335
+ // note still sounding at its end falls back to the full window.
336
+ const shortSeconds =
337
+ !windowed &&
338
+ this.options.fastTail !== false &&
339
+ liveFastTail(track, liveEngine !== undefined)
340
+ ? seconds + Math.min(tail, LIVE_TAIL_RELEASES * release) + 0.1
341
+ : Infinity;
342
+ let audio =
343
+ // The renderer's shortest one-shot is 1 s; the window is cut below.
344
+ windowed ? renderFor(1) : renderFor(Math.min(fullSeconds, shortSeconds));
345
+ if (
346
+ shortSeconds < fullSeconds &&
347
+ audibleFrames(audio.pcm) >
348
+ audio.frames - Math.round(LIVE_TAIL_GUARD_SECONDS * this.sampleRate)
349
+ )
350
+ audio = renderFor(fullSeconds);
351
+ const frames = windowed
352
+ ? Math.min(audibleFrames(audio.pcm), windowFrames)
353
+ : audibleFrames(audio.pcm);
354
+ const ringOut = !windowed && liveEngine?.ringOut?.(track) === true;
355
+ const pcm = audio.pcm.subarray(0, frames * RENDER_CHANNELS);
356
+ if (ringOut) fadeCutTail(pcm, frames, this.sampleRate);
236
357
  const rendered: LiveNotePcm = {
237
- pcm: audio.pcm.subarray(0, frames * RENDER_CHANNELS),
358
+ pcm,
238
359
  frames,
360
+ ...(ringOut ? { ringOut: true as const } : {}),
239
361
  ...(liveEngine
240
362
  ? {
241
363
  releaseSeconds: Math.min(
@@ -245,8 +367,11 @@ export class LiveSynth {
245
367
  }
246
368
  : {}),
247
369
  ...(windowed ? { partial: true } : {}),
370
+ ...(windowed && organTick !== undefined ? { clock: organTick } : {}),
248
371
  };
249
372
  if (liveFitPending()) return { ...rendered, fitting: true };
373
+ // Organ keys render at their song tick: caching them would only evict.
374
+ if (organTick !== undefined) return rendered;
250
375
  this.cache.set(windowed ? `${key}#window` : key, rendered);
251
376
  this.trim();
252
377
  return rendered;
@@ -261,22 +386,70 @@ export class LiveSynth {
261
386
  }
262
387
  }
263
388
 
389
+ /**
390
+ * Pays a voice's one-off costs before a key can sound (formant.md 4.5): the
391
+ * sing engine builds its LF glottal tables (about 50 ms) on first use and
392
+ * its inner loops need a pass to compile, so the first sing key of a session
393
+ * would miss the 15 ms first-window budget by 4x. Every other voice has the
394
+ * same shape at a smaller scale: a cold first press of a grand, a bowed
395
+ * cello, a granular swarm or a track with effects costs 2-4x a warm one
396
+ * (docs/perf.md, `live.*`). Play mode calls this when it opens on a track,
397
+ * so the first key costs what later keys do. The warm render goes
398
+ * through a throwaway synth, so no cache entry or state is left behind.
399
+ */
400
+ export function warmLive(
401
+ score: TrackScore,
402
+ trackId: string,
403
+ sampleRate: number,
404
+ ): void {
405
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
406
+ if (!track) return;
407
+ if (singTrack(track)) warmGlottal();
408
+ const synth = new LiveSynth(sampleRate);
409
+ for (const pitch of [48, 60])
410
+ synth.render({ score, trackId, pitch, velocity: 0.8, seconds: 0.3 });
411
+ }
412
+
264
413
  /**
265
414
  * The track as it sounds live: audible regardless of mute and solo, and
266
415
  * without track `time`, so a key sounds once, as played, not re-placed.
267
416
  */
268
417
  function liveTrack(track: Track): Track {
269
- // A held key is the performance: the recorded pedal and the render-time
418
+ // A held key is the performance: the recorded pedals and the render-time
270
419
  // humanize stay out of it. The velocity curve stays, so a key sounds like
271
420
  // the note it records as.
272
421
  const {
273
422
  solo: _solo,
274
423
  time: _time,
275
424
  pedal: _pedal,
425
+ softPedal: _softPedal,
426
+ sostenuto: _sostenuto,
276
427
  humanize: _humanize,
277
428
  ...rest
278
429
  } = track;
279
- return { ...rest, muted: false };
430
+ // A bowed section plays two players live (f061-bowed render budget).
431
+ return liveStringTrack({ ...rest, muted: false });
432
+ }
433
+
434
+ /**
435
+ * Whether the track is a bowed string: a held bow costs about 5 ms per
436
+ * second, so it windows like a rig (the bow loop is causal too).
437
+ */
438
+ function bowedTrack(track: Track): boolean {
439
+ return (
440
+ track.instrument === "string" &&
441
+ track.string !== undefined &&
442
+ isBowed(resolveString(track.string))
443
+ );
444
+ }
445
+
446
+ /**
447
+ * Whether the track plays through the 0.7 formant fx. It is not causal (one
448
+ * STFT frame of lookahead), but the window renders a 1 s source, well past
449
+ * the 0.75 s window plus one frame, so the window is an exact prefix.
450
+ */
451
+ function hasFormant(track: Track): boolean {
452
+ return Boolean(track.fx?.formant);
280
453
  }
281
454
 
282
455
  /** Whether the track plays through a guitar rig stage. */
@@ -299,6 +472,53 @@ function samplerDigest(track: Track, bank: SampleBank | undefined): string[] {
299
472
  }
300
473
 
301
474
  /** Frames up to the last non-silent sample, so mixing skips the dead tail. */
475
+ /**
476
+ * A ring-out note cut by the render length still sounds at its last frame:
477
+ * fade its final `RING_OUT_FADE_SECONDS` linearly to zero so the cap never
478
+ * clicks (each step stays under -60 dB of full scale). A tail that already
479
+ * decayed to silence is left alone.
480
+ */
481
+ export function fadeCutTail(
482
+ pcm: Int16Array,
483
+ frames: number,
484
+ sampleRate: number,
485
+ ): void {
486
+ if (frames === 0) return;
487
+ const last = (frames - 1) * RENDER_CHANNELS;
488
+ // Below -60 dBFS the cut is inaudible.
489
+ if (Math.abs(pcm[last]!) < 33 && Math.abs(pcm[last + 1]!) < 33) return;
490
+ const fade = Math.min(frames, Math.round(RING_OUT_FADE_SECONDS * sampleRate));
491
+ for (let i = 0; i < fade; i += 1) {
492
+ const frame = frames - fade + i;
493
+ const gain = (fade - 1 - i) / fade;
494
+ for (let c = 0; c < RENDER_CHANNELS; c += 1) {
495
+ const at = frame * RENDER_CHANNELS + c;
496
+ pcm[at] = Math.round(pcm[at]! * gain);
497
+ }
498
+ }
499
+ }
500
+
501
+ /** A short live render ends at 12 x the engine's release past the note. */
502
+ const LIVE_TAIL_RELEASES = 12;
503
+ /** Audio within this much of a short render's end means it was cut. */
504
+ const LIVE_TAIL_GUARD_SECONDS = 0.05;
505
+
506
+ /**
507
+ * Whether a live note may render a buffer sized by its engine's release
508
+ * rather than the whole tail window: only a 0.6 engine's dry track (echoes
509
+ * and reverb tails are not bounded by the key's release).
510
+ */
511
+ function liveFastTail(track: Track, engine: boolean): boolean {
512
+ return (
513
+ engine &&
514
+ track.delay === undefined &&
515
+ track.reverb === undefined &&
516
+ (track.fx === undefined || Object.keys(track.fx).length === 0) &&
517
+ (track.fxAutomation === undefined ||
518
+ Object.keys(track.fxAutomation).length === 0)
519
+ );
520
+ }
521
+
302
522
  function audibleFrames(pcm: Int16Array): number {
303
523
  let last = pcm.length - 1;
304
524
  while (last >= 0 && pcm[last] === 0) last -= 1;
@@ -344,6 +564,7 @@ export class LiveFullRenderer {
344
564
  velocity: request.velocity,
345
565
  seconds: request.seconds,
346
566
  ...(request.tick === undefined ? {} : { tick: request.tick }),
567
+ ...(request.clock === undefined ? {} : { clock: request.clock }),
347
568
  ...(request.samples ? { samples: request.samples } : {}),
348
569
  };
349
570
  try {