@libraz/libsonare 1.7.2 → 1.8.1

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 (303) hide show
  1. package/NOTICE +178 -0
  2. package/README.md +26 -203
  3. package/dist/_chain_config.d.ts +14 -0
  4. package/dist/_chain_config.d.ts.map +1 -0
  5. package/dist/_effects_common.d.ts +7 -0
  6. package/dist/_effects_common.d.ts.map +1 -0
  7. package/dist/_feature_validation.d.ts +8 -0
  8. package/dist/_feature_validation.d.ts.map +1 -0
  9. package/dist/_fft_options.d.ts +24 -0
  10. package/dist/_fft_options.d.ts.map +1 -0
  11. package/dist/align_take.d.ts +50 -0
  12. package/dist/align_take.d.ts.map +1 -0
  13. package/dist/analysis.d.ts +29 -5840
  14. package/dist/analysis.d.ts.map +1 -0
  15. package/dist/analysis.js +874 -722
  16. package/dist/analysis.js.map +1 -1
  17. package/dist/analysis_helpers.d.ts +9 -0
  18. package/dist/analysis_helpers.d.ts.map +1 -0
  19. package/dist/audio.d.ts +163 -0
  20. package/dist/audio.d.ts.map +1 -0
  21. package/dist/clip_page_streamer.d.ts +133 -0
  22. package/dist/clip_page_streamer.d.ts.map +1 -0
  23. package/dist/codes.d.ts +44 -0
  24. package/dist/codes.d.ts.map +1 -0
  25. package/dist/effects_mastering.d.ts +23 -0
  26. package/dist/effects_mastering.d.ts.map +1 -0
  27. package/dist/effects_note_ops.d.ts +502 -0
  28. package/dist/effects_note_ops.d.ts.map +1 -0
  29. package/dist/effects_percussive.d.ts +185 -0
  30. package/dist/effects_percussive.d.ts.map +1 -0
  31. package/dist/effects_separation.d.ts +65 -0
  32. package/dist/effects_separation.d.ts.map +1 -0
  33. package/dist/effects_spectral.d.ts +28 -0
  34. package/dist/effects_spectral.d.ts.map +1 -0
  35. package/dist/effects_timepitch.d.ts +134 -0
  36. package/dist/effects_timepitch.d.ts.map +1 -0
  37. package/dist/effects_voice_change.d.ts +53 -0
  38. package/dist/effects_voice_change.d.ts.map +1 -0
  39. package/dist/errors.d.ts +51 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/feature_core.d.ts +341 -0
  42. package/dist/feature_core.d.ts.map +1 -0
  43. package/dist/feature_decompose.d.ts +278 -0
  44. package/dist/feature_decompose.d.ts.map +1 -0
  45. package/dist/feature_inverse.d.ts +128 -0
  46. package/dist/feature_inverse.d.ts.map +1 -0
  47. package/dist/feature_loudness.d.ts +66 -0
  48. package/dist/feature_loudness.d.ts.map +1 -0
  49. package/dist/feature_music.d.ts +307 -0
  50. package/dist/feature_music.d.ts.map +1 -0
  51. package/dist/feature_pitch.d.ts +108 -0
  52. package/dist/feature_pitch.d.ts.map +1 -0
  53. package/dist/feature_resample.d.ts +16 -0
  54. package/dist/feature_resample.d.ts.map +1 -0
  55. package/dist/feature_spectral.d.ts +137 -0
  56. package/dist/feature_spectral.d.ts.map +1 -0
  57. package/dist/feature_spectrogram.d.ts +198 -0
  58. package/dist/feature_spectrogram.d.ts.map +1 -0
  59. package/dist/features.d.ts +10 -0
  60. package/dist/features.d.ts.map +1 -0
  61. package/dist/hrtf/default.shrf +0 -0
  62. package/dist/index.d.ts +74 -7431
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4173 -1621
  65. package/dist/index.js.map +1 -1
  66. package/dist/instrument_types.d.ts +517 -0
  67. package/dist/instrument_types.d.ts.map +1 -0
  68. package/dist/live_audio.d.ts +35 -0
  69. package/dist/live_audio.d.ts.map +1 -0
  70. package/dist/mastering_chain.d.ts +213 -0
  71. package/dist/mastering_chain.d.ts.map +1 -0
  72. package/dist/mastering_core.d.ts +483 -0
  73. package/dist/mastering_core.d.ts.map +1 -0
  74. package/dist/mastering_dynamics.d.ts +80 -0
  75. package/dist/mastering_dynamics.d.ts.map +1 -0
  76. package/dist/metering.d.ts +287 -0
  77. package/dist/metering.d.ts.map +1 -0
  78. package/dist/mixer.d.ts +464 -0
  79. package/dist/mixer.d.ts.map +1 -0
  80. package/dist/mixing_assistant.d.ts +62 -0
  81. package/dist/mixing_assistant.d.ts.map +1 -0
  82. package/dist/mixing_oneshot.d.ts +40 -0
  83. package/dist/mixing_oneshot.d.ts.map +1 -0
  84. package/dist/module_state.d.ts +15 -0
  85. package/dist/module_state.d.ts.map +1 -0
  86. package/dist/opfs_clip_pages.d.ts +28 -0
  87. package/dist/opfs_clip_pages.d.ts.map +1 -0
  88. package/dist/playback_renderer.d.ts +128 -0
  89. package/dist/playback_renderer.d.ts.map +1 -0
  90. package/dist/polyphony.d.ts +202 -0
  91. package/dist/polyphony.d.ts.map +1 -0
  92. package/dist/project.d.ts +8 -0
  93. package/dist/project.d.ts.map +1 -0
  94. package/dist/project_class.d.ts +562 -0
  95. package/dist/project_class.d.ts.map +1 -0
  96. package/dist/project_internal.d.ts +194 -0
  97. package/dist/project_internal.d.ts.map +1 -0
  98. package/dist/project_synth.d.ts +74 -0
  99. package/dist/project_synth.d.ts.map +1 -0
  100. package/dist/project_types.d.ts +654 -0
  101. package/dist/project_types.d.ts.map +1 -0
  102. package/dist/public_types.d.ts +185 -0
  103. package/dist/public_types.d.ts.map +1 -0
  104. package/dist/public_types_acoustic.d.ts +215 -0
  105. package/dist/public_types_acoustic.d.ts.map +1 -0
  106. package/dist/public_types_mastering.d.ts +528 -0
  107. package/dist/public_types_mastering.d.ts.map +1 -0
  108. package/dist/public_types_mixing.d.ts +436 -0
  109. package/dist/public_types_mixing.d.ts.map +1 -0
  110. package/dist/public_types_music.d.ts +619 -0
  111. package/dist/public_types_music.d.ts.map +1 -0
  112. package/dist/public_types_playback.d.ts +165 -0
  113. package/dist/public_types_playback.d.ts.map +1 -0
  114. package/dist/public_types_realtime.d.ts +174 -0
  115. package/dist/public_types_realtime.d.ts.map +1 -0
  116. package/dist/public_types_repair.d.ts +424 -0
  117. package/dist/public_types_repair.d.ts.map +1 -0
  118. package/dist/public_types_spectral.d.ts +697 -0
  119. package/dist/public_types_spectral.d.ts.map +1 -0
  120. package/dist/quick_analysis.d.ts +445 -0
  121. package/dist/quick_analysis.d.ts.map +1 -0
  122. package/dist/realtime_engine.d.ts +893 -0
  123. package/dist/realtime_engine.d.ts.map +1 -0
  124. package/dist/realtime_voice_changer.d.ts +158 -0
  125. package/dist/realtime_voice_changer.d.ts.map +1 -0
  126. package/dist/repair_dereverb.d.ts +187 -0
  127. package/dist/repair_dereverb.d.ts.map +1 -0
  128. package/dist/repair_impulsive.d.ts +186 -0
  129. package/dist/repair_impulsive.d.ts.map +1 -0
  130. package/dist/repair_noise.d.ts +239 -0
  131. package/dist/repair_noise.d.ts.map +1 -0
  132. package/dist/repair_trim.d.ts +123 -0
  133. package/dist/repair_trim.d.ts.map +1 -0
  134. package/dist/sample_bank.d.ts +84 -0
  135. package/dist/sample_bank.d.ts.map +1 -0
  136. package/dist/scale.d.ts +10 -0
  137. package/dist/scale.d.ts.map +1 -0
  138. package/dist/schemas/mixer-scene.schema.json +393 -0
  139. package/dist/schemas/playback-renderer-config.schema.json +392 -0
  140. package/dist/sonare-analysis.d.ts +8 -0
  141. package/dist/sonare-analysis.js +2 -2
  142. package/dist/sonare-analysis.wasm +0 -0
  143. package/dist/sonare.d.ts +3945 -0
  144. package/dist/sonare.js +2 -2
  145. package/dist/sonare.wasm +0 -0
  146. package/dist/stream_analyzer.d.ts +163 -0
  147. package/dist/stream_analyzer.d.ts.map +1 -0
  148. package/dist/stream_types.d.ts +214 -0
  149. package/dist/stream_types.d.ts.map +1 -0
  150. package/dist/streaming_mixing.d.ts +6 -0
  151. package/dist/streaming_mixing.d.ts.map +1 -0
  152. package/dist/streaming_processors.d.ts +340 -0
  153. package/dist/streaming_processors.d.ts.map +1 -0
  154. package/dist/transcribe.d.ts +77 -0
  155. package/dist/transcribe.d.ts.map +1 -0
  156. package/dist/validation.d.ts +140 -0
  157. package/dist/validation.d.ts.map +1 -0
  158. package/dist/web_midi.d.ts +77 -0
  159. package/dist/web_midi.d.ts.map +1 -0
  160. package/dist/worker.d.ts +5 -48
  161. package/dist/worker.d.ts.map +1 -0
  162. package/dist/worker.js +94 -41
  163. package/dist/worker.js.map +1 -1
  164. package/dist/worker_client.d.ts +96 -0
  165. package/dist/worker_client.d.ts.map +1 -0
  166. package/dist/worker_protocol.d.ts +43 -0
  167. package/dist/worker_protocol.d.ts.map +1 -0
  168. package/dist/worklet/audio_types.d.ts +21 -0
  169. package/dist/worklet/audio_types.d.ts.map +1 -0
  170. package/dist/worklet/engine-automation.d.ts +29 -0
  171. package/dist/worklet/engine-automation.d.ts.map +1 -0
  172. package/dist/worklet/engine-capture-facade.d.ts +35 -0
  173. package/dist/worklet/engine-capture-facade.d.ts.map +1 -0
  174. package/dist/worklet/engine-clips.d.ts +23 -0
  175. package/dist/worklet/engine-clips.d.ts.map +1 -0
  176. package/dist/worklet/engine-markers.d.ts +40 -0
  177. package/dist/worklet/engine-markers.d.ts.map +1 -0
  178. package/dist/worklet/engine-mixer-facade.d.ts +164 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +83 -0
  181. package/dist/worklet/engine-node.d.ts.map +1 -0
  182. package/dist/worklet/engine-offline.d.ts +81 -0
  183. package/dist/worklet/engine-offline.d.ts.map +1 -0
  184. package/dist/worklet/engine-options.d.ts +12 -0
  185. package/dist/worklet/engine-options.d.ts.map +1 -0
  186. package/dist/worklet/engine-parameter-facade.d.ts +106 -0
  187. package/dist/worklet/engine-parameter-facade.d.ts.map +1 -0
  188. package/dist/worklet/engine-processor.d.ts +72 -0
  189. package/dist/worklet/engine-processor.d.ts.map +1 -0
  190. package/dist/worklet/engine-register.d.ts +2 -0
  191. package/dist/worklet/engine-register.d.ts.map +1 -0
  192. package/dist/worklet/engine-strips.d.ts +75 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +39 -0
  195. package/dist/worklet/engine-sync.d.ts.map +1 -0
  196. package/dist/worklet/engine-tempo-facade.d.ts +48 -0
  197. package/dist/worklet/engine-tempo-facade.d.ts.map +1 -0
  198. package/dist/worklet/engine.d.ts +418 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +53 -0
  201. package/dist/worklet/guards.d.ts.map +1 -0
  202. package/dist/worklet/messages.d.ts +710 -0
  203. package/dist/worklet/messages.d.ts.map +1 -0
  204. package/dist/worklet/mixer-processor.d.ts +46 -0
  205. package/dist/worklet/mixer-processor.d.ts.map +1 -0
  206. package/dist/worklet/playback-processor.d.ts +62 -0
  207. package/dist/worklet/playback-processor.d.ts.map +1 -0
  208. package/dist/worklet/protocol.d.ts +331 -0
  209. package/dist/worklet/protocol.d.ts.map +1 -0
  210. package/dist/worklet/voice-changer-processor.d.ts +41 -0
  211. package/dist/worklet/voice-changer-processor.d.ts.map +1 -0
  212. package/dist/worklet.d.ts +16 -2515
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +3200 -541
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +47 -0
  218. package/src/_feature_validation.ts +34 -0
  219. package/src/_fft_options.ts +39 -0
  220. package/src/align_take.ts +64 -0
  221. package/src/analysis.ts +56 -3
  222. package/src/analysis_helpers.ts +7 -0
  223. package/src/audio.ts +106 -3
  224. package/src/codes.ts +39 -2
  225. package/src/effects_mastering.ts +101 -22
  226. package/src/effects_note_ops.ts +683 -0
  227. package/src/effects_percussive.ts +217 -0
  228. package/src/effects_separation.ts +150 -0
  229. package/src/effects_spectral.ts +60 -0
  230. package/src/effects_timepitch.ts +388 -0
  231. package/src/errors.ts +23 -1
  232. package/src/feature_core.ts +127 -2
  233. package/src/feature_decompose.ts +633 -0
  234. package/src/feature_inverse.ts +454 -0
  235. package/src/feature_loudness.ts +125 -0
  236. package/src/feature_music.ts +107 -14
  237. package/src/feature_pitch.ts +96 -1
  238. package/src/feature_spectral.ts +16 -611
  239. package/src/feature_spectrogram.ts +63 -450
  240. package/src/features.ts +36 -22
  241. package/src/index.ts +288 -30
  242. package/src/instrument_types.ts +645 -0
  243. package/src/live_audio.ts +27 -1
  244. package/src/mastering_chain.ts +184 -0
  245. package/src/mastering_core.ts +441 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +251 -22
  249. package/src/mixing_assistant.ts +138 -0
  250. package/src/mixing_oneshot.ts +10 -5
  251. package/src/module_state.ts +24 -2
  252. package/src/playback_renderer.ts +252 -0
  253. package/src/polyphony.ts +279 -0
  254. package/src/project.ts +61 -24
  255. package/src/project_class.ts +450 -27
  256. package/src/project_internal.ts +149 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +271 -271
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +275 -73
  262. package/src/public_types_mixing.ts +363 -1
  263. package/src/public_types_music.ts +312 -2
  264. package/src/public_types_playback.ts +196 -0
  265. package/src/public_types_realtime.ts +39 -7
  266. package/src/public_types_repair.ts +446 -0
  267. package/src/public_types_spectral.ts +491 -5
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +773 -34
  270. package/src/realtime_voice_changer.ts +55 -1
  271. package/src/repair_dereverb.ts +299 -0
  272. package/src/repair_impulsive.ts +395 -0
  273. package/src/repair_noise.ts +425 -0
  274. package/src/repair_trim.ts +226 -0
  275. package/src/sample_bank.ts +113 -0
  276. package/src/sonare.js.d.ts +1158 -30
  277. package/src/stream_analyzer.ts +36 -4
  278. package/src/stream_types.ts +37 -0
  279. package/src/streaming_mixing.ts +1 -1
  280. package/src/streaming_processors.ts +202 -10
  281. package/src/transcribe.ts +89 -0
  282. package/src/validation.ts +285 -11
  283. package/src/web_midi.ts +1 -6
  284. package/src/worker.ts +18 -2
  285. package/src/worklet/audio_types.ts +37 -0
  286. package/src/worklet/engine-mixer-facade.ts +800 -32
  287. package/src/worklet/engine-node.ts +99 -29
  288. package/src/worklet/engine-offline.ts +14 -8
  289. package/src/worklet/engine-parameter-facade.ts +21 -0
  290. package/src/worklet/engine-processor.ts +332 -93
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +275 -9
  293. package/src/worklet/engine-sync.ts +20 -7
  294. package/src/worklet/engine.ts +394 -48
  295. package/src/worklet/guards.ts +195 -44
  296. package/src/worklet/messages.ts +229 -2
  297. package/src/worklet/mixer-processor.ts +117 -48
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +82 -11
  300. package/src/worklet/voice-changer-processor.ts +17 -11
  301. package/src/worklet.ts +17 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
