@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
@@ -44,22 +44,31 @@ export type TextAgentTurnOptions = Readonly<{
44
44
  export const TEXT_AGENT_SYSTEM_PROMPT = [
45
45
  "You are dawg, a loop composer inside a terminal music workstation.",
46
46
  "You edit the score by returning operations. Each op is validated and applied immediately, in order, as its own revision.",
47
- "Times are in beats from the loop start (0-based). Keep notes inside loopBeats unless you extend the loop first.",
47
+ 'Times are in beats from the loop start (0-based), while musicians count from 1: in 4/4 "beats 2 and 4" are beats 1 and 3 of each bar here, "the and of 1" is 0.5, and bar n starts at (n-1)×beats per bar. Keep notes inside loopBeats unless you extend the loop first.',
48
48
  "Prefer a few well-formed ops (one add_notes op per track part) over many tiny ones.",
49
- 'For drums, create a track with instrument "kit" and prefer set_rhythm (Euclidean rows: pulses over steps, rotate, repeats for rolls, accent, probability, swing) so the beat stays editable as parameters; for a genre groove start from apply_drum_pattern (list_drum_patterns) and pick a sound with set_drum_kit; use add_drums only for one-off fills. Drum pitches select voices, so do not use add_notes for beats.',
49
+ 'For drums, create a track with instrument "kit" and prefer set_rhythm (Euclidean rows: pulses over steps, rotate, repeats for rolls, accent, probability, swing) so the beat stays editable as parameters; for a style groove start from apply_drum_pattern (list_drum_patterns) and pick a sound with set_drum_kit. Drum pitches select voices, so do not use add_notes for beats.',
50
50
  "Song structure: list_sections shows named sections (bars count from 1); edit_section marks, adds, duplicates, moves, renames, deletes, mutes, varies (transpose/gain) and loops them; set_form sets the play order with repeats (intro verse chorus*2 outro); add_transition generates builds (riser, snare roll, filter sweep, uplifter), drops (cut plus impact) and drum fills at section boundaries.",
51
51
  CHORD_PROCESS,
52
52
  "Synth voices: set_synth shapes a synth track with Strudel params (ADSR, lpf/lpenv filter envelopes, fm/fmh, supersaw unison/detune, vib, penv, noise; z_* ZzFX sounds take slide/pitchJump/lfo/zcrush…) or a preset (pad lead pluck bass keys bell…).",
53
- "Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modelled ones.",
54
- "Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modelled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes). A track already stored as instrument piano keeps the legacy tone until you set it again.",
53
+ "Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modeled ones.",
54
+ "Bowed strings: set_string preset violin|viola|cello|contrabass|fiddle|erhu|kamancheh|violins|violas|cellos|contrabasses|pizz|trem plays a bowed physical model (bow params pressure speed attack vib vibmod vibdelay tremhz sord dyn). Overlapping or legato notes slur in one bow. The bare words cello, contrabass and strings keep their older voices; use set_string (or the words violin, viola, fiddle, erhu, violins, cellos) for the bowed ones. Sampler voices take vel [lo, hi] velocity layers and rr round-robin groups via set_sample.",
55
+ "Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modeled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes; sym 0.5 adds sympathetic bloom under the sustain pedal for pedalled Romantic and Impressionist playing). A track already stored as instrument piano keeps the legacy tone until you set it again.",
56
+ "Electric keys: set_instrument epiano (rhodes; suitcase stereo vibe, dyno bright 80s), wurli, clav (funkclav); set_keys tweaks bark bell tone vibe (epiano), trem (wurli), pickup neck|bridge|both|out and mute (clav). Soft and sostenuto pedals: set_piano_pedals on any modeled piano.",
57
+ 'Organs: set_instrument tonewheel (hammond b3 gospel jazzorgan), combo (farfisa vox) or pipe (church flutes cornet reeds celeste) loads a modeled organ; instrument organ stays the legacy sine. set_keys drawbars "888800008" (16\' to 1\'), registers "08880" (combo), stops ["principal8","octave4"] or plenum|flutes|cornet|reeds|strings|full (pipe), rotary slow|fast|stop, params perc 3rd percdecay percvol click scanner c3 drive chiff wind trem (each family takes only its own rows); automate keys-rotary and keys-drive lanes.',
55
58
  "Mallets and bells: set_instrument vibes (or marimba via modal, xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) plays the modal engine; set_modal changes mallet (yarn…brass), hardness, position, ring, damp, motor/motordepth (vibes tremolo), buzz, click. Plain instrument marimba stays the legacy tone.",
59
+ "Gamelan: set_modal preset saron demung slenthem gangsa gender bonang kenong kethuk kempul (gong too) with tuning slendro or pelog; for ombak (the shimmer) put the same part on two gangsa tracks and set_modal pair <other track> on one: it sounds ombak Hz (default 6) above its partner. Frame drums: daf bodhran tabla.",
60
+ "Winds and brass: set_instrument flute recorder whistle ney shakuhachi panpipe suling bansuri clarinet bassclarinet oboe bassoon sax altosax barisax trumpet harmon plunger trombone tuba horn plays the wind engine; set_wind changes breath, bright, mute (open straight cup harmon plunger), players (2..8 spreads a section over chord tones), growl, vib. Lines slur unless re-tongued (gaps or articulations). Plain instrument wind stays the legacy tone.",
61
+ "Voices (0.7): set_instrument aah ooh choir chorale (or set_sing preset oohchoir airy glass lament soprano basso drone) plays the built-in singing voice, a synthetic glottal source through formants (never a cloned voice); set_sing params voice (auto soprano alto tenor bass), vowel (a e i o u or a>o), voices 2..8 (an ensemble), breath, vib, jitter, bright; set_vowels puts vowels on notes. Throat singing: khoomei, sygyt (whistle) and kargyraa (sub growl) hold a drone (set_sing params drone D2) and each note picks a harmonic of it.",
62
+ "Vocoder: a vocoder is a carrier track whose vocoder.src is the modulator (usually the user's vocal). make one with create_track (instrument vocoder), then set_vocoder with src set to the vocal, and mute the vocal; talkbox suits funk and soul, choir and glass suit hyperpop, robot and smear suit electronic or ambient. Vocode only the user's own or licensed audio, never an identifiable third party's speech unless the user confirms they hold the rights.",
56
63
  "Sampler voices: set_sample sets Strudel sample controls per voice (begin end speed unit loop loopBegin loopEnd clip/legato fit loopAt accelerate squiz cut); splice = slices with fit.",
57
64
  "Fitting samples (0.6): fit_sample sets a voice's own bpm (or len in beats) so it follows the song's tempo map; fitmode beats for drums and speech (hits stay sharp), tones for pads and vocals (pitch kept), repitch (default) like tape.",
58
- "Effects (set_fx; set_effects and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff).",
65
+ "Effects (set_fx and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff).",
59
66
  "Guitar rig (set_rig): rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) on a guitar or bass track; stomp, head and cab tweak the pedal, amp (level-matched, gate in dB) and cabinet. Tracks named jangle punk funk ragged gtr-lead gtr-metal bachata start with a guitar voice and that rig. `amp` is Strudel gain; the guitar amp is head.",
67
+ "Shoegaze: rig shoegaze|glide|dreampop|swell|ebow (or a track named shoegaze); set_fx wobble (tremolo-arm bend: depth cents, rate, drift), bloom (feedback swell on the top note), swell (volume swell: time), double (second take: time, drift, width); reverb ir builtin:reverse|gate|spring. Strumming: set_guitar (tune dadgad, capo, hand, ring), strum_chords (chords or the track's own, strokes folk|pop|punk… or D-DU-UDU, speed ms), write_chords perform guitar.",
68
+ "Formant (0.7): set_formant shift -12..12 st moves a track's formants at constant pitch (negative deeper or bigger, positive smaller; 2-4 natural), mix 0..1, preset deep giant bright tiny, off; lanes formant-shift formant-mix. The vowel filter is set_fx vowel, whose to and morph 0..1 glide between two vowels (lane vowel-morph).",
60
69
  "Previewing: preview_sound renders a track, or candidate sound tool calls (changes: [{tool, args}]), without committing, returns loudness, brightness and a comparison, and plays it once in the user's window. Use it when choosing between sounds (tables, presets, effect amounts), say how it sounds, then commit with the normal tools.",
61
70
  "Wavetable synth: set_wavetable picks a table (built-ins offline, Strudel wt_ sets fetched once) and scans it by position wt.",
62
- `Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains.`,
71
+ `Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains. Grain play: sync "1/16" puts grain onsets on the tempo grid, quant scale|chord snaps grain pitches, mono true glides legato notes through one cloud, pedal true freezes the head while the sustain pedal is down; set_automation grain-<param> lanes (grain-pos, grain-scan, grain-pitch …) move a parameter over beats. Resample: the resample tool renders a track, orbit or the mix (a section or bars) to a pinned WAV and adds a sampler track, or with grain a granular track, that plays it; set_sample shift (semitones, length kept, formant 0 keeps a voice's character) and fadeInTime/fadeTime shape a sampler voice.`,
63
72
  "Mastering: set_master sets the song master and loudness target; measure_mix reports LUFS, true peak, balance and correlation. Master only when asked for loudness or a finished sound, and measure before and after.",
64
73
  'Reply with exactly one JSON object and nothing else, shaped {"ops":[{"tool":"<tool name>","args":{...}}],"say":"<one short sentence describing the musical change>","done":true}.',
65
74
  'Set "done":false only if you need to see the results of these ops before continuing; you will then get each op\'s result and can send more ops.',
@@ -74,6 +83,7 @@ export function renderToolCatalog(
74
83
  tools: readonly AgentTool[] = AGENT_TOOLS,
75
84
  ): string {
76
85
  return tools
86
+ .filter((tool) => !tool.hidden)
77
87
  .map(
78
88
  (tool) =>
79
89
  `- ${tool.name}: ${tool.description}\n args schema: ${JSON.stringify(tool.parameters)}`,
package/src/argv.ts ADDED
@@ -0,0 +1,38 @@
1
+ /** The small argv contract plain subcommands share (no dependencies). */
2
+
3
+ export type SimpleArgv =
4
+ | { kind: "help" }
5
+ | { kind: "run"; positionals: readonly string[] }
6
+ | { kind: "error"; problem: string };
7
+
8
+ /**
9
+ * The contract `dawg init`, `dawg check` and `dawg sessions` share: argv
10
+ * after the subcommand. `--help`/`-h` anywhere asks for usage; any other
11
+ * option, or more than `maxPositionals` words, is an error (exit 2) before
12
+ * the command does anything.
13
+ */
14
+ export function parseSimpleArgv(
15
+ rest: readonly string[],
16
+ maxPositionals: number,
17
+ ): SimpleArgv {
18
+ if (rest.some((arg) => arg === "--help" || arg === "-h"))
19
+ return { kind: "help" };
20
+ const positionals: string[] = [];
21
+ for (const arg of rest) {
22
+ if (arg === "--")
23
+ return { kind: "error", problem: "unexpected argument · --" };
24
+ if (arg.startsWith("-") && arg.length > 1)
25
+ return { kind: "error", problem: `unknown option · ${clip(arg)}` };
26
+ positionals.push(arg);
27
+ }
28
+ if (positionals.length > maxPositionals)
29
+ return {
30
+ kind: "error",
31
+ problem: `unexpected argument · ${clip(positionals[maxPositionals]!)}`,
32
+ };
33
+ return { kind: "run", positionals };
34
+ }
35
+
36
+ function clip(value: string): string {
37
+ return value.length > 40 ? `${value.slice(0, 40)}…` : value;
38
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The pitch-analysis cache (0.7). `pitchCurve` tracks a decoded sample once
3
+ * and keeps the canonical curve in a small in-memory LRU and on disk at
4
+ * `.dawg/analysis/<sha256>.<voice>.v<tracker version>.f0` (format DWF0).
5
+ *
6
+ * DWF0 holds exactly the canonical curve `trackPitch` returns (f0 Float32,
7
+ * prob and aperiodic u8), so a cache hit renders the same bytes as a cache
8
+ * miss. Layout, little endian:
9
+ *
10
+ * 0 "DWF0" magic
11
+ * 4 u16 DWF0 version, u16 tracker version
12
+ * 8 u32 frames
13
+ * 12 f64 hop seconds, 20 f64 t0 seconds
14
+ * 28 f32[frames] f0 Hz (0 unvoiced)
15
+ * … u8[frames] prob, u8[frames] aperiodic (1/255 steps)
16
+ * … 8 bytes: the sha256 prefix of every byte before it
17
+ *
18
+ * About 1.2 KB per audio second. Writes go to `<name>.<pid>.tmp` and are
19
+ * renamed into place; write errors are swallowed (the cache is an
20
+ * optimisation). Reads validate magic, versions, length and checksum, and
21
+ * any mismatch is a miss. Analysis stays inside the project.
22
+ */
23
+ import { createHash } from "node:crypto";
24
+ import { mkdir, readFile, rename, utimes, writeFile } from "node:fs/promises";
25
+ import { join } from "node:path";
26
+ import {
27
+ cacheUsage,
28
+ pruneLru,
29
+ type CacheUsage,
30
+ type PruneResult,
31
+ } from "./cache.ts";
32
+ import {
33
+ PITCH_TRACKER_VERSION,
34
+ trackPitchAsync,
35
+ type PitchCurve,
36
+ type PitchVoice,
37
+ } from "./dsp/pitch.ts";
38
+
39
+ export const DWF0_VERSION = 1;
40
+ const HEAD = 28;
41
+ const CHECK = 8;
42
+ const MAGIC = [0x44, 0x57, 0x46, 0x30] as const;
43
+
44
+ /** Disk budget for `.dawg/analysis` (curves are small; 64 MB is ~15 h). */
45
+ export const ANALYSIS_CACHE_BYTES = 64 * 1024 * 1024;
46
+ /** In-memory curves kept (least recently used go first). */
47
+ export const ANALYSIS_MEMORY_ENTRIES = 64;
48
+
49
+ /** Every file the analysis cache writes. */
50
+ export const ANALYSIS_FILE = /^[0-9a-f]{64}\.\w+\.v\d+\.f0$/;
51
+
52
+ function checksum(bytes: Uint8Array, length: number): Uint8Array {
53
+ return createHash("sha256").update(bytes.subarray(0, length)).digest();
54
+ }
55
+
56
+ /** The DWF0 bytes of a curve. */
57
+ export function encodeCurve(curve: PitchCurve): Uint8Array {
58
+ const n = curve.f0.length;
59
+ const size = HEAD + 6 * n + CHECK;
60
+ const bytes = new Uint8Array(size);
61
+ const view = new DataView(bytes.buffer);
62
+ bytes.set(MAGIC);
63
+ view.setUint16(4, DWF0_VERSION, true);
64
+ view.setUint16(6, PITCH_TRACKER_VERSION, true);
65
+ view.setUint32(8, n, true);
66
+ view.setFloat64(12, curve.hop, true);
67
+ view.setFloat64(20, curve.t0, true);
68
+ for (let f = 0; f < n; f += 1)
69
+ view.setFloat32(HEAD + 4 * f, curve.f0[f]!, true);
70
+ bytes.set(curve.prob, HEAD + 4 * n);
71
+ bytes.set(curve.aperiodic, HEAD + 5 * n);
72
+ bytes.set(checksum(bytes, size - CHECK).subarray(0, CHECK), size - CHECK);
73
+ return bytes;
74
+ }
75
+
76
+ /** The curve in DWF0 `bytes`, or undefined for anything not exactly valid. */
77
+ export function decodeCurve(bytes: Uint8Array): PitchCurve | undefined {
78
+ if (bytes.length < HEAD + CHECK) return undefined;
79
+ for (let i = 0; i < 4; i += 1) if (bytes[i] !== MAGIC[i]) return undefined;
80
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
81
+ if (view.getUint16(4, true) !== DWF0_VERSION) return undefined;
82
+ if (view.getUint16(6, true) !== PITCH_TRACKER_VERSION) return undefined;
83
+ const n = view.getUint32(8, true);
84
+ if (bytes.length !== HEAD + 6 * n + CHECK) return undefined;
85
+ const sum = checksum(bytes, bytes.length - CHECK);
86
+ for (let i = 0; i < CHECK; i += 1)
87
+ if (bytes[bytes.length - CHECK + i] !== sum[i]) return undefined;
88
+ const hop = view.getFloat64(12, true);
89
+ const t0 = view.getFloat64(20, true);
90
+ if (!(hop > 0) || !Number.isFinite(t0)) return undefined;
91
+ const f0 = new Float32Array(n);
92
+ for (let f = 0; f < n; f += 1) f0[f] = view.getFloat32(HEAD + 4 * f, true);
93
+ return {
94
+ t0,
95
+ hop,
96
+ f0,
97
+ prob: bytes.slice(HEAD + 4 * n, HEAD + 5 * n),
98
+ aperiodic: bytes.slice(HEAD + 5 * n, HEAD + 6 * n),
99
+ };
100
+ }
101
+
102
+ /** The cache file name of a sample's curve. */
103
+ export function analysisFileName(sha256: string, voice: PitchVoice): string {
104
+ return `${sha256}.${voice}.v${PITCH_TRACKER_VERSION}.f0`;
105
+ }
106
+
107
+ /** `<projectRoot>/.dawg/analysis`. */
108
+ export function analysisDir(projectRoot: string): string {
109
+ return join(projectRoot, ".dawg", "analysis");
110
+ }
111
+
112
+ /** What `pitchCurve` tracks: a decoded sample (mono mixdown) and its hash. */
113
+ export type AnalysisSource = Readonly<{
114
+ sha256: string;
115
+ sampleRate: number;
116
+ mono: ArrayLike<number>;
117
+ }>;
118
+
119
+ export type PitchCurveOptions = Readonly<{
120
+ voice?: PitchVoice;
121
+ /** Project root; without one only the memory cache is used. */
122
+ projectRoot?: string;
123
+ /** Overrides `<projectRoot>/.dawg/analysis`. */
124
+ dir?: string;
125
+ /** Disk budget (default ANALYSIS_CACHE_BYTES). */
126
+ maxBytes?: number;
127
+ /** Fraction of frames tracked so far, on a cache miss. */
128
+ progress?: (done: number) => void;
129
+ }>;
130
+
131
+ const memory = new Map<string, PitchCurve>();
132
+ const counters = { tracks: 0, diskHits: 0, memoryHits: 0 };
133
+
134
+ /** Tracking and cache-hit counts, for tests and the doctor. */
135
+ export function analysisCounters(): Readonly<typeof counters> {
136
+ return { ...counters };
137
+ }
138
+
139
+ /** Drops the in-memory curves (tests; the disk cache stays). */
140
+ export function clearAnalysisMemory(): void {
141
+ memory.clear();
142
+ }
143
+
144
+ function remember(key: string, curve: PitchCurve): void {
145
+ memory.delete(key);
146
+ memory.set(key, curve);
147
+ while (memory.size > ANALYSIS_MEMORY_ENTRIES)
148
+ memory.delete(memory.keys().next().value!);
149
+ }
150
+
151
+ /** The memory-cached curve of a sample, without tracking. */
152
+ export function cachedPitchCurve(
153
+ sha256: string,
154
+ voice: PitchVoice = "auto",
155
+ ): PitchCurve | undefined {
156
+ const key = analysisFileName(sha256, voice);
157
+ const hit = memory.get(key);
158
+ if (hit) remember(key, hit);
159
+ return hit;
160
+ }
161
+
162
+ /**
163
+ * The pitch curve of a decoded sample: memory, then disk, then tracked and
164
+ * written back. Every path returns identical curves.
165
+ */
166
+ export async function pitchCurve(
167
+ source: AnalysisSource,
168
+ options: PitchCurveOptions = {},
169
+ ): Promise<PitchCurve> {
170
+ const voice = options.voice ?? "auto";
171
+ const name = analysisFileName(source.sha256, voice);
172
+ const hit = memory.get(name);
173
+ if (hit) {
174
+ counters.memoryHits += 1;
175
+ remember(name, hit);
176
+ return hit;
177
+ }
178
+ const dir =
179
+ options.dir ??
180
+ (options.projectRoot ? analysisDir(options.projectRoot) : undefined);
181
+ if (dir) {
182
+ const path = join(dir, name);
183
+ const bytes = await readFile(path).catch(() => undefined);
184
+ const curve = bytes ? decodeCurve(new Uint8Array(bytes)) : undefined;
185
+ if (curve) {
186
+ counters.diskHits += 1;
187
+ const now = new Date();
188
+ await utimes(path, now, now).catch(() => undefined);
189
+ remember(name, curve);
190
+ return curve;
191
+ }
192
+ }
193
+ counters.tracks += 1;
194
+ const curve = await trackPitchAsync(
195
+ Float64Array.from(source.mono),
196
+ source.sampleRate,
197
+ { voice },
198
+ options.progress,
199
+ );
200
+ remember(name, curve);
201
+ if (dir) await writeCurve(dir, name, curve, options.maxBytes, source.sha256);
202
+ return curve;
203
+ }
204
+
205
+ async function writeCurve(
206
+ dir: string,
207
+ name: string,
208
+ curve: PitchCurve,
209
+ maxBytes = ANALYSIS_CACHE_BYTES,
210
+ keep: string,
211
+ ): Promise<void> {
212
+ try {
213
+ await mkdir(dir, { recursive: true });
214
+ const path = join(dir, name);
215
+ const temporary = `${path}.${process.pid}.tmp`;
216
+ await writeFile(temporary, encodeCurve(curve));
217
+ await rename(temporary, path);
218
+ await pruneAnalysisCache(dir, maxBytes, new Set([keep]));
219
+ } catch {
220
+ /* the cache is an optimisation; a read-only project still renders */
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Deletes least recently used curves until `dir` fits `maxBytes`, never
226
+ * the ones whose sha256 is in `keep` (the open project's samples).
227
+ */
228
+ export async function pruneAnalysisCache(
229
+ dir: string,
230
+ maxBytes = ANALYSIS_CACHE_BYTES,
231
+ keep: ReadonlySet<string> = new Set(),
232
+ ): Promise<PruneResult> {
233
+ return pruneLru(dir, ANALYSIS_FILE, maxBytes, (name) =>
234
+ keep.has(name.slice(0, 64)),
235
+ );
236
+ }
237
+
238
+ /** Files and bytes in the project's analysis cache, for `/pack cache`. */
239
+ export async function analysisCacheStatus(
240
+ projectRoot: string,
241
+ ): Promise<CacheUsage & { max: number }> {
242
+ return {
243
+ ...(await cacheUsage(analysisDir(projectRoot), ANALYSIS_FILE)),
244
+ max: ANALYSIS_CACHE_BYTES,
245
+ };
246
+ }
247
+
248
+ /** The `dawg media doctor` line for the project's analysis cache. */
249
+ export async function analysisDoctorLine(projectRoot: string): Promise<string> {
250
+ const s = await analysisCacheStatus(projectRoot);
251
+ const mb = (bytes: number): string => (bytes / (1024 * 1024)).toFixed(1);
252
+ return `· analysis cache .dawg/analysis · ${s.files} file${s.files === 1 ? "" : "s"} · ${mb(s.bytes)} MB of ${mb(s.max)} MB (least recently used pruned) · pitch tracker v${PITCH_TRACKER_VERSION}`;
253
+ }
@@ -8,6 +8,8 @@
8
8
  * that carries held notes and tails across the seam, so memory stays
9
9
  * bounded however long the form plays (up to MAX_SONG_SECONDS).
10
10
  */
11
+ import { withClipLengths } from "./clips.ts";
12
+ import { vocoderPrerollSeconds } from "../../core/vocoder.ts";
11
13
  import {
12
14
  SCORE_LIMITS,
13
15
  ScoreValidationError,
@@ -51,6 +53,7 @@ import {
51
53
  type RenderedAudio,
52
54
  type WavCue,
53
55
  } from "./wav.ts";
56
+ import { engineFor } from "./instruments.ts";
54
57
 
55
58
  /** What one default render pass holds before the renderer cuts it. */
56
59
  const DEFAULT_PASS_SECONDS = 30;
@@ -174,6 +177,8 @@ export function renderArranged(
174
177
  score: TrackScore,
175
178
  options: RenderOptions = {},
176
179
  ): RenderedAudio {
180
+ // Clips cut at sections, forms and windows need their files' real ends.
181
+ score = withClipLengths(score, options.samples);
177
182
  if (
178
183
  !(options.loop && loopedSection(score)) &&
179
184
  formSegments(score).length === 0 &&
@@ -330,8 +335,13 @@ function renderWindows(
330
335
  const slowBar = ((beatsPerBar * 60) / Math.min(...bpms)) * fermataStretch;
331
336
  const frameAt = (bar: number) =>
332
337
  Math.round(secondsAtTick(timeline, bar * ticks) * sampleRate);
333
- const ring = Math.max(1, Math.ceil(OVERHANG_SECONDS / fastBar));
334
- const ringTicks = Math.ceil((OVERHANG_SECONDS / fastBar) * ticks);
338
+ // A vocoder's envelopes need 5 releases (+0.1 s) to settle (0.7).
339
+ const overhang = Math.max(
340
+ OVERHANG_SECONDS,
341
+ ...score.tracks.map((track) => vocoderPrerollSeconds(track.vocoder)),
342
+ );
343
+ const ring = Math.max(1, Math.ceil(overhang / fastBar));
344
+ const ringTicks = Math.ceil((overhang / fastBar) * ticks);
335
345
  const budget = Math.max(1, Math.floor(WINDOW_BUDGET_SECONDS / slowBar));
336
346
  const reach = budget - 1;
337
347
  const most = Math.max(1, Math.floor(WINDOW_SECONDS / slowBar));
@@ -346,6 +356,10 @@ function renderWindows(
346
356
  /** Frames at the end that fade out under the next window. */
347
357
  fadeOut: number;
348
358
  }[] = [];
359
+ const nonFinite: { samples: number; tracks: string[] } = {
360
+ samples: 0,
361
+ tracks: [],
362
+ };
349
363
  for (let start = 0; start < totalBars;) {
350
364
  const startTick = start * ticks;
351
365
  // Every note up to the next window, in render order: tracks sum in the
@@ -393,11 +407,31 @@ function renderWindows(
393
407
  notes,
394
408
  master: null,
395
409
  });
410
+ // State an engine cannot rebuild in the pre-roll (organ rotors along a
411
+ // lane) is integrated over the song before the window.
412
+ const seedState: Record<string, string> = {};
413
+ if (
414
+ pre > 0 &&
415
+ window.tracks.some((track) => engineFor(track)?.windowSeed)
416
+ ) {
417
+ const history = frame(0, pre);
418
+ for (const track of history.tracks) {
419
+ const seed = engineFor(track)?.windowSeed?.(
420
+ history,
421
+ track,
422
+ frameAt(pre),
423
+ sampleRate,
424
+ );
425
+ if (seed !== undefined) seedState[track.id] = seed;
426
+ }
427
+ }
396
428
  const audio = renderer.render(window, {
397
429
  ...options,
398
430
  sampleRate,
399
431
  loop: false,
400
432
  seedTick: preTick,
433
+ ...(pre > 0 ? { seedSeconds: secondsAtTick(timeline, pre * ticks) } : {}),
434
+ ...(Object.keys(seedState).length > 0 ? { seedState } : {}),
401
435
  // Past `end` too, so voices that fade where the buffer stops (the
402
436
  // legacy tone voice) fade in frames this window drops.
403
437
  maxSeconds: last
@@ -408,6 +442,11 @@ function renderWindows(
408
442
  secondsAtTick(timeline, pre * ticks),
409
443
  ),
410
444
  });
445
+ if (audio.nonFinite) {
446
+ nonFinite.samples += audio.nonFinite.samples;
447
+ for (const id of audio.nonFinite.tracks)
448
+ if (!nonFinite.tracks.includes(id)) nonFinite.tracks.push(id);
449
+ }
411
450
  // A restarted note's phase differs from the previous window's, so the
412
451
  // seam crossfades over the previous window's last frames instead of
413
452
  // stepping; seams without a restart join sample for sample.
@@ -477,6 +516,15 @@ function renderWindows(
477
516
  master,
478
517
  options.loop === true,
479
518
  );
519
+ const report =
520
+ nonFinite.samples > 0
521
+ ? {
522
+ nonFinite: {
523
+ samples: nonFinite.samples,
524
+ tracks: [...nonFinite.tracks],
525
+ },
526
+ }
527
+ : {};
480
528
  if (mastered)
481
529
  return {
482
530
  sampleRate,
@@ -484,6 +532,13 @@ function renderWindows(
484
532
  frames: length,
485
533
  pcm: mastered.pcm,
486
534
  master: mastered.report,
535
+ ...report,
487
536
  };
488
- return { sampleRate, channels: RENDER_CHANNELS, frames: length, pcm };
537
+ return {
538
+ sampleRate,
539
+ channels: RENDER_CHANNELS,
540
+ frames: length,
541
+ pcm,
542
+ ...report,
543
+ };
489
544
  }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The built-in pitch engine for autotune (0.7): the pitch lane's tracker
3
+ * (src/audio/dsp/pitch.ts) and PSOLA (src/audio/dsp/psola.ts) behind the
4
+ * `PitchEngine` seam of src/audio/autotune.ts. Curves are memoised per
5
+ * audio (`id ?? sha256`, else the buffer itself) and voice range; a file
6
+ * whose curve the analysis cache already holds is not tracked again. With
7
+ * `defer` (live play) a missing curve is tracked in the background and the
8
+ * buffer plays untuned until it is ready. Deterministic: the same audio
9
+ * always gives the same curve and the same tuned bytes.
10
+ */
11
+ import { cachedPitchCurve } from "./analysis.ts";
12
+ import {
13
+ PITCH_TRACKER_VERSION,
14
+ trackPitch,
15
+ trackPitchAsync,
16
+ type PitchCurve,
17
+ } from "./dsp/pitch.ts";
18
+ import { psola } from "./dsp/psola.ts";
19
+ import type { PitchEngine, TunableBuffer } from "./autotune.ts";
20
+
21
+ /** Curves kept in memory by key; older ones are tracked again on demand. */
22
+ const CURVE_ENTRIES = 64;
23
+
24
+ const curves = new Map<string, PitchCurve>();
25
+ const anonymous = new WeakMap<Float32Array, Map<string, PitchCurve>>();
26
+ const pending = new Set<string>();
27
+
28
+ function keyOf(buffer: TunableBuffer): string | undefined {
29
+ return buffer.id ?? buffer.sha256;
30
+ }
31
+
32
+ function lookup(buffer: TunableBuffer, voice: string): PitchCurve | undefined {
33
+ const key = keyOf(buffer);
34
+ if (key === undefined) return anonymous.get(buffer.mono)?.get(voice);
35
+ const name = `${key}|${voice}`;
36
+ const hit = curves.get(name);
37
+ if (hit) {
38
+ curves.delete(name);
39
+ curves.set(name, hit);
40
+ return hit;
41
+ }
42
+ // The decoded file itself: the pitch lane's analysis cache may know it.
43
+ if (buffer.id === undefined && buffer.sha256 !== undefined) {
44
+ const cached = cachedPitchCurve(
45
+ buffer.sha256,
46
+ voice as Parameters<typeof cachedPitchCurve>[1],
47
+ );
48
+ if (cached) store(buffer, voice, cached);
49
+ return cached;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ function store(buffer: TunableBuffer, voice: string, curve: PitchCurve): void {
55
+ const key = keyOf(buffer);
56
+ if (key === undefined) {
57
+ const byVoice = anonymous.get(buffer.mono) ?? new Map<string, PitchCurve>();
58
+ byVoice.set(voice, curve);
59
+ anonymous.set(buffer.mono, byVoice);
60
+ return;
61
+ }
62
+ const name = `${key}|${voice}`;
63
+ curves.delete(name);
64
+ curves.set(name, curve);
65
+ while (curves.size > CURVE_ENTRIES)
66
+ curves.delete(curves.keys().next().value!);
67
+ }
68
+
69
+ /** Drops the memoised curves (tests). */
70
+ export function clearEngineCurves(): void {
71
+ curves.clear();
72
+ pending.clear();
73
+ }
74
+
75
+ /** The pitch lane's tracker and PSOLA as an autotune engine. */
76
+ export const builtinPitchEngine: PitchEngine = Object.freeze({
77
+ version: `yin${PITCH_TRACKER_VERSION}-psola1`,
78
+ curve(buffer, voice, options) {
79
+ const hit = lookup(buffer, voice);
80
+ if (hit) return hit;
81
+ const x = Float64Array.from(buffer.mono);
82
+ if (!options?.defer) {
83
+ const curve = trackPitch(x, buffer.sampleRate, { voice });
84
+ store(buffer, voice, curve);
85
+ return curve;
86
+ }
87
+ const name = `${keyOf(buffer) ?? "anon"}|${voice}`;
88
+ if (!pending.has(name)) {
89
+ pending.add(name);
90
+ void trackPitchAsync(x, buffer.sampleRate, { voice }).then(
91
+ (curve) => {
92
+ pending.delete(name);
93
+ store(buffer, voice, curve);
94
+ },
95
+ () => pending.delete(name),
96
+ );
97
+ }
98
+ return undefined;
99
+ },
100
+ psola: (x, sampleRate, curve) => psola(x, sampleRate, curve),
101
+ });