@hraness/dawg 0.6.1 → 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 (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
package/src/audio/wav.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type Track,
10
10
  type TrackScore,
11
11
  } from "../../core/score.ts";
12
+ import { autotuneStemDigests } from "./autotune.ts";
12
13
  import { drumVoiceForPitch, isDrumInstrument } from "../../core/drums.ts";
13
14
  import {
14
15
  performanceTimingFor,
@@ -21,12 +22,18 @@ import {
21
22
  renderSamplerVoices,
22
23
  samplerTailSeconds,
23
24
  } from "./sampler.ts";
24
- import { EFFECT_NAMES, FX_PRESETS, effectSpec } from "../../core/fx.ts";
25
+ import {
26
+ EFFECT_NAMES,
27
+ FX_CHAIN,
28
+ FX_PRESETS,
29
+ effectSpec,
30
+ } from "../../core/fx.ts";
25
31
  import { seededRandom } from "./random.ts";
26
32
  import {
27
33
  engineFor,
28
34
  engineTailSeconds,
29
35
  RING_OUT_FADE_SECONDS,
36
+ type InstrumentEngine,
30
37
  } from "./instruments.ts";
31
38
  import {
32
39
  isStereoVoice,
@@ -35,7 +42,7 @@ import {
35
42
  usesSynthVoice,
36
43
  type VoiceContext,
37
44
  } from "./synth/voice.ts";
38
- import { legacyWave } from "./synth/oscillators.ts";
45
+ import { legacyWaveFor } from "./synth/oscillators.ts";
39
46
  import {
40
47
  EMPTY_SAMPLE_BANK,
41
48
  sampleKey,
@@ -43,7 +50,15 @@ import {
43
50
  type SampleBank,
44
51
  } from "./samples.ts";
45
52
  import { synthKit } from "../../core/kits.ts";
46
- import { kitDrumSample, kitTailSeconds, newKitVoiceState } from "./kits.ts";
53
+ import {
54
+ kitDrumSample,
55
+ kitTailSeconds,
56
+ METAL_SECONDS,
57
+ metalKindForPitch,
58
+ metalSample,
59
+ newKitVoiceState,
60
+ tomRatio,
61
+ } from "./kits.ts";
47
62
  import {
48
63
  addReverbWet,
49
64
  applyMonoChain,
@@ -79,8 +94,13 @@ import {
79
94
  slowestBpmOf,
80
95
  } from "../../core/tempo.ts";
81
96
  import { sampleWarpFor, warpedSpan, type SampleWarp } from "./warp.ts";
97
+ import { clipsDigest, hasClips, renderClips } from "./clips.ts";
98
+ import { isGuideInstrument } from "../../core/clips.ts";
82
99
  import { applyMaster, type MasterReport } from "./master.ts";
83
100
  import type { SongMaster } from "../../core/master.ts";
101
+ import { resolveTrackRef } from "../../core/routing.ts";
102
+ import { resolveVocoder, VOCODER_INSTRUMENT } from "../../core/vocoder.ts";
103
+ import { applyVocoder, autoGateDb } from "./vocoder/index.ts";
84
104
 
85
105
  export type WavOptions = Readonly<{ sampleRate?: number; maxSeconds?: number }>;
86
106
 
@@ -92,6 +112,12 @@ export type RenderOptions = WavOptions &
92
112
  * what the streaming engine plays; exports keep the default one-shot.
93
113
  */
94
114
  loop?: boolean;
115
+ /**
116
+ * 0.7 clips: a `vocal` track's guide notes sound as a soft sine. The
117
+ * live loop (audition, play mode) sets it; exports never do, so a
118
+ * rendered file holds only the clips.
119
+ */
120
+ guide?: boolean;
95
121
  /**
96
122
  * Decoded sampler voices (`SampleLibrary.load`). Without it sampler
97
123
  * tracks render silent; scores without samplers are unaffected.
@@ -126,8 +152,36 @@ export type RenderedAudio = Readonly<{
126
152
  pcm: Int16Array;
127
153
  /** Loudness the song master reached; absent without a master. */
128
154
  master?: MasterReport;
155
+ /**
156
+ * Non-finite samples (NaN, Infinity) a track's source or effects made,
157
+ * zeroed before they could poison a feedback stage or the mix (q08);
158
+ * absent when every sample was finite.
159
+ */
160
+ nonFinite?: NonFiniteReport;
129
161
  }>;
130
162
 
163
+ /** Where a render zeroed non-finite samples. */
164
+ export type NonFiniteReport = Readonly<{
165
+ samples: number;
166
+ /** Track ids in render order, then `bus` for the bus returns. */
167
+ tracks: readonly string[];
168
+ }>;
169
+
170
+ /**
171
+ * Zeroes NaN and Infinity in `buffer` and returns how many it found. A
172
+ * NaN written into an Int16Array stores 0, and one fed to a filter, comb
173
+ * or reverb feedback silences everything after it.
174
+ */
175
+ export function scrubNonFinite(buffer: Float64Array): number {
176
+ let count = 0;
177
+ for (let i = 0; i < buffer.length; i += 1)
178
+ if (!Number.isFinite(buffer[i]!)) {
179
+ buffer[i] = 0;
180
+ count += 1;
181
+ }
182
+ return count;
183
+ }
184
+
131
185
  /** Output channel count for every render and export. */
132
186
  export const RENDER_CHANNELS = 2 as const;
133
187
  export const DEFAULT_SAMPLE_RATE = 22_050;
@@ -162,6 +216,8 @@ export const AVAILABLE_INSTRUMENTS = Object.freeze([
162
216
  // 0.6 instruments (src/audio/instruments.ts), one line per lane.
163
217
  "granular",
164
218
  "modal",
219
+ // 0.7 clips: a track of audio clips whose notes are guides.
220
+ "vocal",
165
221
  ] as const);
166
222
 
167
223
  /** Per-track effects understood by the renderer and the agent, in chain
@@ -288,6 +344,8 @@ type Stem = {
288
344
  /** Stem, pre-reverb pair and tail (counted in the cache budget). */
289
345
  bytes: number;
290
346
  used: number;
347
+ /** Non-finite samples zeroed while rendering it (0 or absent: none). */
348
+ nonFinite?: number;
291
349
  /** For a track with a reverb: its stem before the reverb, and the tail. */
292
350
  room?: Readonly<{
293
351
  /** `stemKey` of the track with its reverb mix and mix lane left out. */
@@ -306,6 +364,9 @@ type Stem = {
306
364
  * byte-identical to a cold render; mute and solo only pick which stems are
307
365
  * summed. Scratch buffers are reused across renders.
308
366
  */
367
+ /** A guide note's level against a plain sine note (about -12 dB). */
368
+ const GUIDE_LEVEL = 0.25;
369
+
309
370
  export class StemRenderer {
310
371
  private readonly maxCacheBytes: number;
311
372
  private readonly stems = new Map<string, Stem>();
@@ -316,6 +377,11 @@ export class StemRenderer {
316
377
  >();
317
378
  private cacheBytes = 0;
318
379
  private renders = 0;
380
+ /** Vocoder modulator taps by source key (0.7), kept one render. */
381
+ private readonly modSources = new Map<
382
+ string,
383
+ { tap: Float64Array; used: number }
384
+ >();
319
385
  private scratch: {
320
386
  dry: Float64Array;
321
387
  dryR: Float64Array;
@@ -450,6 +516,10 @@ export class StemRenderer {
450
516
  };
451
517
  this.renders += 1;
452
518
  const { dry, dryR, left, right, mixL, mixR } = this.scratchFor(samples);
519
+ const nonFinite: { samples: number; tracks: string[] } = {
520
+ samples: 0,
521
+ tracks: [],
522
+ };
453
523
  mixL.fill(0);
454
524
  mixR.fill(0);
455
525
  const tracks = new Map(score.tracks.map((track) => [track.id, track]));
@@ -460,6 +530,14 @@ export class StemRenderer {
460
530
  if (group) group.push(note);
461
531
  else groups.set(note.trackId, [note]);
462
532
  }
533
+ // Tracks with audio clips (0.7) render even without notes, after the
534
+ // tracks with notes, so songs without clips keep their sum order.
535
+ for (const track of score.tracks)
536
+ if (hasClips(track) && !groups.has(track.id)) groups.set(track.id, []);
537
+ // A built-in vocoder carrier following chords or a drone needs no notes.
538
+ for (const track of score.tracks)
539
+ if (!groups.has(track.id) && vocoderPlaysWithoutNotes(track))
540
+ groups.set(track.id, []);
463
541
  // Note expression and track performance (core/expression.ts): the
464
542
  // notes as played. A track with neither gets its notes back unchanged.
465
543
  const timing = performanceTimingFor(score);
@@ -508,13 +586,31 @@ export class StemRenderer {
508
586
  const engine = engineFor(track);
509
587
  // The song key always joins an engine's stem key: engines may tune to
510
588
  // it (sympathetic strings) without a song tuning.
511
- const engineDigests =
589
+ const clipDigest = track
590
+ ? clipsDigest(track, bank, options.guide === true)
591
+ : undefined;
592
+ const baseDigests =
512
593
  engine && track
513
594
  ? [
514
595
  ...(engine.assetDigests?.(track, bank, score) ?? []),
515
596
  `key:${score.key ?? ""}`,
597
+ ...(clipDigest === undefined ? [] : [`clips:${clipDigest}`]),
516
598
  ]
517
- : undefined;
599
+ : clipDigest === undefined
600
+ ? undefined
601
+ : [`clips:${clipDigest}`];
602
+ // A vocoder carrier (0.7) also keys on its modulator's tap.
603
+ const vocoderSource = track?.vocoder
604
+ ? vocoderSourceOf(track, groups, performed, context, bank)
605
+ : undefined;
606
+ const withVocoder = vocoderSource
607
+ ? [...(baseDigests ?? []), vocoderSource.key]
608
+ : baseDigests;
609
+ // 0.7 autotune: its targets (guide notes, chords, key, tuning) and the
610
+ // pitch engine version join the stem key; absent adds nothing.
611
+ const engineDigests = track?.autotune
612
+ ? [...(withVocoder ?? []), ...autotuneStemDigests(track, score)]
613
+ : withVocoder;
518
614
  // Wavetable hook: the oscillator factory for a wavetable track (its
519
615
  // table id joins the stem key), undefined for every other instrument.
520
616
  const wavetable = track ? wavetableHook(track, bank, context) : undefined;
@@ -570,74 +666,32 @@ export class StemRenderer {
570
666
  }
571
667
  : { left, right };
572
668
  dry.fill(0);
573
- const synthVoice = !engine && !sampler && usesSynthVoice(track);
574
- let stereo = engine
575
- ? engine.stereo(track!)
576
- : synthVoice && isStereoVoice(track);
577
- if (stereo) dryR.fill(0);
578
- if (engine && track) {
579
- engine.render(
669
+ const stereo = renderVoiceInto(
670
+ dry,
671
+ dryR,
672
+ played,
673
+ track,
674
+ context,
675
+ score,
676
+ bank,
677
+ engine,
678
+ sampler,
679
+ wavetable,
680
+ tuning,
681
+ options.guide === true,
682
+ );
683
+ // The vocoder stage (0.7): after the voice, before the chain.
684
+ if (track && vocoderSource)
685
+ applyVocoder(
580
686
  dry,
581
687
  stereo ? dryR : undefined,
582
- played,
688
+ this.vocoderTap(vocoderSource, context, bank),
583
689
  track,
584
- {
585
- ...context,
586
- ticksPerBeat: score.ticksPerBeat,
587
- ...(tuning ? { tuning } : {}),
588
- },
589
- bank,
690
+ context,
691
+ vocoderSource.gateDb,
590
692
  );
591
- } else if (sampler) {
592
- // A resampled stereo file plays as stereo (0.6.1).
593
- if (track)
594
- stereo = renderSamplerNotes(
595
- dry,
596
- played,
597
- track,
598
- context,
599
- bank,
600
- dryR,
601
- );
602
- } else if (synthVoice && track) {
603
- const gainAt = (tick: number) => trackGainAt(track, tick);
604
- const voice = {
605
- ...context,
606
- ticksPerBeat: score.ticksPerBeat,
607
- ...(wavetable ? { oscillatorFor: wavetable.oscillatorFor } : {}),
608
- ...(tuning ? { tuning } : {}),
609
- };
610
- const warp = context.warp;
611
- for (const note of played) {
612
- const { start, length } = noteSpan(note, context);
613
- // The voice asks for `startTick + elapsed / samplesPerTick`;
614
- // through a tempo map that sample's tick comes from the map.
615
- const noteGainAt = warp
616
- ? (tick: number) =>
617
- gainAt(
618
- warp.tick(
619
- start + (tick - note.startTick) * context.samplesPerTick,
620
- ),
621
- )
622
- : gainAt;
623
- renderSynthNote(
624
- dry,
625
- stereo ? dryR : undefined,
626
- note,
627
- track,
628
- start,
629
- length,
630
- voice,
631
- noteGainAt,
632
- );
633
- }
634
- } else {
635
- const drums = isDrumInstrument(track?.instrument);
636
- for (const note of played) {
637
- if (drums) renderDrumNote(dry, note, track, context);
638
- else renderToneNote(dry, note, track, context, tuning);
639
- }
640
- }
693
+ // A non-finite source sample would poison every feedback stage.
694
+ let bad = scrubNonFinite(dry) + (stereo ? scrubNonFinite(dryR) : 0);
641
695
  // Note-aware stages (bloom, swell) see the notes; others never do.
642
696
  const chain =
643
697
  track && needsEffectNotes(track)
@@ -664,6 +718,8 @@ export class StemRenderer {
664
718
  true,
665
719
  false,
666
720
  );
721
+ // The room's input: scrubbed so the saved pair is clean too.
722
+ bad += scrubNonFinite(target.left) + scrubNonFinite(target.right);
667
723
  const pre = {
668
724
  left: Float64Array.from(target.left),
669
725
  right: Float64Array.from(target.right),
@@ -681,6 +737,7 @@ export class StemRenderer {
681
737
  chain,
682
738
  busOrbit === undefined,
683
739
  );
740
+ bad += scrubNonFinite(target.left) + scrubNonFinite(target.right);
684
741
  stem = {
685
742
  key,
686
743
  left: target.left,
@@ -688,9 +745,14 @@ export class StemRenderer {
688
745
  bytes: target.left.byteLength * (saved ? 6 : 2),
689
746
  used: this.renders,
690
747
  ...(saved ? { room: saved } : {}),
748
+ ...(bad > 0 ? { nonFinite: bad } : {}),
691
749
  };
692
750
  if (caching) this.store(trackId, stem);
693
751
  }
752
+ if (stem.nonFinite) {
753
+ nonFinite.samples += stem.nonFinite;
754
+ nonFinite.tracks.push(trackId);
755
+ }
694
756
  if (track && busOrbit !== undefined) {
695
757
  let bus = buses.get(busOrbit);
696
758
  if (!bus)
@@ -749,6 +811,12 @@ export class StemRenderer {
749
811
  Math.round(RING_OUT_FADE_SECONDS * sampleRate),
750
812
  );
751
813
  if (samples > frames) foldTail(mixL, mixR, frames, samples);
814
+ // Bus returns: a last scrub before the master's own feedback stages.
815
+ const busBad = scrubNonFinite(mixL) + scrubNonFinite(mixR);
816
+ if (busBad > 0) {
817
+ nonFinite.samples += busBad;
818
+ nonFinite.tracks.push("bus");
819
+ }
752
820
  // The song master (src/audio/master.ts); none leaves the mix untouched.
753
821
  const mastered = applyMaster(
754
822
  mixL,
@@ -769,9 +837,40 @@ export class StemRenderer {
769
837
  frames,
770
838
  pcm,
771
839
  ...(mastered ? { master: mastered.report } : {}),
840
+ ...(nonFinite.samples > 0
841
+ ? {
842
+ nonFinite: Object.freeze({
843
+ samples: nonFinite.samples,
844
+ tracks: Object.freeze([...nonFinite.tracks]),
845
+ }),
846
+ }
847
+ : {}),
772
848
  });
773
849
  }
774
850
 
851
+ /**
852
+ * A vocoder modulator's mono tap over this render: the source's voice,
853
+ * through its mono chain for `tap: chain`, stereo summed (L+R)/2. Kept
854
+ * across renders under the stem cache budget, keyed by the source key.
855
+ */
856
+ private vocoderTap(
857
+ source: VocoderSource,
858
+ context: RenderContext,
859
+ bank: SampleBank,
860
+ ): Float64Array {
861
+ const known = this.modSources.get(source.key);
862
+ if (known && known.tap.length === context.samples) {
863
+ known.used = this.renders;
864
+ return known.tap;
865
+ }
866
+ const tap = renderVocoderTap(source, context, bank);
867
+ for (const [key, entry] of this.modSources)
868
+ if (entry.used < this.renders - 1) this.modSources.delete(key);
869
+ if (this.maxCacheBytes > 0)
870
+ this.modSources.set(source.key, { tap, used: this.renders });
871
+ return tap;
872
+ }
873
+
775
874
  private scratchFor(samples: number) {
776
875
  if (this.scratch.dry.length < samples) {
777
876
  this.scratch = {
@@ -994,6 +1093,8 @@ function stemKey(
994
1093
  ...(context.score.tuning || track?.tuning
995
1094
  ? [context.score.tuning ?? null, context.score.key]
996
1095
  : []),
1096
+ // Calibrated engines (0.7) render differently; absent keeps old keys.
1097
+ ...(context.score.calibration ? [`cal:${context.score.calibration}`] : []),
997
1098
  // Seeded noise follows the window origin of an arranged render.
998
1099
  ...(context.seedTick ? [`seed:${context.seedTick}`] : []),
999
1100
  ...(context.seedSeconds ? [`at:${context.seedSeconds}`] : []),
@@ -1014,6 +1115,279 @@ function samplerVoiceDigest(track: Track, bank: SampleBank): string[] {
1014
1115
  });
1015
1116
  }
1016
1117
 
1118
+ /** A vocoder carrier's modulator: its track, notes, key and gate. */
1119
+ /** FX_CHAIN index of pan: a `tap: chain` modulator runs the stages before. */
1120
+ const TAP_END = FX_CHAIN.indexOf("pan");
1121
+
1122
+ type VocoderSource = Readonly<{
1123
+ track: Track;
1124
+ played: readonly PerformedNote[];
1125
+ tap: "chain" | "dry";
1126
+ key: string;
1127
+ gateDb: number;
1128
+ }>;
1129
+
1130
+ /** True for a built-in carrier that sounds without notes (chords, drone). */
1131
+ function vocoderPlaysWithoutNotes(track: Track): boolean {
1132
+ if (!track.vocoder || track.instrument !== VOCODER_INSTRUMENT) return false;
1133
+ const follow = resolveVocoder(track.vocoder).follow;
1134
+ return follow === "chords" || follow === "drone";
1135
+ }
1136
+
1137
+ const assetGates = new WeakMap<DecodedSample, number>();
1138
+
1139
+ /**
1140
+ * The auto gate from a source's assets (its sampler files): the quietest
1141
+ * file's 10th-percentile 10 ms frame level + 6 dB, never the render span.
1142
+ * A source without assets (a synth) has no noise floor: the gate is off.
1143
+ */
1144
+ function sourceGateDb(source: Track, bank: SampleBank): number {
1145
+ let gate: number | undefined;
1146
+ for (const voice of Object.keys(source.sampler?.voices ?? {}).sort()) {
1147
+ const sample = bank.voices.get(sampleKey(source.id, voice));
1148
+ if (!sample) continue;
1149
+ let level = assetGates.get(sample);
1150
+ if (level === undefined) {
1151
+ level = autoGateDb(Float64Array.from(sample.mono), sample.sampleRate);
1152
+ assetGates.set(sample, level);
1153
+ }
1154
+ if (level > -120) gate = gate === undefined ? level : Math.min(gate, level);
1155
+ }
1156
+ return gate ?? -120;
1157
+ }
1158
+
1159
+ /** Resolves a carrier's modulator, or undefined (the stage is bypassed). */
1160
+ function vocoderSourceOf(
1161
+ track: Track,
1162
+ groups: ReadonlyMap<string, readonly Note[]>,
1163
+ performed: ReadonlyMap<string, readonly PerformedNote[]>,
1164
+ context: RenderContext,
1165
+ bank: SampleBank,
1166
+ ): VocoderSource | undefined {
1167
+ const ref = track.vocoder?.src;
1168
+ if (ref === undefined) return undefined;
1169
+ const source = resolveTrackRef(context.score, ref);
1170
+ if (!source || source.id === track.id) return undefined;
1171
+ const settings = resolveVocoder(track.vocoder);
1172
+ const notes = groups.get(source.id) ?? [];
1173
+ const engine = engineFor(source);
1174
+ // Post-tap fields never change the tap (mute, solo and volume included).
1175
+ const {
1176
+ vocoder: _vocoder,
1177
+ pan: _pan,
1178
+ volume: _volume,
1179
+ volumeAutomation: _volumeAutomation,
1180
+ panAutomation: _panAutomation,
1181
+ delay: _delay,
1182
+ delayFeedbackAutomation: _delayFeedback,
1183
+ delayMixAutomation: _delayMix,
1184
+ reverb: _reverb,
1185
+ ...kept
1186
+ } = source;
1187
+ // Only the stages the tap runs (before pan, and none for `tap: dry`):
1188
+ // a source's reverb, delay, double or other post-pan fx never re-render
1189
+ // its carriers. fxAutomation stays (rarely post-pan; a cheap re-render).
1190
+ const tapped = settings.tap === "chain" ? FX_CHAIN.slice(0, TAP_END) : [];
1191
+ const fx = kept.fx
1192
+ ? Object.fromEntries(
1193
+ Object.entries(kept.fx).filter(
1194
+ ([stage]) =>
1195
+ tapped.includes(stage as never) ||
1196
+ !FX_CHAIN.includes(stage as never),
1197
+ ),
1198
+ )
1199
+ : undefined;
1200
+ const stripped = {
1201
+ ...kept,
1202
+ fx: fx && Object.keys(fx).length > 0 ? fx : undefined,
1203
+ } as unknown as Track;
1204
+ const gateDb =
1205
+ settings.gate === "auto" ? sourceGateDb(source, bank) : settings.gate;
1206
+ // The modulator's clips (0.7) are heard; guide tones never are.
1207
+ const clipDigest = clipsDigest(source, bank, false);
1208
+ // A tuned modulator (0.7 autotune) keys on its targets too: its guide
1209
+ // track's notes, the chord timeline and the key change the tap.
1210
+ const clipKeys = [
1211
+ ...(clipDigest === undefined ? [] : [`clips:${clipDigest}`]),
1212
+ ...(source.autotune ? autotuneStemDigests(source, context.score) : []),
1213
+ ];
1214
+ const key = stemKey(
1215
+ stripped,
1216
+ notes,
1217
+ context,
1218
+ isSamplerInstrument(source.instrument) ? bank : undefined,
1219
+ wavetableHook(source, bank, context)?.id,
1220
+ undefined,
1221
+ engine
1222
+ ? [
1223
+ ...(engine.assetDigests?.(source, bank, context.score) ?? []),
1224
+ `key:${context.score.key ?? ""}`,
1225
+ ...clipKeys,
1226
+ ]
1227
+ : clipKeys.length > 0
1228
+ ? clipKeys
1229
+ : undefined,
1230
+ );
1231
+ return {
1232
+ track: source,
1233
+ played: performed.get(source.id) ?? [],
1234
+ tap: settings.tap,
1235
+ key: `vocsrc:${settings.tap}:${gateDb}:${key}`,
1236
+ gateDb,
1237
+ };
1238
+ }
1239
+
1240
+ /** Renders a modulator's mono tap (see `StemRenderer.vocoderTap`). */
1241
+ function renderVocoderTap(
1242
+ source: VocoderSource,
1243
+ context: RenderContext,
1244
+ bank: SampleBank,
1245
+ ): Float64Array {
1246
+ const { played } = source;
1247
+ // The tap is rendered at unity gain: the voice bakes track volume in
1248
+ // (trackGainAt), and volume is a post-tap field the cache key leaves out.
1249
+ const track: Track = {
1250
+ ...source.track,
1251
+ volume: 1,
1252
+ volumeAutomation: [],
1253
+ };
1254
+ const { score } = context;
1255
+ const dry = new Float64Array(context.samples);
1256
+ const dryR = new Float64Array(context.samples);
1257
+ const tuning = resolveTuning(score.tuning, track.tuning, score.key);
1258
+ const stereo = renderVoiceInto(
1259
+ dry,
1260
+ dryR,
1261
+ played,
1262
+ track,
1263
+ context,
1264
+ score,
1265
+ bank,
1266
+ engineFor(track),
1267
+ isSamplerInstrument(track.instrument),
1268
+ wavetableHook(track, bank, context),
1269
+ tuning,
1270
+ false,
1271
+ );
1272
+ if (source.tap === "chain") {
1273
+ const chain = needsEffectNotes(track)
1274
+ ? { ...context, notes: effectNotes(played, context, tuning) }
1275
+ : context;
1276
+ applyMonoChain(dry, track, chain);
1277
+ if (stereo) applyMonoChain(dryR, track, chain);
1278
+ }
1279
+ if (stereo)
1280
+ for (let i = 0; i < dry.length; i += 1) dry[i] = (dry[i]! + dryR[i]!) / 2;
1281
+ return dry;
1282
+ }
1283
+
1284
+ /**
1285
+ * Renders a track's voice (engine, sampler, synth or tone/drum notes) into
1286
+ * `dry` (and `dryR` when stereo) before any effect; returns whether it
1287
+ * wrote a right channel. Shared by the stem pass and the vocoder tap.
1288
+ */
1289
+ function renderVoiceInto(
1290
+ dry: Float64Array,
1291
+ dryR: Float64Array,
1292
+ played: readonly PerformedNote[],
1293
+ track: Track | undefined,
1294
+ context: RenderContext,
1295
+ score: TrackScore,
1296
+ bank: SampleBank,
1297
+ engine: InstrumentEngine | undefined,
1298
+ sampler: boolean,
1299
+ wavetable: ReturnType<typeof wavetableHook> | undefined,
1300
+ tuning: TuningTable | undefined,
1301
+ guide: boolean,
1302
+ ): boolean {
1303
+ const synthVoice = !engine && !sampler && usesSynthVoice(track);
1304
+ let stereo = engine
1305
+ ? engine.stereo(track!)
1306
+ : synthVoice && isStereoVoice(track);
1307
+ if (stereo) dryR.fill(0);
1308
+ if (engine && track) {
1309
+ engine.render(
1310
+ dry,
1311
+ stereo ? dryR : undefined,
1312
+ played,
1313
+ track,
1314
+ {
1315
+ ...context,
1316
+ ticksPerBeat: score.ticksPerBeat,
1317
+ ...(tuning ? { tuning } : {}),
1318
+ },
1319
+ bank,
1320
+ );
1321
+ } else if (sampler) {
1322
+ // A resampled stereo file plays as stereo (0.6.1).
1323
+ if (track)
1324
+ stereo = renderSamplerNotes(dry, played, track, context, bank, dryR);
1325
+ } else if (synthVoice && track) {
1326
+ const gainAt = (tick: number) => trackGainAt(track, tick);
1327
+ const voice = {
1328
+ ...context,
1329
+ ticksPerBeat: score.ticksPerBeat,
1330
+ ...(wavetable ? { oscillatorFor: wavetable.oscillatorFor } : {}),
1331
+ ...(tuning ? { tuning } : {}),
1332
+ };
1333
+ const warp = context.warp;
1334
+ for (const note of played) {
1335
+ const { start, length } = noteSpan(note, context);
1336
+ // The voice asks for `startTick + elapsed / samplesPerTick`;
1337
+ // through a tempo map that sample's tick comes from the map.
1338
+ const noteGainAt = warp
1339
+ ? (tick: number) =>
1340
+ gainAt(
1341
+ warp.tick(
1342
+ start + (tick - note.startTick) * context.samplesPerTick,
1343
+ ),
1344
+ )
1345
+ : gainAt;
1346
+ renderSynthNote(
1347
+ dry,
1348
+ stereo ? dryR : undefined,
1349
+ note,
1350
+ track,
1351
+ start,
1352
+ length,
1353
+ voice,
1354
+ noteGainAt,
1355
+ );
1356
+ }
1357
+ } else if (track?.clips && isGuideInstrument(track.instrument)) {
1358
+ // A `vocal` track's notes guide its clips: silent in a render,
1359
+ // a soft sine in the live loop with `guide` on. (Without clips,
1360
+ // an older project's `vocal` keeps its tone.)
1361
+ if (guide)
1362
+ for (const note of played)
1363
+ renderToneNote(
1364
+ dry,
1365
+ { ...note, velocity: note.velocity * GUIDE_LEVEL },
1366
+ { ...track, instrument: "sine" },
1367
+ context,
1368
+ tuning,
1369
+ );
1370
+ } else {
1371
+ const drums = isDrumInstrument(track?.instrument);
1372
+ // Calibration 1+ (0.7): a closed or pedal hat chokes a ringing open hat.
1373
+ const chokes =
1374
+ drums && (context.score.calibration ?? 0) >= 1
1375
+ ? hatChokes(played, context)
1376
+ : undefined;
1377
+ for (const note of played) {
1378
+ if (drums) renderDrumNote(dry, note, track, context, chokes?.get(note));
1379
+ else renderToneNote(dry, note, track, context, tuning);
1380
+ }
1381
+ }
1382
+ // Audio clips (0.7) sum into the dry buffer before the chain.
1383
+ if (track?.clips) {
1384
+ const gainAt = (tick: number) => trackGainAt(track, tick);
1385
+ renderClips(dry, track, context, bank, gainAt, score);
1386
+ if (stereo) renderClips(dryR, track, context, bank, gainAt, score);
1387
+ }
1388
+ return stereo;
1389
+ }
1390
+
1017
1391
  /**
1018
1392
  * Sample voices of a sampler track into its mono dry buffer. When a voice
1019
1393
  * plays a resampled stereo file (0.6.1), `target` takes the left channel,
@@ -1113,6 +1487,7 @@ function renderToneNote(
1113
1487
  const performance = (note as PerformedNote).performance;
1114
1488
  const cents = performance?.cents;
1115
1489
  let bent = 0;
1490
+ const wave = legacyWaveFor(instrument);
1116
1491
  for (let index = start; index < end; index += 1) {
1117
1492
  const elapsed = index - start;
1118
1493
  const remaining = end - index;
@@ -1139,7 +1514,7 @@ function renderToneNote(
1139
1514
  const t = elapsed / sampleRate;
1140
1515
  const held =
1141
1516
  damp && t > damp.from ? Math.exp(-(t - damp.from) / damp.tau) : 1;
1142
- target[index]! += legacyWave(instrument, phase) * envelope * held;
1517
+ target[index]! += wave(phase) * envelope * held;
1143
1518
  }
1144
1519
  }
1145
1520
 
@@ -1148,11 +1523,40 @@ function renderToneNote(
1148
1523
  * id and start tick, so a hit sounds identical on every render and in every
1149
1524
  * window regardless of note order.
1150
1525
  */
1526
+ /** Closed hat (42) and pedal hat (44) GM numbers that choke an open hat. */
1527
+ const CHOKING_HATS = new Set([42, 44]);
1528
+ /** Open hat fade once choked, in seconds. */
1529
+ const CHOKE_SECONDS = 0.008;
1530
+
1531
+ /**
1532
+ * For each open hat (46), the sample where the next closed or pedal hat
1533
+ * on the same track starts, as on a real hi-hat stand.
1534
+ */
1535
+ function hatChokes(
1536
+ notes: readonly Note[],
1537
+ context: RenderContext,
1538
+ ): Map<Note, number> {
1539
+ const closers = notes
1540
+ .filter((note) => CHOKING_HATS.has(note.pitch))
1541
+ .map((note) => noteSpan(note, context).start)
1542
+ .sort((a, b) => a - b);
1543
+ const chokes = new Map<Note, number>();
1544
+ if (closers.length === 0) return chokes;
1545
+ for (const note of notes) {
1546
+ if (note.pitch !== 46) continue;
1547
+ const start = noteSpan(note, context).start;
1548
+ const next = closers.find((closer) => closer > start);
1549
+ if (next !== undefined) chokes.set(note, next);
1550
+ }
1551
+ return chokes;
1552
+ }
1553
+
1151
1554
  function renderDrumNote(
1152
1555
  target: Float64Array,
1153
1556
  note: Note,
1154
1557
  track: Track | undefined,
1155
1558
  context: RenderContext,
1559
+ chokeAt?: number,
1156
1560
  ): void {
1157
1561
  const { sampleRate, samples, samplesPerTick } = context;
1158
1562
  const { start } = noteSpan(note, context);
@@ -1160,9 +1564,24 @@ function renderDrumNote(
1160
1564
  // one the voices below render exactly as they always have.
1161
1565
  const kit = synthKit(track?.kit);
1162
1566
  const kitState = kit ? newKitVoiceState() : undefined;
1567
+ // Calibration 1+ (0.7): pitched toms, GM cymbals and cowbell as metal
1568
+ // instead of a rim click, band-limited kit hats, and hat choke.
1569
+ const calibrated = (context.score.calibration ?? 0) >= 1;
1570
+ const metal = calibrated ? metalKindForPitch(note.pitch) : undefined;
1571
+ const metalState = metal ? newKitVoiceState() : undefined;
1572
+ const ratio = calibrated ? tomRatio(note.pitch) : 1;
1573
+ const chokeEnd =
1574
+ chokeAt === undefined
1575
+ ? Number.POSITIVE_INFINITY
1576
+ : chokeAt + Math.ceil(CHOKE_SECONDS * sampleRate);
1163
1577
  const end = Math.min(
1164
1578
  samples,
1165
- start + Math.ceil((kit?.seconds ?? MAX_DRUM_SECONDS) * sampleRate),
1579
+ chokeEnd,
1580
+ start +
1581
+ Math.ceil(
1582
+ (metal ? METAL_SECONDS : (kit?.seconds ?? MAX_DRUM_SECONDS)) *
1583
+ sampleRate,
1584
+ ),
1166
1585
  );
1167
1586
  const voice = drumVoiceForPitch(note.pitch);
1168
1587
  const random = seededRandom(
@@ -1179,7 +1598,10 @@ function renderDrumNote(
1179
1598
  const bright = noise - previousNoise;
1180
1599
  previousNoise = noise;
1181
1600
  let sample = 0;
1182
- if (kit && kitState)
1601
+ if (metal && metalState) {
1602
+ sample = metalSample(metal, t, bright, metalState, sampleRate);
1603
+ if (kit) sample *= kit.gain;
1604
+ } else if (kit && kitState)
1183
1605
  sample = kitDrumSample(
1184
1606
  kit,
1185
1607
  voice,
@@ -1188,6 +1610,7 @@ function renderDrumNote(
1188
1610
  bright,
1189
1611
  kitState,
1190
1612
  sampleRate,
1613
+ calibrated ? { tomRatio: ratio } : undefined,
1191
1614
  );
1192
1615
  else if (voice === "kick") {
1193
1616
  const frequency = 45 + 105 * Math.exp(-t * 28);
@@ -1196,7 +1619,7 @@ function renderDrumNote(
1196
1619
  Math.sin(2 * Math.PI * phase) * Math.exp(-t * 7.5) +
1197
1620
  noise * 0.12 * Math.exp(-t * 300);
1198
1621
  } else if (voice === "tom") {
1199
- const frequency = 105 + 95 * Math.exp(-t * 18);
1622
+ const frequency = (105 + 95 * Math.exp(-t * 18)) * ratio;
1200
1623
  phase += frequency / sampleRate;
1201
1624
  sample = Math.sin(2 * Math.PI * phase) * Math.exp(-t * 9);
1202
1625
  } else if (voice === "snare") {
@@ -1223,10 +1646,10 @@ function renderDrumNote(
1223
1646
  1,
1224
1647
  (index - start) / Math.max(1, sampleRate * 0.001),
1225
1648
  );
1226
- const release = Math.min(
1227
- 1,
1228
- (end - index) / Math.max(1, sampleRate * 0.005),
1229
- );
1649
+ const release =
1650
+ chokeAt !== undefined && index >= chokeAt
1651
+ ? Math.max(0, chokeEnd - index) / Math.max(1, chokeEnd - chokeAt)
1652
+ : Math.min(1, (end - index) / Math.max(1, sampleRate * 0.005));
1230
1653
  target[index]! +=
1231
1654
  sample *
1232
1655
  attack *