@@ -0,0 +1,683 @@
1
+ /**
2
+ * Note-level editing: segmenting audio and a pitch track into editable notes,
3
+ * rendering an edited set back, and the split/merge operations over it.
4
+ */
5
+
6
+ import { assertPitchTrackLengths, toVoicedFloat32 } from './_effects_common';
7
+ import { getSonareModule } from './module_state';
8
+ import type {
9
+ NoteExtractorOptions,
10
+ NoteMoveOptions,
11
+ NoteObject,
12
+ NoteObjectInput,
13
+ NoteSetEntry,
14
+ NoteStretchOptions,
15
+ NoteTarget,
16
+ NoteTargetAssignResult,
17
+ NoteTargetUnmatchedPolicy,
18
+ PitchDecompositionResult,
19
+ VoicedFlags,
20
+ } from './public_types';
21
+ import type { ValidateOptions } from './validation';
22
+ import { assertFiniteScalar, assertSampleRate, assertSamples } from './validation';
23
+
24
+ function requireModule() {
25
+ return getSonareModule();
26
+ }
27
+
28
+ // Shared input checks of the entry points that take audio plus a whole F0 track,
29
+ // returning the voicing flags in the Float32Array form the embind layer reads. A
30
+ // companion array of the wrong length is a RangeError here because the native
31
+ // side can only report it as a flat InvalidParameter.
32
+ function assertNoteTrack(
33
+ fnName: string,
34
+ request: ExtractNotesRequest | NoteSetRequest,
35
+ ): Float32Array | undefined {
36
+ assertSamples(fnName, request.samples, request.validate !== false);
37
+ assertSampleRate(fnName, request.sampleRate);
38
+ const voicedF32 = request.voiced == null ? undefined : toVoicedFloat32(fnName, request.voiced);
39
+ assertPitchTrackLengths(fnName, request.f0Hz, request.voiced, request.voicedProb);
40
+ return voicedF32;
41
+ }
42
+
43
+ export interface NoteStretchRequest extends NoteStretchOptions, ValidateOptions {
44
+ samples: Float32Array;
45
+ sampleRate?: number;
46
+ }
47
+
48
+ export interface NoteMoveRequest extends NoteMoveOptions, ValidateOptions {
49
+ samples: Float32Array;
50
+ sampleRate?: number;
51
+ }
52
+
53
+ /** Canonical request form for {@link extractNotes}. */
54
+ export interface ExtractNotesRequest extends NoteExtractorOptions, ValidateOptions {
55
+ samples: Float32Array;
56
+ /**
57
+ * Sample rate in Hz. Required: `minNoteMs` and the per-frame RMS windows are
58
+ * converted to samples with this rate, so a wrong/omitted value silently
59
+ * segments differently.
60
+ */
61
+ sampleRate: number;
62
+ /**
63
+ * Per-frame F0 in Hz. A frame carrying no pitch is spelled as zero, a
64
+ * negative value or a non-finite one, and all three read the same: that
65
+ * frame contributes no measurement. A {@link pitchPyin} track can be passed
66
+ * straight through — its `fillNa` is a choice about the contour you want,
67
+ * not a requirement of this call.
68
+ */
69
+ f0Hz: Float32Array;
70
+ /** F0 frames per second. */
71
+ frameRate: number;
72
+ /** Per-frame voiced flags (truthy = voiced); takes precedence over `voicedProb` when supplied. Omit or pass `null` to use the probability array. */
73
+ voiced?: VoicedFlags | null;
74
+ /** Per-frame voicing probability in `[0, 1]`; read only when `voiced` is omitted or `null`. */
75
+ voicedProb?: Float32Array | null;
76
+ }
77
+
78
+ /** Canonical request form for {@link renderNotes}. */
79
+ export interface RenderNotesRequest extends ValidateOptions {
80
+ samples: Float32Array;
81
+ /**
82
+ * Sample rate in Hz. Required: `fadeMs` is converted to samples with this
83
+ * rate, so a wrong/omitted value changes the cross-fade length.
84
+ */
85
+ sampleRate: number;
86
+ /** The notes to render, with their edits. Source spans must not overlap. */
87
+ notes: readonly NoteObjectInput[];
88
+ /**
89
+ * Equal-power cross-fade at each edited note's edges. Default 5 ms; a hard cut
90
+ * is deliberately not selectable, because the seam it leaves is a click.
91
+ */
92
+ fadeMs?: number;
93
+ /**
94
+ * The F0 track the notes were extracted from. Required only by
95
+ * `vibratoDepthChange` and `driftChange`, which act on the note's own pitch
96
+ * curve, or when `voiced` is supplied; every other edit ignores it. The curve is not carried on a
97
+ * {@link NoteObject} for the same reason it is not returned by
98
+ * {@link extractNotes} — it is this array sliced by
99
+ * `[frameStart, frameEnd)`, which the caller already holds.
100
+ * Positive finite values are candidates for pitch. Zero, negative and
101
+ * non-finite values carry no measurement.
102
+ */
103
+ f0Hz?: Float32Array;
104
+ /** Per-frame voiced flags for `f0Hz`; false suppresses a candidate and true cannot make an unusable F0 valid. */
105
+ voiced?: VoicedFlags | null;
106
+ /** F0 frames per second. Required when `f0Hz` is given. */
107
+ frameRate?: number;
108
+ /**
109
+ * Boundary between the drift and the vibrato that `vibratoDepthChange` and
110
+ * `driftChange` act on, in Hz. Default 3 Hz.
111
+ *
112
+ * Pass whatever {@link decomposeNotePitch} was called with. A host that draws
113
+ * the vibrato at one cutoff and edits it at another edits a curve it never
114
+ * showed anyone.
115
+ */
116
+ vibratoCutoffHz?: number;
117
+ }
118
+
119
+ /** Canonical request form for {@link decomposeNotePitch}. */
120
+ export interface DecomposeNotePitchRequest {
121
+ /**
122
+ * The note's slice of the F0 track — `f0Hz.subarray(frameStart, frameEnd)`.
123
+ * Positive finite values carry pitch; other values carry no measurement.
124
+ */
125
+ f0Hz: Float32Array;
126
+ /** Per-frame voiced flags matching `f0Hz`; false suppresses a candidate and true cannot make an unusable F0 valid. */
127
+ voiced?: VoicedFlags | null;
128
+ /** F0 frames per second. */
129
+ frameRate: number;
130
+ /**
131
+ * The note's `medianHz`. A note with no pitch is spelled 0, so a negative
132
+ * value is rejected rather than read as a second way of saying that.
133
+ */
134
+ medianHz: number;
135
+ /** Where drift ends and vibrato begins, in Hz. Default 3 Hz. */
136
+ vibratoCutoffHz?: number;
137
+ }
138
+
139
+ /** The audio, track and note set the two note-set reshaping calls share. */
140
+ export interface NoteSetRequest extends NoteExtractorOptions, ValidateOptions {
141
+ /**
142
+ * The audio the set was extracted from. Every note is re-measured against it,
143
+ * so a different buffer re-derives the whole set against something else.
144
+ */
145
+ samples: Float32Array;
146
+ sampleRate: number;
147
+ /**
148
+ * Per-frame F0 in Hz. A frame carrying no pitch is spelled as zero, a
149
+ * negative value or a non-finite one, and all three read the same: that
150
+ * frame contributes no measurement. A {@link pitchPyin} track can be passed
151
+ * straight through — its `fillNa` is a choice about the contour you want,
152
+ * not a requirement of this call.
153
+ */
154
+ f0Hz: Float32Array;
155
+ /** F0 frames per second. */
156
+ frameRate: number;
157
+ /** Per-frame voiced flags (truthy = voiced); takes precedence over `voicedProb` when supplied. Omit or pass `null` to use the probability array. */
158
+ voiced?: VoicedFlags | null;
159
+ /** Per-frame voicing probability in `[0, 1]`; read only when `voiced` is omitted or `null`. */
160
+ voicedProb?: Float32Array | null;
161
+ /** The current note set. Each note's `[frameStart, frameEnd)` must be non-empty and inside the track. */
162
+ notes: readonly NoteSetEntry[];
163
+ }
164
+
165
+ /** Canonical request form for {@link splitNote}. */
166
+ export interface SplitNoteRequest extends NoteSetRequest {
167
+ /** Index of the note to split. */
168
+ index: number;
169
+ /** Track frame to cut at, strictly inside that note's own span. */
170
+ frame: number;
171
+ }
172
+
173
+ /** Canonical request form for {@link mergeNotes}. */
174
+ export interface MergeNotesRequest extends NoteSetRequest {
175
+ /** Index of the first note to join; must be `< last`. */
176
+ first: number;
177
+ /** Index of the last note to join, inclusive. */
178
+ last: number;
179
+ }
180
+
181
+ /** Canonical request form for {@link noteTargetsFromSmf}. */
182
+ export interface NoteTargetsFromSmfRequest {
183
+ /** The Standard MIDI File, in memory. */
184
+ data: Uint8Array;
185
+ /**
186
+ * Which MIDI-bearing track to read, NOT an index into the file's own tracks: a
187
+ * track holding only meta events — a conductor track carrying the tempo map is
188
+ * the usual one — is not counted. A file whose first track is a conductor track
189
+ * therefore has its melody at 0, which is also the default.
190
+ */
191
+ trackIndex?: number;
192
+ }
193
+
194
+ /** Canonical request form for {@link assignNoteTargets}. */
195
+ export interface AssignNoteTargetsRequest {
196
+ /**
197
+ * The notes to assign targets to. Not modified; the result carries a new array.
198
+ * Only `onsetSample`, `offsetSample` and `medianHz` are read.
199
+ */
200
+ notes: readonly NoteObject[];
201
+ /** Converts each note's sample span to seconds, so the targets line up. Must be > 0. */
202
+ sampleRate: number;
203
+ /** The reference melody. An empty array assigns nothing and applies the policy. */
204
+ targets: readonly NoteTarget[];
205
+ /** What to do with a note that has a pitch and no target. Default `'leave'`. */
206
+ unmatchedPolicy?: NoteTargetUnmatchedPolicy;
207
+ /**
208
+ * Fraction of the note that must overlap a target for it to count, in `[0, 1]`.
209
+ * Default 0.5. `0` is its own meaning — any overlap at all counts — not a
210
+ * request for the default.
211
+ */
212
+ minOverlapRatio?: number;
213
+ /**
214
+ * Where the assigned shift saturates, in semitones. Default 12. `0` is its own
215
+ * meaning, as above: every correction saturates to nothing and the take is left
216
+ * as recorded. Must not be negative.
217
+ */
218
+ maxCorrectionSemitones?: number;
219
+ }
220
+
221
+ /**
222
+ * Time-stretch a note region between two sample offsets without changing pitch.
223
+ *
224
+ * @param samples - Audio samples (mono, float32)
225
+ * @param sampleRate - Sample rate in Hz
226
+ * @param onsetSample - Note onset position in samples
227
+ * @param offsetSample - Note offset position in samples
228
+ * @param stretchRatio - Stretch ratio (0.5 = half duration, 2.0 = double duration)
229
+ * @returns Audio with the note region stretched
230
+ */
231
+ export function noteStretch(request: NoteStretchRequest): Float32Array;
232
+ export function noteStretch(
233
+ samples: Float32Array,
234
+ sampleRate?: number,
235
+ options?: NoteStretchOptions & ValidateOptions,
236
+ ): Float32Array;
237
+ export function noteStretch(
238
+ samples: Float32Array | NoteStretchRequest,
239
+ sampleRate = 22050,
240
+ options: NoteStretchOptions & ValidateOptions = {},
241
+ ): Float32Array {
242
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
243
+ assertSamples('noteStretch', request.samples, request.validate !== false);
244
+ return requireModule().noteStretch(
245
+ request.samples,
246
+ request.sampleRate ?? 22050,
247
+ request.onsetSample ?? 0,
248
+ request.offsetSample ?? request.samples.length,
249
+ request.stretchRatio ?? 1.0,
250
+ );
251
+ }
252
+
253
+ /** Move a note region to a new onset without changing its duration. */
254
+ export function noteMove(request: NoteMoveRequest): Float32Array;
255
+ export function noteMove(
256
+ samples: Float32Array,
257
+ sampleRate?: number,
258
+ options?: NoteMoveOptions & ValidateOptions,
259
+ ): Float32Array;
260
+ export function noteMove(
261
+ samples: Float32Array | NoteMoveRequest,
262
+ sampleRate = 22050,
263
+ options: NoteMoveOptions & ValidateOptions = {},
264
+ ): Float32Array {
265
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
266
+ assertSamples('noteMove', request.samples, request.validate !== false);
267
+ return requireModule().noteMove(
268
+ request.samples,
269
+ request.sampleRate ?? 22050,
270
+ request.onsetSample ?? 0,
271
+ request.offsetSample ?? request.samples.length,
272
+ request.targetOnsetSample ?? 0,
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Extract editable note objects from audio and a caller-supplied F0 track.
278
+ *
279
+ * The track is segmented into monophonic notes; each note carries its span in
280
+ * source samples, its span in the track's own frames, its median pitch, two
281
+ * measured quality figures, its per-frame amplitude (RMS) curve, and an identity
282
+ * {@link NoteEdit}. Edit the notes and hand them to {@link renderNotes} to apply
283
+ * the result — the source audio is never mutated, and a set whose edits are all
284
+ * identity renders back to the input bit for bit.
285
+ *
286
+ * The per-note F0 curve is deliberately not returned: it is the caller's own
287
+ * `f0Hz` sliced by `[frameStart, frameEnd)`. The amplitude curve is measured
288
+ * here, so it is, one entry per F0 frame over that note's span.
289
+ *
290
+ * Voicing comes from `voiced` (truthy = voiced) when it is supplied. `voicedProb`
291
+ * is read only when `voiced` is absent or `null`, and then a frame counts as voiced at or above
292
+ * `voicedThreshold` (default 0.5). At least one of the two is required. Because
293
+ * `voicedProb` from pYIN rises with F0 for a fixed frame length, prefer passing
294
+ * a {@link PitchResult}'s `voicedFlag` through `voiced`.
295
+ *
296
+ * @param request - Audio, F0 track, frame cadence and segmenter options
297
+ * @returns One {@link NoteObject} per segmented note, in time order; an empty
298
+ * array when the track segments to nothing
299
+ * @throws RangeError when the selected voicing array differs from `f0Hz` in
300
+ * length (`voiced`, or `voicedProb` when `voiced` is omitted or `null`), or the
301
+ * samples/sample rate fail the shared input checks
302
+ * @throws SonareError (`InvalidParameter`) on an empty `f0Hz`, a non-positive
303
+ * `frameRate`, a `voicedProb` outside
304
+ * `[0, 1]`, or a negative option value
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * // pitchPyin's default leaves unvoiced frames NaN, which reads here as a
309
+ * // frame carrying no pitch, so fillNa is a choice rather than a requirement.
310
+ * const pitch = pitchPyin({ samples, sampleRate });
311
+ * const notes = extractNotes({
312
+ * samples,
313
+ * sampleRate,
314
+ * f0Hz: pitch.f0,
315
+ * voiced: pitch.voicedFlag,
316
+ * frameRate: sampleRate / 512,
317
+ * minNoteMs: 40,
318
+ * });
319
+ * // Lift the second note by a semitone and mute the third.
320
+ * notes[1].edit.pitchShiftSemitones = 1;
321
+ * notes[2].edit.muted = true;
322
+ * const edited = renderNotes({ samples, sampleRate, notes });
323
+ * ```
324
+ */
325
+ export function extractNotes(request: ExtractNotesRequest): NoteObject[] {
326
+ const voicedF32 = assertNoteTrack('extractNotes', request);
327
+ return requireModule().extractNotes(
328
+ request.samples,
329
+ request.sampleRate,
330
+ request.f0Hz,
331
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
332
+ voicedF32,
333
+ request.frameRate,
334
+ request,
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Render edited note objects back over their source audio.
340
+ *
341
+ * Only a note whose edit is non-identity is resynthesized; the source passes
342
+ * through everywhere else, so a set of untouched {@link extractNotes} output
343
+ * reproduces the input exactly. The output has the input's length: an edit that
344
+ * pushes a note past either end is truncated there.
345
+ *
346
+ * Per note the order is: pitch curve, time stretch, pitch shift, formant warp,
347
+ * amplitude envelope, then gain.
348
+ *
349
+ * Each note's `onsetSample`, `offsetSample` and `edit` are read, plus its
350
+ * `frameStart`, `frameEnd` and `medianHz` when the request carries an `f0Hz`
351
+ * track for a curve edit to act on; so an extracted note can be passed back
352
+ * as-is, or a note can be built by hand from those fields alone. Overlap is
353
+ * checked on the source spans only — where `timeOffsetSamples` lands a note is
354
+ * not, and a note lengthened past its own span writes into its neighbours'
355
+ * samples, so two moved or stretched notes may be written over each other.
356
+ *
357
+ * @param request - Source audio, the notes to render, the cross-fade length, and
358
+ * the F0 track a vibrato or drift edit reads
359
+ * @returns The rendered audio, the same length and sample rate as the input
360
+ * @throws TypeError when `notes` is not an array, `f0Hz` is not a
361
+ * `Float32Array`, or `voiced` is not a supported flag array
362
+ * @throws RangeError when `frameRate` is non-finite, `voiced` is supplied
363
+ * without `f0Hz`, when its length differs from `f0Hz`, or when the samples or
364
+ * sample rate fail shared checks
365
+ * @throws SonareError (`InvalidParameter`) on a note whose span is empty,
366
+ * reversed or missing, overlapping source spans, a non-finite or non-positive
367
+ * edit field, a negative or non-finite envelope value, a negative `fadeMs` or
368
+ * `vibratoCutoffHz`, a frame span outside `f0Hz`, or a `vibratoDepthChange` /
369
+ * `driftChange` on a note with no usable pitch curve to apply it to
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * const notes = extractNotes({ samples, sampleRate, f0Hz, voiced, frameRate });
374
+ *
375
+ * // Silence the third note and leave the rest untouched.
376
+ * const muted = notes.map((note, index) =>
377
+ * index === 2 ? { ...note, edit: { ...note.edit, muted: true } } : note,
378
+ * );
379
+ * const rendered = renderNotes({ samples, sampleRate, notes: muted, fadeMs: 10 });
380
+ *
381
+ * // Flatten the first note's vibrato, which needs the track it was measured on.
382
+ * const flattened = notes.map((note, index) =>
383
+ * index === 0 ? { ...note, edit: { ...note.edit, vibratoDepthChange: -1 } } : note,
384
+ * );
385
+ * const steady = renderNotes({
386
+ * samples,
387
+ * sampleRate,
388
+ * notes: flattened,
389
+ * f0Hz,
390
+ * frameRate: sampleRate / 512,
391
+ * });
392
+ * ```
393
+ */
394
+ export function renderNotes(request: RenderNotesRequest): Float32Array {
395
+ assertSampleRate('renderNotes', request.sampleRate);
396
+ if (!Array.isArray(request.notes)) {
397
+ throw new TypeError('renderNotes: notes must be an array');
398
+ }
399
+ const f0Hz = request.f0Hz;
400
+ if (f0Hz !== undefined && !(f0Hz instanceof Float32Array)) {
401
+ throw new TypeError('renderNotes: f0Hz must be a Float32Array');
402
+ }
403
+ if (f0Hz !== undefined) {
404
+ assertFiniteScalar('renderNotes', request.frameRate as number, 'frameRate');
405
+ }
406
+ const { voiced: publicVoiced, ...withoutVoiced } = request;
407
+ const voiced = publicVoiced == null ? undefined : toVoicedFloat32('renderNotes', publicVoiced);
408
+ if (publicVoiced != null && f0Hz === undefined) {
409
+ throw new RangeError('renderNotes: voiced requires f0Hz');
410
+ }
411
+ if (f0Hz !== undefined) {
412
+ assertPitchTrackLengths('renderNotes', f0Hz, publicVoiced);
413
+ }
414
+ assertSamples('renderNotes', request.samples, request.validate !== false);
415
+ const options = { ...withoutVoiced, voiced };
416
+ return requireModule().renderNotes(request.samples, request.sampleRate, request.notes, options);
417
+ }
418
+
419
+ /**
420
+ * Split one note's pitch curve into a centre, a slow drift and a vibrato.
421
+ *
422
+ * A performed note's pitch is one curve carrying three things at once: the note
423
+ * that was aimed at, a slow wander around it, and a periodic oscillation on top.
424
+ * Editing any of them on its own needs them separated first, and the only thing
425
+ * that decides where drift ends and vibrato begins is `vibratoCutoffHz`. Hand
426
+ * the same cutoff to {@link renderNotes}, or it edits a curve nobody was shown.
427
+ *
428
+ * Frames whose F0 is unusable carry no measurement, so the curve is held at the
429
+ * nearest usable neighbour across them. Both curves therefore have an entry
430
+ * everywhere; when `voiced` is supplied, a host marking held frames should
431
+ * retain both the `f0Hz` and `voiced` arrays because a positive F0 can be
432
+ * explicitly suppressed. A false flag suppresses that frame and a true flag
433
+ * cannot make an unusable F0 valid.
434
+ *
435
+ * A note with no usable pitch is reported as a zero `centreHz` and two empty
436
+ * curves rather than as an error — that is a measurement which came up empty,
437
+ * not a bad argument.
438
+ *
439
+ * @param request - The note's slice of the F0 track, its cadence, its centre, and
440
+ * the cutoff
441
+ * @returns The centre and the two curves, each one entry per frame of `f0Hz`
442
+ * @throws TypeError when `f0Hz` is not a `Float32Array` or `voiced` is not a
443
+ * supported flag array
444
+ * @throws RangeError when `frameRate` is not finite or `voiced` differs in
445
+ * length from `f0Hz`
446
+ * @throws SonareError (`InvalidParameter`) on an empty `f0Hz`, a non-positive
447
+ * `frameRate`, a negative `medianHz`, or a negative `vibratoCutoffHz`
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * const note = notes[0];
452
+ * const { centreHz, driftCents, vibratoCents } = decomposeNotePitch({
453
+ * f0Hz: f0Hz.subarray(note.frameStart, note.frameEnd),
454
+ * frameRate: sampleRate / 512,
455
+ * medianHz: note.medianHz,
456
+ * });
457
+ * ```
458
+ */
459
+ export function decomposeNotePitch(request: DecomposeNotePitchRequest): PitchDecompositionResult {
460
+ if (!(request.f0Hz instanceof Float32Array)) {
461
+ throw new TypeError('decomposeNotePitch: f0Hz must be a Float32Array');
462
+ }
463
+ assertFiniteScalar('decomposeNotePitch', request.frameRate, 'frameRate');
464
+ const voiced =
465
+ request.voiced == null ? undefined : toVoicedFloat32('decomposeNotePitch', request.voiced);
466
+ assertPitchTrackLengths('decomposeNotePitch', request.f0Hz, request.voiced);
467
+ return requireModule().decomposeNotePitch(
468
+ request.f0Hz,
469
+ voiced,
470
+ request.frameRate,
471
+ request.medianHz,
472
+ request.vibratoCutoffHz ?? 0,
473
+ );
474
+ }
475
+
476
+ /**
477
+ * Split one note of a set in two at a track frame.
478
+ *
479
+ * Both halves are re-derived from the audio and the track the way
480
+ * {@link extractNotes} derives its own, rather than by patching the fields of
481
+ * the note they replace. Both inherit the source note's edit, and the tail's
482
+ * `timeOffsetSamples` absorbs the duration change of the stretched head, so the
483
+ * halves occupy the destination timeline the unsplit note did; a note whose edit
484
+ * is the identity still renders bit for bit after being split. A half with no
485
+ * sample span at that boundary is omitted and the other keeps the edit
486
+ * unchanged. A one-entry envelope is a constant over the span, so both halves
487
+ * get that same entry; a longer one is resampled onto a grid that preserves the
488
+ * rendered gain at every integer source sample.
489
+ *
490
+ * Every note in the set, not just the two halves, has its spans, curves, medians
491
+ * and stability re-derived from `samples` and the track, because a
492
+ * {@link NoteSetEntry} carries no curves for this call to copy through. The
493
+ * frame bounds are therefore what a note is identified by here, and the audio
494
+ * and track must be the ones the set was extracted from or the whole set is
495
+ * re-measured against something else. A set whose re-derived notes would
496
+ * include one with no sample span is rejected rather than shortened.
497
+ *
498
+ * @param request - The source, the current note set, and where to cut
499
+ * @returns The whole new note set, one note longer than the one handed in, or
500
+ * the same length when a half with no sample span is omitted
501
+ * @throws RangeError when the selected voicing array differs from `f0Hz` in
502
+ * length (`voiced`, or `voicedProb` when `voiced` is omitted or `null`), or the
503
+ * samples/sample rate fail the shared input checks
504
+ * @throws SonareError (`InvalidParameter`) on an out-of-range `index`, a `frame`
505
+ * that is not strictly inside that note's own span, a note whose frame span is
506
+ * empty or runs past the track, or the track arguments {@link extractNotes}
507
+ * itself rejects
508
+ *
509
+ * @example
510
+ * ```ts
511
+ * const notes = extractNotes({ samples, sampleRate, f0Hz, voiced, frameRate });
512
+ * const halves = splitNote({
513
+ * samples,
514
+ * sampleRate,
515
+ * f0Hz,
516
+ * voiced,
517
+ * frameRate,
518
+ * notes,
519
+ * index: 1,
520
+ * frame: Math.floor((notes[1].frameStart + notes[1].frameEnd) / 2),
521
+ * });
522
+ * ```
523
+ */
524
+ export function splitNote(request: SplitNoteRequest): NoteObject[] {
525
+ const voicedF32 = assertNoteTrack('splitNote', request);
526
+ return requireModule().splitNote(
527
+ request.samples,
528
+ request.sampleRate,
529
+ request.f0Hz,
530
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
531
+ voicedF32,
532
+ request.frameRate,
533
+ request.notes,
534
+ request.index,
535
+ request.frame,
536
+ request,
537
+ );
538
+ }
539
+
540
+ /**
541
+ * Join a run of notes into one.
542
+ *
543
+ * The result spans from `notes[first]`'s onset to `notes[last]`'s offset,
544
+ * including whatever the segmenter cut out between them, and its measured fields
545
+ * are derived over that whole span — the pitch and amplitude of an unvoiced gap
546
+ * live in the track and the audio, not in either neighbour.
547
+ *
548
+ * It takes `notes[first]`'s edit, envelope included. Notes carrying different
549
+ * edits have no single correct answer here, so the rule is stated rather than
550
+ * guessed at; a host that cares sets the edit afterwards. Every note in the set
551
+ * is re-derived from `samples` and the track, exactly as {@link splitNote}
552
+ * describes.
553
+ *
554
+ * @param request - The source, the current note set, and the run to join
555
+ * @returns The whole new note set, `last - first` notes shorter than the one
556
+ * handed in
557
+ * @throws RangeError when the selected voicing array differs from `f0Hz` in
558
+ * length (`voiced`, or `voicedProb` when `voiced` is omitted or `null`), or the
559
+ * samples/sample rate fail the shared input checks
560
+ * @throws SonareError (`InvalidParameter`) unless `first < last < notes.length`,
561
+ * on a note whose frame span is empty or runs past the track, or on the track
562
+ * arguments {@link extractNotes} itself rejects
563
+ *
564
+ * @example
565
+ * ```ts
566
+ * // Undo a split by rejoining the two halves it produced.
567
+ * const rejoined = mergeNotes({
568
+ * samples,
569
+ * sampleRate,
570
+ * f0Hz,
571
+ * voiced,
572
+ * frameRate,
573
+ * notes: halves,
574
+ * first: 1,
575
+ * last: 2,
576
+ * });
577
+ * ```
578
+ */
579
+ export function mergeNotes(request: MergeNotesRequest): NoteObject[] {
580
+ const voicedF32 = assertNoteTrack('mergeNotes', request);
581
+ return requireModule().mergeNotes(
582
+ request.samples,
583
+ request.sampleRate,
584
+ request.f0Hz,
585
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
586
+ voicedF32,
587
+ request.frameRate,
588
+ request.notes,
589
+ request.first,
590
+ request.last,
591
+ request,
592
+ );
593
+ }
594
+
595
+ /**
596
+ * Read one track of an in-memory Standard MIDI File as a reference melody.
597
+ *
598
+ * Each note-on is paired with the next note-off of the same note number on the
599
+ * same channel, and the pair becomes one {@link NoteTarget} at the note's own
600
+ * pitch. Times come from the file's tempo map, so a tempo change or a ramp inside
601
+ * it is followed rather than the initial tempo being scaled.
602
+ *
603
+ * A note-on the track never closes is dropped: it has no end, and the track's end
604
+ * is not a substitute for one — with events after it the note would span the whole
605
+ * remainder and, being the longest overlap everywhere, take the assignment away
606
+ * from every note that follows. Zero-length notes are skipped for the mirror
607
+ * reason: they overlap nothing, so they could never be assigned.
608
+ *
609
+ * @param request - The file's bytes and which MIDI-bearing track to read
610
+ * @returns One target per closed note, sorted by `startSec`; an empty array for a
611
+ * track with no closed note, which is a measurement that came up empty rather
612
+ * than an error
613
+ * @throws SonareError (`InvalidFormat`) when the bytes are not a readable SMF
614
+ * @throws SonareError (`InvalidParameter`) when `trackIndex` names no
615
+ * MIDI-bearing track
616
+ *
617
+ * @example
618
+ * ```ts
619
+ * // A project's own export writes the tempo map as a conductor track, which
620
+ * // carries no MIDI, so the melody is at index 0 rather than 1.
621
+ * const targets = noteTargetsFromSmf({ data: project.exportSmf() });
622
+ * ```
623
+ */
624
+ export function noteTargetsFromSmf(request: NoteTargetsFromSmfRequest): NoteTarget[] {
625
+ const module = requireModule();
626
+ if (typeof module.noteTargetsFromSmf !== 'function') {
627
+ throw new Error('libsonare was built without arrangement support');
628
+ }
629
+ return module.noteTargetsFromSmf(request.data, request.trackIndex ?? 0);
630
+ }
631
+
632
+ /**
633
+ * Write each note's `edit.pitchShiftSemitones` from the reference target it
634
+ * overlaps, which is what makes a take follow a written melody instead of a
635
+ * single stated interval.
636
+ *
637
+ * A note is matched to the target it overlaps longest, provided that overlap is
638
+ * at least `minOverlapRatio` of the note's own span; an exact tie goes to the
639
+ * target that starts first, so the answer does not depend on the order the
640
+ * targets arrived in. The shift is `targetMidi` minus the note's own `medianHz`
641
+ * as a MIDI number, saturated at `maxCorrectionSemitones` — a reference an octave
642
+ * out is a wrong reference, and a rejected call would tell the caller less than a
643
+ * bounded correction does.
644
+ *
645
+ * A note whose `medianHz` is not finite and positive is never assigned and never
646
+ * edited, whatever `unmatchedPolicy` says. Such a note has no measured pitch to
647
+ * correct from, so `'nearest'` would compute a shift from a pitch that does not
648
+ * exist; the policy governs notes that have a pitch and no target, which is a
649
+ * different thing from having no pitch.
650
+ *
651
+ * The notes handed in are not modified. Only `edit.pitchShiftSemitones` and
652
+ * `edit.muted` are written on the returned copies; every other field of a note —
653
+ * its spans, its metrics, its amplitude curve and its other edits — comes back
654
+ * exactly as it went in.
655
+ *
656
+ * @param request - The notes, their sample rate, the reference melody, and the
657
+ * three tunable knobs
658
+ * @returns The new note set and how many notes received a target
659
+ * @throws SonareError (`InvalidParameter`) on a non-positive `sampleRate`, an
660
+ * unknown `unmatchedPolicy`, a `minOverlapRatio` outside `[0, 1]`, a negative
661
+ * `maxCorrectionSemitones`, a note missing `onsetSample` / `offsetSample`, or a
662
+ * target whose `startSec`, `endSec` or `targetMidi` is not finite
663
+ *
664
+ * @example
665
+ * ```ts
666
+ * const notes = extractNotes({ samples, sampleRate, f0Hz, voiced, frameRate });
667
+ * const { notes: retuned, assignedCount } = assignNoteTargets({
668
+ * notes,
669
+ * sampleRate,
670
+ * targets: noteTargetsFromSmf({ data: smf }),
671
+ * unmatchedPolicy: 'mute',
672
+ * });
673
+ * const corrected = renderNotes({ samples, sampleRate, notes: retuned });
674
+ * ```
675
+ */
676
+ export function assignNoteTargets(request: AssignNoteTargetsRequest): NoteTargetAssignResult {
677
+ return requireModule().assignNoteTargets(
678
+ request.notes,
679
+ request.sampleRate,
680
+ request.targets,
681
+ request,
682
+ );
683
+ }