@hraness/dawg 0.6.1 → 0.8.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 (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  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 +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  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/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  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 +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
@@ -19,6 +19,7 @@
19
19
  * frames, so held notes and effect tails cross into the range as in the
20
20
  * song.
21
21
  */
22
+ import { withClipLengths } from "./clips.ts";
22
23
  import { createHash } from "node:crypto";
23
24
  import {
24
25
  SCORE_LIMITS,
@@ -216,7 +217,7 @@ export function renderResample(
216
217
  startTick,
217
218
  durationTicks,
218
219
  leadTicks,
219
- } = resampleScore(score, request);
220
+ } = resampleScore(withClipLengths(score, samples), request);
220
221
  // The song's export rate (48 kHz once it has a master), so the new
221
222
  // track nulls against the source when both play at that rate.
222
223
  const sampleRate = exportSampleRate(score);
@@ -48,6 +48,7 @@ import {
48
48
  type SampleWarp,
49
49
  } from "./warp.ts";
50
50
  import { fittedBuffer, shiftedBuffer, windowAt } from "./fit.ts";
51
+ import { autotuneVoice } from "./autotune.ts";
51
52
  import {
52
53
  performanceTimingFor,
53
54
  tunedTiming,
@@ -419,9 +420,36 @@ export function planSamplerVoices(
419
420
  : ref.loopEnd * sample.frames;
420
421
  const start = noteStartFrame(note, timing);
421
422
  const performance = (note as PerformedNote).performance;
423
+ // 0.7 autotune: the played buffer (after fit and shift) retuned at the
424
+ // pitch and place it sounds; same length, so the plan above holds.
425
+ // Reversed voices play untuned. Absent `autotune` skips this.
426
+ // Timed targets tune only the frames this voice can read.
427
+ const reads =
428
+ loop || ramp !== 0 || performance?.cents
429
+ ? {
430
+ from: Math.min(regionStart, loopFrom),
431
+ to: Math.max(regionEnd, loopTo),
432
+ }
433
+ : {
434
+ from: regionStart,
435
+ to: Math.min(regionEnd, regionStart + length * step + 2),
436
+ };
437
+ const tuned =
438
+ track.autotune && speed > 0
439
+ ? autotuneSample(
440
+ timing,
441
+ track,
442
+ sample,
443
+ sample === decoded,
444
+ start,
445
+ regionStart,
446
+ step,
447
+ reads,
448
+ )
449
+ : { sample, from: 0 };
422
450
  const voice: SamplerVoice = {
423
451
  voice: target.voice,
424
- sample,
452
+ sample: tuned.sample,
425
453
  startTick: note.startTick,
426
454
  start,
427
455
  end: start + length,
@@ -429,8 +457,8 @@ export function planSamplerVoices(
429
457
  step,
430
458
  reverse: speed < 0,
431
459
  loop,
432
- regionStart,
433
- regionEnd,
460
+ regionStart: regionStart - tuned.from,
461
+ regionEnd: regionEnd - tuned.from,
434
462
  gain: (ref.gain ?? 1) * Math.max(0, Math.min(1, note.velocity)),
435
463
  choke: ref.choke,
436
464
  loopStart: speed < 0 ? regionEnd - loopTo : loopFrom - regionStart,
@@ -440,7 +468,7 @@ export function planSamplerVoices(
440
468
  ...(ref.fadeInTime !== undefined
441
469
  ? { attack: Math.max(1, ref.fadeInTime * sampleRate) }
442
470
  : {}),
443
- ...(ref.from !== undefined && sample.left && sample.right
471
+ ...(ref.from !== undefined && tuned.sample.left && tuned.sample.right
444
472
  ? { stereo: true }
445
473
  : {}),
446
474
  ...(performance?.cents ? { cents: performance.cents } : {}),
@@ -535,6 +563,7 @@ function fitVoice(
535
563
  length,
536
564
  );
537
565
  if (!buffer) return null;
566
+ audioIds.set(buffer, key);
538
567
  const sample: DecodedSample = Object.freeze({
539
568
  sha256: decoded.sha256,
540
569
  sampleRate,
@@ -558,6 +587,57 @@ function fitVoice(
558
587
  });
559
588
  }
560
589
 
590
+ /**
591
+ * The voice's sample retuned by the track's autotune (0.7), and the source
592
+ * frame its first frame came from (timed targets tune only `reads`). A
593
+ * shifted or fitted buffer keeps the file's sha256 but not its audio, so
594
+ * it is named by its own shift or fit key (`audioIds`).
595
+ */
596
+ function autotuneSample(
597
+ timing: SamplerTiming,
598
+ track: Track,
599
+ sample: DecodedSample,
600
+ decoded: boolean,
601
+ start: number,
602
+ regionStart: number,
603
+ step: number,
604
+ reads: Readonly<{ from: number; to: number }>,
605
+ ): Readonly<{ sample: DecodedSample; from: number }> {
606
+ const id = decoded ? undefined : audioIds.get(sample.mono);
607
+ const tuned = autotuneVoice(
608
+ timing.score,
609
+ track,
610
+ {
611
+ ...(id !== undefined ? { id } : decoded ? { sha256: sample.sha256 } : {}),
612
+ sampleRate: sample.sampleRate,
613
+ mono: sample.mono,
614
+ // A shifted or fitted copy keeps the file's channels unshifted:
615
+ // only the decoded file tunes in stereo.
616
+ ...(decoded && sample.left && sample.right
617
+ ? { left: sample.left, right: sample.right }
618
+ : {}),
619
+ },
620
+ start / timing.sampleRate,
621
+ regionStart,
622
+ (step * timing.sampleRate) / sample.sampleRate,
623
+ reads,
624
+ );
625
+ if (tuned.mono === sample.mono) return { sample, from: 0 };
626
+ return {
627
+ sample: Object.freeze({
628
+ ...sample,
629
+ frames: tuned.mono.length,
630
+ mono: tuned.mono,
631
+ left: tuned.left,
632
+ right: tuned.right,
633
+ }),
634
+ from: tuned.from,
635
+ };
636
+ }
637
+
638
+ /** Shifted and fitted buffers by their own cache key (0.7 autotune ids). */
639
+ const audioIds = new WeakMap<Float32Array, string>();
640
+
561
641
  /** A SampleRef with a pitch or formant shift (0.6.1). */
562
642
  export function isShifted(ref: SampleRef): boolean {
563
643
  return (
@@ -597,6 +677,7 @@ function shiftVoice(
597
677
  ref.formant,
598
678
  );
599
679
  if (!buffer) return undefined;
680
+ audioIds.set(buffer, key);
600
681
  return Object.freeze({ ...sample, frames: buffer.length, mono: buffer });
601
682
  }
602
683
 
@@ -36,6 +36,7 @@ import {
36
36
  type TrackScore,
37
37
  } from "../../core/score.ts";
38
38
  import { GRANULAR_SOURCE_VOICE } from "../../core/granular.ts";
39
+ import { CLIP_VOICE_PREFIX, clipSampleRef } from "../../core/clips.ts";
39
40
  import { PackError, PackStore } from "./packs.ts";
40
41
  import {
41
42
  DEFAULT_MEMORY_CACHE_BYTES,
@@ -103,7 +104,8 @@ export function hasSamplerTracks(score: TrackScore): boolean {
103
104
  packWavetable(track) !== undefined ||
104
105
  localWavetable(track) !== undefined ||
105
106
  reverbIrSample(track) !== undefined ||
106
- granularSample(track) !== undefined,
107
+ granularSample(track) !== undefined ||
108
+ (track.clips?.length ?? 0) > 0,
107
109
  );
108
110
  }
109
111
 
@@ -128,6 +130,9 @@ function trackSampleRefs(track: Track): [string, SampleRef][] {
128
130
  }
129
131
  const granular = granularSample(track);
130
132
  if (granular) refs.push([GRANULAR_SOURCE_VOICE, granular]);
133
+ // Audio clips (0.7) load as `clip:<id>` voices, sha256-pinned alike.
134
+ for (const clip of track.clips ?? [])
135
+ refs.push([`${CLIP_VOICE_PREFIX}${clip.id}`, clipSampleRef(clip)]);
131
136
  return refs;
132
137
  }
133
138
 
@@ -623,6 +628,14 @@ export class SampleLibrary implements SampleSource {
623
628
  );
624
629
  }
625
630
 
631
+ /**
632
+ * Decodes one project file (0.7: an audio clip for pitch analysis), with
633
+ * the same path rules, limits and caches as a sampler voice.
634
+ */
635
+ public decodeFile(src: string): Promise<DecodedSample> {
636
+ return this.loadFile(src);
637
+ }
638
+
626
639
  public async load(score: TrackScore): Promise<SampleBank> {
627
640
  const voices = new Map<string, DecodedSample>();
628
641
  const problems: SampleProblem[] = [];
@@ -753,7 +766,12 @@ export class SampleLibrary implements SampleSource {
753
766
  );
754
767
  voices.set(sampleKey(track.id, voice), loaded);
755
768
  } catch (error) {
756
- report("error", describeFailure(voice, src, error, this.ffmpeg));
769
+ // A clip whose audio is missing renders as silence with a warning;
770
+ // a missing instrument voice still fails the render.
771
+ report(
772
+ voice.startsWith(CLIP_VOICE_PREFIX) ? "warning" : "error",
773
+ describeFailure(voice, src, error, this.ffmpeg),
774
+ );
757
775
  }
758
776
  }
759
777
  }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Test-only measurement helpers for the sing engine (formant.md section 9;
3
+ * never on the render path): a YIN f0 tracker (the pitch lane's
4
+ * src/audio/dsp/pitch.ts replaces it once merged), harmonic levels, the
5
+ * alias-to-harmonic power ratio and harmonic-peak formant fits. Ported from
6
+ * the reviewed prototype (proto/formant/analysis.ts, review.ts).
7
+ */
8
+ import { fftInPlace } from "../dsp/fft.ts";
9
+ import { hann } from "../dsp/stft.ts";
10
+
11
+ /**
12
+ * YIN (de Cheveigne and Kawahara 2002) with an FFT difference function,
13
+ * CMNDF threshold, parabolic refinement. Returns f0 per hop (0 unvoiced).
14
+ */
15
+ export function yin(
16
+ x: Float64Array,
17
+ sampleRate: number,
18
+ hop: number,
19
+ minHz = 60,
20
+ maxHz = 1000,
21
+ threshold = 0.12,
22
+ ): Float64Array {
23
+ const tauMax = Math.ceil(sampleRate / minHz);
24
+ const tauMin = Math.floor(sampleRate / maxHz);
25
+ const w = tauMax; // integration window
26
+ let n = 1;
27
+ while (n < w + tauMax) n <<= 1;
28
+ const frames = Math.floor(x.length / hop);
29
+ const result = new Float64Array(frames);
30
+ const re = new Float64Array(n);
31
+ const im = new Float64Array(n);
32
+ const re2 = new Float64Array(n);
33
+ const im2 = new Float64Array(n);
34
+ const d = new Float64Array(tauMax + 1);
35
+ for (let f = 0; f < frames; f += 1) {
36
+ const start = f * hop - Math.floor(w / 2);
37
+ // r(tau) = sum_{j<w} x[j] x[j+tau]: correlate window (len w) with segment (len w+tauMax)
38
+ re.fill(0);
39
+ im.fill(0);
40
+ re2.fill(0);
41
+ im2.fill(0);
42
+ let energy0 = 0;
43
+ for (let j = 0; j < w + tauMax; j += 1) {
44
+ const k = start + j;
45
+ const v = k >= 0 && k < x.length ? x[k]! : 0;
46
+ re2[j] = v;
47
+ if (j < w) {
48
+ re[j] = v;
49
+ energy0 += v * v;
50
+ }
51
+ }
52
+ if (energy0 < 1e-7) continue;
53
+ fftInPlace(re, im);
54
+ fftInPlace(re2, im2);
55
+ for (let k = 0; k < n; k += 1) {
56
+ // conj(A) * B
57
+ const a = re[k]!;
58
+ const b = -im[k]!;
59
+ const c = re2[k]!;
60
+ const e = im2[k]!;
61
+ re[k] = a * c - b * e;
62
+ im[k] = a * e + b * c;
63
+ }
64
+ fftInPlace(re, im, true);
65
+ // running energy of x[start+tau .. start+tau+w)
66
+ let et = energy0;
67
+ d[0] = 0;
68
+ let running = 0;
69
+ let found = -1;
70
+ const cm = new Float64Array(tauMax + 1);
71
+ cm[0] = 1;
72
+ for (let tau = 1; tau <= tauMax; tau += 1) {
73
+ const outIdx = start + tau - 1;
74
+ const inIdx = start + tau - 1 + w;
75
+ const vOut = outIdx >= 0 && outIdx < x.length ? x[outIdx]! : 0;
76
+ const vIn = inIdx >= 0 && inIdx < x.length ? x[inIdx]! : 0;
77
+ et += vIn * vIn - vOut * vOut;
78
+ d[tau] = energy0 + et - (2 * re[tau]!) / n;
79
+ running += d[tau]!;
80
+ cm[tau] = running > 0 ? (d[tau]! * tau) / running : 1;
81
+ }
82
+ for (let tau = tauMin; tau < tauMax; tau += 1) {
83
+ if (cm[tau]! < threshold) {
84
+ while (tau + 1 < tauMax && cm[tau + 1]! < cm[tau]!) tau += 1;
85
+ found = tau;
86
+ break;
87
+ }
88
+ }
89
+ if (found < 0) continue;
90
+ const a = cm[found - 1]!;
91
+ const b = cm[found]!;
92
+ const c = cm[found + 1]!;
93
+ const den = a - 2 * b + c;
94
+ const shift = den !== 0 ? (0.5 * (a - c)) / den : 0;
95
+ result[f] = sampleRate / (found + Math.max(-1, Math.min(1, shift)));
96
+ }
97
+ return result;
98
+ }
99
+
100
+ /** dB magnitude of harmonics 1..count at `f0` from a Hann frame centred at `centre`. */
101
+ export function harmonicLevels(
102
+ x: Float64Array,
103
+ sampleRate: number,
104
+ centre: number,
105
+ f0: number,
106
+ maxHz: number,
107
+ n = 4096,
108
+ ): number[] {
109
+ const re = new Float64Array(n);
110
+ const im = new Float64Array(n);
111
+ const w = hann(n);
112
+ for (let i = 0; i < n; i += 1) {
113
+ const j = centre - n / 2 + i;
114
+ re[i] = j >= 0 && j < x.length ? x[j]! * w[i]! : 0;
115
+ }
116
+ fftInPlace(re, im);
117
+ const levels: number[] = [];
118
+ for (let h = 1; h * f0 < maxHz; h += 1) {
119
+ const bin = (h * f0 * n) / sampleRate;
120
+ let best = 0;
121
+ for (let k = Math.floor(bin) - 2; k <= Math.ceil(bin) + 2; k += 1)
122
+ best = Math.max(best, Math.hypot(re[k]!, im[k]!));
123
+ levels.push(20 * Math.log10(best + 1e-12));
124
+ }
125
+ return levels;
126
+ }
127
+
128
+ export function median(v: number[]): number {
129
+ const s = [...v].sort((a, b) => a - b);
130
+ return s.length ? s[Math.floor(s.length / 2)]! : NaN;
131
+ }
132
+
133
+ /** Inharmonic over harmonic power between 50 Hz and 5 kHz, dB. */
134
+ export function aliasDb(x: Float64Array, sr: number, f0: number): number {
135
+ const n = 16384;
136
+ const re = new Float64Array(n);
137
+ const im = new Float64Array(n);
138
+ const off = x.length - n;
139
+ for (let i = 0; i < n; i += 1) {
140
+ const t = (2 * Math.PI * i) / (n - 1);
141
+ const w =
142
+ 0.35875 -
143
+ 0.48829 * Math.cos(t) +
144
+ 0.14128 * Math.cos(2 * t) -
145
+ 0.01168 * Math.cos(3 * t);
146
+ re[i] = x[off + i]! * w;
147
+ }
148
+ fftInPlace(re, im);
149
+ let harm = 0;
150
+ let alias = 0;
151
+ for (let k = Math.ceil((50 * n) / sr); k < (5000 * n) / sr; k += 1) {
152
+ const hz = (k * sr) / n;
153
+ const h = Math.round(hz / f0);
154
+ const p = re[k]! ** 2 + im[k]! ** 2;
155
+ if (h >= 1 && Math.abs(hz - h * f0) <= (5 * sr) / n) harm += p;
156
+ else alias += p;
157
+ }
158
+ return 10 * Math.log10(alias / harm);
159
+ }
160
+
161
+ /** Peaks of a harmonic-level curve, parabolically refined, in Hz. */
162
+ export function harmonicPeaks(levels: readonly number[], f0: number): number[] {
163
+ const peaks: number[] = [];
164
+ for (let h = 1; h < levels.length - 1; h += 1) {
165
+ const [a, b, c] = [levels[h - 1]!, levels[h]!, levels[h + 1]!];
166
+ if (b > a && b >= c) {
167
+ const d = a - 2 * b + c;
168
+ const off = d < 0 ? (0.5 * (a - c)) / d : 0;
169
+ peaks.push((h + 1 + off) * f0);
170
+ }
171
+ }
172
+ return peaks;
173
+ }
174
+
175
+ /** Spectral centroid (Hz) of an 8192-sample frame from `from`. */
176
+ export function centroidHz(x: Float64Array, sr: number, from: number): number {
177
+ const n = 8192;
178
+ const re = new Float64Array(n);
179
+ const im = new Float64Array(n);
180
+ const w = hann(n);
181
+ for (let i = 0; i < n; i += 1) re[i] = (x[from + i] ?? 0) * w[i]!;
182
+ fftInPlace(re, im);
183
+ let num = 0;
184
+ let den = 0;
185
+ for (let k = 1; k < n / 2; k += 1) {
186
+ const p = Math.hypot(re[k]!, im[k]!);
187
+ num += p * ((k * sr) / n);
188
+ den += p;
189
+ }
190
+ return num / den;
191
+ }
192
+
193
+ export const cents = (a: number, b: number): number => 1200 * Math.log2(a / b);