@libraz/libsonare 1.7.2 → 1.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 (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 +6 -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 +873 -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 +477 -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 +129 -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 +3758 -1362
  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 +457 -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 +652 -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 +510 -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 +164 -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 +850 -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 +3909 -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 +335 -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 +128 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +81 -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 +68 -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 +73 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +37 -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 +397 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +47 -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 +323 -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 +2763 -501
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +17 -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 +97 -22
  226. package/src/effects_note_ops.ts +635 -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 +377 -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 +282 -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 +346 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +212 -3
  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 +268 -270
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +254 -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 +195 -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 +487 -1
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +711 -28
  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 +1122 -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 +194 -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 +444 -10
  287. package/src/worklet/engine-node.ts +78 -26
  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 +272 -83
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +280 -9
  293. package/src/worklet/engine-sync.ts +14 -6
  294. package/src/worklet/engine.ts +365 -31
  295. package/src/worklet/guards.ts +140 -44
  296. package/src/worklet/messages.ts +227 -2
  297. package/src/worklet/mixer-processor.ts +109 -47
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +53 -3
  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
package/src/validation.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ErrorCode, SonareError } from './errors';
2
+
1
3
  /**
2
4
  * Per-call validation options accepted by guarded wrappers. Empty-buffer
3
5
  * checks are always performed; pass `{ validate: false }` to opt out of the
@@ -6,9 +8,11 @@
6
8
  * `{ validate: false }` only skips this JS-side pre-scan (which raises a
7
9
  * `RangeError` naming the exact offending index). It is NOT a way to push
8
10
  * non-finite samples into the core: the native layer always re-validates the
9
- * buffer (see `validate_offline_audio_input` in the C++ core), matching the C
10
- * ABI / Node / Python surfaces, so an NaN/Inf buffer still throws — just with a
11
- * generic native message instead of the indexed JS one.
11
+ * buffer — through `validate_offline_audio_input` for the offline-analysis
12
+ * entry points, and through the computation's own per-sample guard where a call
13
+ * takes no sample rate to validate against (the waveform bucket kernels) —
14
+ * matching the C ABI / Node / Python surfaces, so an NaN/Inf buffer still
15
+ * throws, just with a generic native message instead of the indexed JS one.
12
16
  */
13
17
  export interface ValidateOptions {
14
18
  validate?: boolean;
@@ -60,20 +64,68 @@ export function assertSamples(
60
64
  assertFiniteSamples(fnName, samples, validate, argName);
61
65
  }
62
66
 
67
+ /**
68
+ * `assertSamples` restricted to the span a windowed entry point actually reads.
69
+ *
70
+ * The emptiness check still covers the whole buffer, and the reported index is
71
+ * the absolute one, so the message keeps naming the sample the caller passed.
72
+ * What narrows is the scan: a windowed call refuses a non-finite sample inside
73
+ * its frame and is indifferent to one outside it, which is the contract the C
74
+ * ABI states and the cost model it promises -- per call the scan is bounded by
75
+ * the frame, not by the length of the buffer being polled.
76
+ */
77
+ export function assertSamplesInWindow(
78
+ fnName: string,
79
+ samples: ArrayLike<number>,
80
+ validate: boolean,
81
+ windowStart: number,
82
+ windowLength: number,
83
+ argName = 'samples',
84
+ ): void {
85
+ assertNonEmptySamples(fnName, samples, argName);
86
+ if (!validate) {
87
+ return;
88
+ }
89
+ // Floored and ceiled before use: a fractional offset is refused by the layer
90
+ // that owns it, but an index of 100.5 reads `undefined` out of the buffer and
91
+ // would be reported here as a non-finite sample that is not there.
92
+ const start = Math.min(Math.max(Math.floor(windowStart), 0), samples.length);
93
+ const stop = Math.min(start + Math.max(Math.ceil(windowLength), 0), samples.length);
94
+ for (let i = start; i < stop; i++) {
95
+ const v = samples[i] as number;
96
+ if (!Number.isFinite(v)) {
97
+ throw new RangeError(`${fnName}: ${argName} contains NaN or Inf at index ${i}`);
98
+ }
99
+ }
100
+ }
101
+
63
102
  export function assertFiniteScalar(fnName: string, value: number, argName: string): void {
64
103
  if (!Number.isFinite(value)) {
65
104
  throw new RangeError(`${fnName}: ${argName} must be a finite number`);
66
105
  }
67
106
  }
68
107
 
69
- export function assertSampleRate(fnName: string, sampleRate: number): void {
70
- if (
71
- !Number.isInteger(sampleRate) ||
72
- sampleRate < MIN_AUDIO_SAMPLE_RATE ||
73
- sampleRate > MAX_AUDIO_SAMPLE_RATE
74
- ) {
108
+ /**
109
+ * A NaN gamma is the automatic-bandwidth sentinel the variable-Q transform
110
+ * documents, alongside a negative value, so only an infinity is out of domain.
111
+ */
112
+ export function assertVqtGamma(fnName: string, gamma: number): void {
113
+ if (gamma === Number.POSITIVE_INFINITY || gamma === Number.NEGATIVE_INFINITY) {
114
+ throw new RangeError(`${fnName}: gamma must not be infinite`);
115
+ }
116
+ }
117
+
118
+ export function assertSampleRate(fnName: string, sampleRate: number, argName = 'sampleRate'): void {
119
+ // Two refusals, not one: 22050.7 sits inside the range, so reporting it as
120
+ // out of range names an argument that is not the one at fault. `argName` is
121
+ // the same point for a rate the caller spelled something else, such as a
122
+ // resampler's source and target.
123
+ if (!Number.isInteger(sampleRate)) {
124
+ throw new RangeError(`${fnName}: ${argName} must be an integer`);
125
+ }
126
+ if (sampleRate < MIN_AUDIO_SAMPLE_RATE || sampleRate > MAX_AUDIO_SAMPLE_RATE) {
75
127
  throw new RangeError(
76
- `${fnName}: sampleRate out of supported range [${MIN_AUDIO_SAMPLE_RATE}, ${MAX_AUDIO_SAMPLE_RATE}]`,
128
+ `${fnName}: ${argName} out of supported range [${MIN_AUDIO_SAMPLE_RATE}, ${MAX_AUDIO_SAMPLE_RATE}]`,
77
129
  );
78
130
  }
79
131
  }
@@ -84,18 +136,240 @@ export function validateAudioBuffer(samples: Float32Array, sampleRate: number):
84
136
  assertSampleRate('Audio.fromBuffer', sampleRate);
85
137
  }
86
138
 
139
+ /** Bounds of the native `int` every embind argument below is narrowed into. */
140
+ export const C_INT_MIN = -2147483648;
141
+ export const C_INT_MAX = 2147483647;
142
+
143
+ /**
144
+ * Reject an argument embind's declared-`int` narrowing would wrap.
145
+ *
146
+ * A positional embind parameter declared `int` WRAPS rather than saturates, so
147
+ * a kernel of `2 ** 32` arrives as 0 and `2 ** 32 + 1` as 1 — both values the
148
+ * native guards accept, so the call succeeds having separated on a setting the
149
+ * caller never asked for. (The options-object path narrows through
150
+ * `checkedIntFromVal`, which refuses the same inputs in the module; this is the
151
+ * positional path's equivalent.)
152
+ *
153
+ * Reported as the branded `SonareError` carrying `InvalidParameter` rather than
154
+ * a `RangeError`, because the class follows what the rejection stands in for: a
155
+ * `RangeError` is this surface refusing an argument on its own authority, while
156
+ * this one pre-empts a native refusal the caller would have received under that
157
+ * code had the narrowing not wrapped the value into the accepted domain first.
158
+ * The agreement with the Node and Python surfaces is asserted by the Node
159
+ * package's `tests/narrowing-code-parity.test.ts`, which drives one value
160
+ * through all three — weakening this check turns that red.
161
+ */
162
+ export function assertInt32(fnName: string, value: number, argName: string): void {
163
+ if (!Number.isInteger(value) || value < C_INT_MIN || value > C_INT_MAX) {
164
+ throw new SonareError(
165
+ ErrorCode.InvalidParameter,
166
+ 'InvalidParameter',
167
+ `${fnName}: ${argName} must be an integer within the signed 32-bit range`,
168
+ );
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Check both HPSS kernels before embind narrows them.
174
+ *
175
+ * Parity, positivity and the ceiling stay the core's to enforce, and it names
176
+ * the median filter that rejected the value. What cannot be deferred is the
177
+ * narrowing itself: a wrapped kernel arrives as a legal one and separates on it.
178
+ *
179
+ * These two arrive POSITIONALLY, so they never pass through the options-bag
180
+ * reader and inherit none of its checks. That is what separates this from
181
+ * {@link assertPercussiveSeparation}, which duplicates a reader check to improve
182
+ * a message; here there is no reader to duplicate.
183
+ */
184
+ export function assertHpssKernels(
185
+ fnName: string,
186
+ kernelHarmonic: number,
187
+ kernelPercussive: number,
188
+ ): void {
189
+ assertInt32(fnName, kernelHarmonic, 'kernelHarmonic');
190
+ assertInt32(fnName, kernelPercussive, 'kernelPercussive');
191
+ }
192
+
193
+ function assertIntegralField(fnName: string, value: number, argName: string): void {
194
+ if (!Number.isInteger(value)) {
195
+ throw new SonareError(
196
+ ErrorCode.InvalidParameter,
197
+ 'InvalidParameter',
198
+ `${fnName}: ${argName} must be an integer`,
199
+ );
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Percussive-event separation fields, as the request objects carry them.
205
+ */
206
+ export interface PercussiveSeparationFields {
207
+ nFft?: number;
208
+ hopLength?: number;
209
+ hpssKernelHarmonic?: number;
210
+ hpssKernelPercussive?: number;
211
+ }
212
+
213
+ /**
214
+ * Check the percussive-event separation's framing and kernels for integrality.
215
+ *
216
+ * All four fields reach the module through one options-bag reader, which now
217
+ * refuses a fractional value itself, so this is the diagnostic rather than the
218
+ * guarantee: it fires first and names the function, where the reader can only
219
+ * name the field. Both reject the same set, so they cannot disagree about an
220
+ * input — only about how the message reads. Do not narrow this to fields the
221
+ * reader misses; there are none, and a check scoped to a gap that no longer
222
+ * exists is how a stale justification outlives its divergence.
223
+ *
224
+ * The fields are iterated rather than named at each call site so a new one is
225
+ * visible here. Absence means the default, so an omitted field is not resolved.
226
+ */
227
+ export function assertPercussiveSeparation(
228
+ fnName: string,
229
+ options: PercussiveSeparationFields,
230
+ ): void {
231
+ const fields = ['nFft', 'hopLength', 'hpssKernelHarmonic', 'hpssKernelPercussive'] as const;
232
+ for (const field of fields) {
233
+ const value = options[field];
234
+ if (value !== undefined) {
235
+ assertIntegralField(fnName, value, field);
236
+ }
237
+ }
238
+ }
239
+
87
240
  export function assertNonNegativeInteger(fnName: string, value: number, argName: string): void {
88
241
  if (!Number.isInteger(value) || value < 0) {
89
242
  throw new RangeError(`${fnName}: ${argName} must be a non-negative integer`);
90
243
  }
91
244
  }
92
245
 
246
+ /** Integer strictly greater than zero, up to the native `int` ceiling. */
93
247
  export function assertPositiveInteger(fnName: string, value: number, argName: string): void {
94
- if (!Number.isInteger(value) || value <= 0) {
248
+ // Two refusals, not one: 512.7 is positive, so reporting it as non-positive
249
+ // names a property it has. The ceiling travels with the sign rather than with
250
+ // integrality, because both describe a value the native `int` can carry.
251
+ if (!Number.isInteger(value)) {
252
+ throw new RangeError(`${fnName}: ${argName} must be an integer`);
253
+ }
254
+ if (value <= 0 || value > C_INT_MAX) {
95
255
  throw new RangeError(`${fnName}: ${argName} must be a positive integer`);
96
256
  }
97
257
  }
98
258
 
259
+ /**
260
+ * Split "not an integer" into the two mistakes it can be: `TypeError` when the
261
+ * value is not a `number` at all, `RangeError` when it is a number but not
262
+ * integral -- the right-type, wrong-domain half of the same message.
263
+ *
264
+ * `Number.isInteger` alone answers both, which is why a single check reads as
265
+ * sufficient; what it cannot do is say which one happened, and a caller can act
266
+ * only on the mistake they made. The classes are the contract on this surface,
267
+ * so a sibling argument in the same call must not report a fraction differently
268
+ * from this one.
269
+ */
270
+ export function assertIntegerValue(
271
+ fnName: string,
272
+ value: unknown,
273
+ argName: string,
274
+ ): asserts value is number {
275
+ if (typeof value !== 'number') {
276
+ throw new TypeError(`${fnName}: ${argName} must be an integer`);
277
+ }
278
+ if (!Number.isInteger(value)) {
279
+ throw new RangeError(`${fnName}: ${argName} must be an integer`);
280
+ }
281
+ }
282
+
283
+ /** Even integer in `[min, max]`, for an FFT-style size where only parity matters. */
284
+ export function assertEvenIntegerAtLeast(
285
+ fnName: string,
286
+ value: number,
287
+ argName: string,
288
+ min: number,
289
+ max: number,
290
+ ): void {
291
+ // Range and parity are separate refusals: 2 ** 31 is already even, so telling
292
+ // its caller the value must be even names a property it has.
293
+ if (!Number.isInteger(value) || value < min || value > max) {
294
+ throw new RangeError(`${fnName}: ${argName} must be an integer in [${min}, ${max}]`);
295
+ }
296
+ if (value % 2 !== 0) {
297
+ throw new RangeError(`${fnName}: ${argName} must be an even integer`);
298
+ }
299
+ }
300
+
301
+ /** General integer-in-`[min, max]` check, for a bound {@link assertU7}/{@link assertNibble} don't cover. */
302
+ export function assertBoundedInteger(
303
+ fnName: string,
304
+ value: number,
305
+ argName: string,
306
+ min: number,
307
+ max: number,
308
+ ): void {
309
+ if (!Number.isInteger(value) || value < min || value > max) {
310
+ throw new RangeError(`${fnName}: ${argName} must be an integer in [${min}, ${max}]`);
311
+ }
312
+ }
313
+
314
+ export function assertU7(fnName: string, value: number, argName: string): number {
315
+ if (!Number.isInteger(value) || value < 0 || value > 127) {
316
+ throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 127]`);
317
+ }
318
+ return value;
319
+ }
320
+
321
+ export function assertNibble(fnName: string, value: number, argName: string): number {
322
+ if (!Number.isInteger(value) || value < 0 || value > 15) {
323
+ throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 15]`);
324
+ }
325
+ return value;
326
+ }
327
+
328
+ export function assertU32(fnName: string, value: number, argName: string): void {
329
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
330
+ throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 4294967295]`);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Convert a caller-supplied index array to `Int32Array`, refusing any element
336
+ * the conversion would change rather than folding it.
337
+ *
338
+ * `Int32Array.from(values, Math.trunc)` turns the sample index `1000.7` into
339
+ * `1000` and `2 ** 31` into `-2 ** 31`. The C ABI takes `const int*`, so both
340
+ * arrive as values nothing downstream can separate from ones the caller chose.
341
+ * An `Int32Array` is returned as-is: its elements are already exact.
342
+ */
343
+ export function toInt32Array(
344
+ fnName: string,
345
+ values: Int32Array | ArrayLike<number>,
346
+ argName: string,
347
+ ): Int32Array {
348
+ if (values instanceof Int32Array) {
349
+ return values;
350
+ }
351
+ const out = new Int32Array(values.length);
352
+ for (let i = 0; i < values.length; i++) {
353
+ const element = values[i];
354
+ // The element shape of `assertIntegerValue`, and it splits the same way: the
355
+ // class says whether the entry was the wrong type or the wrong domain, and a
356
+ // sibling element in the same array must not report a fraction differently.
357
+ if (typeof element !== 'number') {
358
+ throw new TypeError(`${fnName}: ${argName}[${i}] must be an integer`);
359
+ }
360
+ if (!Number.isInteger(element)) {
361
+ throw new RangeError(`${fnName}: ${argName}[${i}] must be an integer`);
362
+ }
363
+ if (element < C_INT_MIN || element > C_INT_MAX) {
364
+ throw new RangeError(
365
+ `${fnName}: ${argName}[${i}] must be an integer in [${C_INT_MIN}, ${C_INT_MAX}]`,
366
+ );
367
+ }
368
+ out[i] = element;
369
+ }
370
+ return out;
371
+ }
372
+
99
373
  export function assertInterleavedSamples(
100
374
  fnName: string,
101
375
  samples: ArrayLike<number>,
package/src/web_midi.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MidiCcBindOptions } from './realtime_engine';
2
+ import { assertNibble } from './validation';
2
3
 
3
4
  export interface WebMidiEngine {
4
5
  bindMidiCc(
@@ -394,12 +395,6 @@ function readU7(data: ArrayLike<number>, index: number): number {
394
395
  return value;
395
396
  }
396
397
 
397
- function assertNibble(fnName: string, value: number, field: string): void {
398
- if (!Number.isInteger(value) || value < 0 || value > 15) {
399
- throw new RangeError(`${fnName}: ${field} must be an integer in [0, 15]`);
400
- }
401
- }
402
-
403
398
  function iterInputs(access: MidiAccessLike): Iterable<[string, MidiInputLike]> {
404
399
  return access.inputs instanceof Map ? access.inputs.entries() : access.inputs;
405
400
  }
package/src/worker.ts CHANGED
@@ -98,8 +98,16 @@ function cancelledError(): SonareError {
98
98
  */
99
99
  export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): void {
100
100
  const cancelled = new Set<number>();
101
+ // IDs with a run() in flight, from message dispatch to its finally block.
102
+ // A cancel naming an ID outside this set has nothing left to cancel -- the
103
+ // run already settled, or never started -- and must not be recorded, or it
104
+ // leaks a Set entry no later message ever clears.
105
+ const inFlight = new Set<number>();
101
106
 
102
107
  const run = async (message: OfflineWorkerRunMessage): Promise<void> => {
108
+ // Synchronous: runs before this async function's first `await`, so it is
109
+ // visible to the listener below before any other message can be handled.
110
+ inFlight.add(message.id);
103
111
  const cancelFlag = message.cancelBuffer ? new Int32Array(message.cancelBuffer) : undefined;
104
112
  const isCancelled = (): boolean =>
105
113
  cancelled.has(message.id) || (cancelFlag !== undefined && Atomics.load(cancelFlag, 0) !== 0);
@@ -107,19 +115,22 @@ export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): v
107
115
  endpoint.postMessage({ type: 'sonare:offline-progress', id: message.id, progress, stage });
108
116
  return isCancelled() ? false : undefined;
109
117
  };
110
-
111
118
  try {
112
119
  await init();
113
120
  if (isCancelled()) {
114
121
  throw cancelledError();
115
122
  }
116
123
 
124
+ // `cancel` is a separate channel from `onProgress`: the native call polls
125
+ // it and never reads what the progress callback returns, so passing only
126
+ // onProgress left a running operation no way to learn it was cancelled.
117
127
  let result: unknown;
118
128
  switch (message.operation) {
119
129
  case 'analyze':
120
130
  result = analyzeWithProgress({
121
131
  ...(message.request as unknown as MusicAnalyzeRequest),
122
132
  onProgress,
133
+ cancel: isCancelled,
123
134
  });
124
135
  break;
125
136
  case 'detectBpm':
@@ -135,12 +146,14 @@ export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): v
135
146
  result = masterAudio({
136
147
  ...(message.request as unknown as MasterAudioRequest),
137
148
  onProgress,
149
+ cancel: isCancelled,
138
150
  });
139
151
  break;
140
152
  case 'masterAudioStereo':
141
153
  result = masterAudioStereo({
142
154
  ...(message.request as unknown as MasterAudioStereoRequest),
143
155
  onProgress,
156
+ cancel: isCancelled,
144
157
  });
145
158
  break;
146
159
  }
@@ -159,13 +172,16 @@ export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): v
159
172
  });
160
173
  } finally {
161
174
  cancelled.delete(message.id);
175
+ inFlight.delete(message.id);
162
176
  }
163
177
  };
164
178
 
165
179
  endpoint.addEventListener('message', (event) => {
166
180
  const message = event.data;
167
181
  if (message.type === 'sonare:offline-cancel') {
168
- cancelled.add(message.id);
182
+ if (inFlight.has(message.id)) {
183
+ cancelled.add(message.id);
184
+ }
169
185
  return;
170
186
  }
171
187
  void run(message);
@@ -1,2 +1,39 @@
1
1
  export type WorkletInput = readonly (readonly Float32Array[])[];
2
2
  export type WorkletOutput = Float32Array[][];
3
+
4
+ /**
5
+ * Copies one plane per output channel. Shared by every worklet output — the
6
+ * engine's program and cue buses and the voice changer — so they cannot drift
7
+ * in their padding behaviour. Allocation-free.
8
+ *
9
+ * A host output wider than the engine's plane count is mapped the way Web Audio
10
+ * up-mixes rather than by padding with a copy of plane 0:
11
+ * - A single plane fans out to every output channel, so a mono engine driving a
12
+ * stereo host stays centred instead of hard-panned left.
13
+ * - With two or more planes, an output channel past the last plane is silence.
14
+ * Duplicating plane 0 into it would put the left signal in a rear or centre
15
+ * channel and add correlated energy the engine never produced.
16
+ *
17
+ * Everything the source does not fill is zeroed — the tail past `frames`, and
18
+ * the remainder of a plane shorter than `frames` — so no sample of the previous
19
+ * block survives into this one.
20
+ */
21
+ export function copyPlanesToOutput(
22
+ output: Float32Array[],
23
+ planes: readonly Float32Array[],
24
+ frames: number,
25
+ ): void {
26
+ const monoFanOut = planes.length === 1;
27
+ for (let ch = 0; ch < output.length; ch++) {
28
+ const target = output[ch];
29
+ const source = monoFanOut ? planes[0] : planes[ch];
30
+ let copied = 0;
31
+ if (source) {
32
+ copied = Math.min(target.length, frames, source.length);
33
+ target.set(source.subarray(0, copied));
34
+ }
35
+ if (copied < target.length) {
36
+ target.fill(0, copied);
37
+ }
38
+ }
39
+ }