@libraz/libsonare 1.7.1 → 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 -170
  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 -5837
  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 -7329
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +3816 -1359
  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 -2142
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +2889 -466
  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 +285 -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 +64 -24
  255. package/src/project_class.ts +502 -29
  256. package/src/project_internal.ts +151 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +302 -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 +750 -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 +1125 -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 +212 -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 +85 -28
  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 +307 -71
  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 +239 -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 +22 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
@@ -1,5 +1,13 @@
1
+ import type {
2
+ BuiltinSynthBinding,
3
+ Sf2InstrumentConfig,
4
+ Sf2ProgramStatus,
5
+ SynthPatch,
6
+ } from './instrument_types';
1
7
  import {
2
8
  assertProjectMidiEvents,
9
+ normalizeSynthInstrument,
10
+ projectAutomationPointValue,
3
11
  projectAutomationTargetKindValue,
4
12
  projectLoopModeValue,
5
13
  projectMidi1Event,
@@ -9,7 +17,6 @@ import {
9
17
  type WasmProject,
10
18
  } from './project_internal';
11
19
  import type {
12
- BuiltinSynthBinding,
13
20
  ExternalSeparatedStemImportRequest,
14
21
  ExternalSeparatedStemImportResult,
15
22
  MidiCcLearnOptions,
@@ -35,22 +42,53 @@ import type {
35
42
  ProjectMidiCcBinding,
36
43
  ProjectMidiClipResult,
37
44
  ProjectMidiEvent,
45
+ ProjectMidiFxBakeRequest,
46
+ ProjectMidiFxBakeResult,
47
+ ProjectMidiFxPreviewRequest,
38
48
  ProjectMidiRouteConfig,
39
49
  ProjectMidiRouteResult,
40
50
  ProjectNotePairValidation,
41
51
  ProjectSource,
42
52
  ProjectTempoCandidate,
53
+ ProjectTempoOptions,
43
54
  ProjectTempoSegment,
44
55
  ProjectTimeSignatureSegment,
45
56
  ProjectTrack,
46
57
  ProjectTrackDesc,
47
58
  ProjectTrackKind,
59
+ ProjectTranscribeRequest,
48
60
  ProjectWarpMapDesc,
49
61
  ProjectWarpMode,
50
- Sf2InstrumentConfig,
51
- Sf2ProgramStatus,
52
- SynthPatch,
53
62
  } from './project_types';
63
+ import {
64
+ assertBoundedInteger,
65
+ assertNibble,
66
+ assertSampleRate,
67
+ assertSamples,
68
+ assertU7,
69
+ } from './validation';
70
+
71
+ /**
72
+ * Folds the positional and request call forms of `bakeMidiFx` into one shape,
73
+ * so defaults, validation and errors cannot diverge between them.
74
+ */
75
+ function normalizeMidiFxBakeRequest(
76
+ clipIdOrRequest: number | ProjectMidiFxBakeRequest,
77
+ configJson?: string,
78
+ ): Required<ProjectMidiFxBakeRequest> {
79
+ if (typeof clipIdOrRequest === 'number') {
80
+ return {
81
+ clipId: clipIdOrRequest,
82
+ configJson: configJson ?? '',
83
+ withSourceIndex: false,
84
+ };
85
+ }
86
+ return {
87
+ clipId: clipIdOrRequest.clipId,
88
+ configJson: clipIdOrRequest.configJson,
89
+ withSourceIndex: clipIdOrRequest.withSourceIndex ?? false,
90
+ };
91
+ }
54
92
 
55
93
  function validateAssistSidecarUint32(value: unknown, field: string): number {
56
94
  if (
@@ -87,6 +125,52 @@ function validateAssistSidecarModuleId(value: unknown): string {
87
125
  return value;
88
126
  }
89
127
 
128
+ const MIDI2_U16_MAX = 0xffff;
129
+ const MIDI2_U32_MAX = 0xffffffff;
130
+
131
+ // UMP MIDI-2.0 channel-voice packet (message type 0x4). Canonical layout is
132
+ // sonare::midi::make_midi2_* (C-ABI sonare_midi2_*); the golden vectors in
133
+ // midi2-ump.test.ts lock this copy against it.
134
+ function midi2Event(
135
+ fnName: string,
136
+ ppq: number,
137
+ group: number,
138
+ opcode: number,
139
+ channel: number,
140
+ byte2: number,
141
+ byte3: number,
142
+ word1: number,
143
+ ): ProjectMidiEvent {
144
+ if (!Number.isFinite(ppq) || ppq < 0) {
145
+ throw new RangeError(`${fnName}: ppq must be a non-negative finite number`);
146
+ }
147
+ const g = assertNibble(fnName, group, 'group');
148
+ const ch = assertNibble(fnName, channel, 'channel');
149
+ const word0 =
150
+ ((0x4 << 28) | (g << 24) | (opcode << 20) | (ch << 16) | (byte2 << 8) | byte3) >>> 0;
151
+ return { ppq, data0: word0, data1: word1 >>> 0 };
152
+ }
153
+
154
+ function midi2U32(fnName: string, value: number, argName: string): number {
155
+ assertBoundedInteger(fnName, value, argName, 0, MIDI2_U32_MAX);
156
+ return value;
157
+ }
158
+
159
+ function midi2U16(fnName: string, value: number, argName: string): number {
160
+ assertBoundedInteger(fnName, value, argName, 0, MIDI2_U16_MAX);
161
+ return value;
162
+ }
163
+
164
+ function midi2I32(fnName: string, value: number, argName: string): number {
165
+ assertBoundedInteger(fnName, value, argName, -0x80000000, 0x7fffffff);
166
+ return value;
167
+ }
168
+
169
+ function midi2Byte(fnName: string, value: number, argName: string): number {
170
+ assertBoundedInteger(fnName, value, argName, 0, 0xff);
171
+ return value;
172
+ }
173
+
90
174
  /**
91
175
  * Headless DAW project (control-thread-only arrangement model).
92
176
  *
@@ -115,6 +199,7 @@ function validateAssistSidecarModuleId(value: unknown): string {
115
199
  */
116
200
  export class Project {
117
201
  private native: WasmProject;
202
+ private released = false;
118
203
 
119
204
  constructor() {
120
205
  this.native = new (projectModule().Project)();
@@ -179,6 +264,233 @@ export class Project {
179
264
  return projectMidi1Event('Project.midiProgram', ppq, group, 0xc, channel, program, 0);
180
265
  }
181
266
 
267
+ /**
268
+ * Pack a MIDI 2.0 note-on event accepted by {@link setMidiEvents}.
269
+ * `velocity16` is the full 16-bit velocity (0 is a sounding note-on);
270
+ * `attributeType` 0 is none, 3 is pitch 7.9.
271
+ */
272
+ static midi2NoteOn(
273
+ ppq: number,
274
+ group: number,
275
+ channel: number,
276
+ note: number,
277
+ velocity16: number,
278
+ attributeType = 0,
279
+ attributeData = 0,
280
+ ): ProjectMidiEvent {
281
+ const fn = 'Project.midi2NoteOn';
282
+ const n = assertU7(fn, note, 'note');
283
+ const vel = midi2U16(fn, velocity16, 'velocity16');
284
+ const type = midi2Byte(fn, attributeType, 'attributeType');
285
+ const data = midi2U16(fn, attributeData, 'attributeData');
286
+ return midi2Event(fn, ppq, group, 0x9, channel, n, type, ((vel << 16) | data) >>> 0);
287
+ }
288
+
289
+ /** Pack a MIDI 2.0 note-off event (`velocity16` is the full 16-bit release velocity). */
290
+ static midi2NoteOff(
291
+ ppq: number,
292
+ group: number,
293
+ channel: number,
294
+ note: number,
295
+ velocity16 = 0,
296
+ ): ProjectMidiEvent {
297
+ const fn = 'Project.midi2NoteOff';
298
+ const n = assertU7(fn, note, 'note');
299
+ const vel = midi2U16(fn, velocity16, 'velocity16');
300
+ return midi2Event(fn, ppq, group, 0x8, channel, n, 0, (vel << 16) >>> 0);
301
+ }
302
+
303
+ /** Pack a MIDI 2.0 control-change event (`value32` is the full 32-bit value). */
304
+ static midi2Cc(
305
+ ppq: number,
306
+ group: number,
307
+ channel: number,
308
+ controller: number,
309
+ value32: number,
310
+ ): ProjectMidiEvent {
311
+ const fn = 'Project.midi2Cc';
312
+ const c = assertU7(fn, controller, 'controller');
313
+ return midi2Event(fn, ppq, group, 0xb, channel, c, 0, midi2U32(fn, value32, 'value32'));
314
+ }
315
+
316
+ /** Pack a MIDI 2.0 poly-pressure event (`pressure32` is the full 32-bit pressure). */
317
+ static midi2PolyPressure(
318
+ ppq: number,
319
+ group: number,
320
+ channel: number,
321
+ note: number,
322
+ pressure32: number,
323
+ ): ProjectMidiEvent {
324
+ const fn = 'Project.midi2PolyPressure';
325
+ const n = assertU7(fn, note, 'note');
326
+ return midi2Event(fn, ppq, group, 0xa, channel, n, 0, midi2U32(fn, pressure32, 'pressure32'));
327
+ }
328
+
329
+ /** Pack a MIDI 2.0 channel-pressure event (`pressure32` is the full 32-bit pressure). */
330
+ static midi2ChannelPressure(
331
+ ppq: number,
332
+ group: number,
333
+ channel: number,
334
+ pressure32: number,
335
+ ): ProjectMidiEvent {
336
+ const fn = 'Project.midi2ChannelPressure';
337
+ return midi2Event(fn, ppq, group, 0xd, channel, 0, 0, midi2U32(fn, pressure32, 'pressure32'));
338
+ }
339
+
340
+ /** Pack a MIDI 2.0 pitch-bend event (`bend32` is unsigned 32-bit, center = 0x80000000). */
341
+ static midi2PitchBend(
342
+ ppq: number,
343
+ group: number,
344
+ channel: number,
345
+ bend32: number,
346
+ ): ProjectMidiEvent {
347
+ const fn = 'Project.midi2PitchBend';
348
+ return midi2Event(fn, ppq, group, 0xe, channel, 0, 0, midi2U32(fn, bend32, 'bend32'));
349
+ }
350
+
351
+ /**
352
+ * Pack a MIDI 2.0 program-change event. The bank travels in the same message
353
+ * and is applied only when `bankValid` is true.
354
+ */
355
+ static midi2Program(
356
+ ppq: number,
357
+ group: number,
358
+ channel: number,
359
+ program: number,
360
+ bankValid = false,
361
+ bankMsb = 0,
362
+ bankLsb = 0,
363
+ ): ProjectMidiEvent {
364
+ const fn = 'Project.midi2Program';
365
+ const p = assertU7(fn, program, 'program');
366
+ const msb = assertU7(fn, bankMsb, 'bankMsb');
367
+ const lsb = assertU7(fn, bankLsb, 'bankLsb');
368
+ const word1 = ((p << 24) | (msb << 8) | lsb) >>> 0;
369
+ return midi2Event(fn, ppq, group, 0xc, channel, 0, bankValid ? 1 : 0, word1);
370
+ }
371
+
372
+ /** Pack a MIDI 2.0 registered controller (RPN) event. */
373
+ static midi2RegisteredController(
374
+ ppq: number,
375
+ group: number,
376
+ channel: number,
377
+ bank: number,
378
+ index: number,
379
+ value32: number,
380
+ ): ProjectMidiEvent {
381
+ const fn = 'Project.midi2RegisteredController';
382
+ const b = assertU7(fn, bank, 'bank');
383
+ const i = assertU7(fn, index, 'index');
384
+ return midi2Event(fn, ppq, group, 0x2, channel, b, i, midi2U32(fn, value32, 'value32'));
385
+ }
386
+
387
+ /** Pack a MIDI 2.0 assignable controller (NRPN) event. */
388
+ static midi2AssignableController(
389
+ ppq: number,
390
+ group: number,
391
+ channel: number,
392
+ bank: number,
393
+ index: number,
394
+ value32: number,
395
+ ): ProjectMidiEvent {
396
+ const fn = 'Project.midi2AssignableController';
397
+ const b = assertU7(fn, bank, 'bank');
398
+ const i = assertU7(fn, index, 'index');
399
+ return midi2Event(fn, ppq, group, 0x3, channel, b, i, midi2U32(fn, value32, 'value32'));
400
+ }
401
+
402
+ /** Pack a MIDI 2.0 relative registered controller event (`delta32` is a signed change). */
403
+ static midi2RelativeRegisteredController(
404
+ ppq: number,
405
+ group: number,
406
+ channel: number,
407
+ bank: number,
408
+ index: number,
409
+ delta32: number,
410
+ ): ProjectMidiEvent {
411
+ const fn = 'Project.midi2RelativeRegisteredController';
412
+ const b = assertU7(fn, bank, 'bank');
413
+ const i = assertU7(fn, index, 'index');
414
+ return midi2Event(fn, ppq, group, 0x4, channel, b, i, midi2I32(fn, delta32, 'delta32'));
415
+ }
416
+
417
+ /** Pack a MIDI 2.0 relative assignable controller event (`delta32` is a signed change). */
418
+ static midi2RelativeAssignableController(
419
+ ppq: number,
420
+ group: number,
421
+ channel: number,
422
+ bank: number,
423
+ index: number,
424
+ delta32: number,
425
+ ): ProjectMidiEvent {
426
+ const fn = 'Project.midi2RelativeAssignableController';
427
+ const b = assertU7(fn, bank, 'bank');
428
+ const i = assertU7(fn, index, 'index');
429
+ return midi2Event(fn, ppq, group, 0x5, channel, b, i, midi2I32(fn, delta32, 'delta32'));
430
+ }
431
+
432
+ /** Pack a MIDI 2.0 registered per-note controller event (`index` is 0..255). */
433
+ static midi2RegisteredPerNoteController(
434
+ ppq: number,
435
+ group: number,
436
+ channel: number,
437
+ note: number,
438
+ index: number,
439
+ value32: number,
440
+ ): ProjectMidiEvent {
441
+ const fn = 'Project.midi2RegisteredPerNoteController';
442
+ const n = assertU7(fn, note, 'note');
443
+ const i = midi2Byte(fn, index, 'index');
444
+ return midi2Event(fn, ppq, group, 0x0, channel, n, i, midi2U32(fn, value32, 'value32'));
445
+ }
446
+
447
+ /** Pack a MIDI 2.0 assignable per-note controller event (`index` is 0..255). */
448
+ static midi2AssignablePerNoteController(
449
+ ppq: number,
450
+ group: number,
451
+ channel: number,
452
+ note: number,
453
+ index: number,
454
+ value32: number,
455
+ ): ProjectMidiEvent {
456
+ const fn = 'Project.midi2AssignablePerNoteController';
457
+ const n = assertU7(fn, note, 'note');
458
+ const i = midi2Byte(fn, index, 'index');
459
+ return midi2Event(fn, ppq, group, 0x1, channel, n, i, midi2U32(fn, value32, 'value32'));
460
+ }
461
+
462
+ /** Pack a MIDI 2.0 per-note pitch-bend event (`bend32` is unsigned 32-bit, center = 0x80000000). */
463
+ static midi2PerNotePitchBend(
464
+ ppq: number,
465
+ group: number,
466
+ channel: number,
467
+ note: number,
468
+ bend32: number,
469
+ ): ProjectMidiEvent {
470
+ const fn = 'Project.midi2PerNotePitchBend';
471
+ const n = assertU7(fn, note, 'note');
472
+ return midi2Event(fn, ppq, group, 0x6, channel, n, 0, midi2U32(fn, bend32, 'bend32'));
473
+ }
474
+
475
+ /**
476
+ * Pack a MIDI 2.0 per-note management event. `detach` sets the D flag
477
+ * (detach per-note controllers from voices already sounding on `note`);
478
+ * `reset` sets the S flag (reset the note's per-note controllers).
479
+ */
480
+ static midi2PerNoteManagement(
481
+ ppq: number,
482
+ group: number,
483
+ channel: number,
484
+ note: number,
485
+ detach = false,
486
+ reset = false,
487
+ ): ProjectMidiEvent {
488
+ const fn = 'Project.midi2PerNoteManagement';
489
+ const n = assertU7(fn, note, 'note');
490
+ const flags = (detach ? 0x02 : 0) | (reset ? 0x01 : 0);
491
+ return midi2Event(fn, ppq, group, 0xf, channel, n, flags, 0);
492
+ }
493
+
182
494
  /** Return the General MIDI instrument name for `program`, or `null` when out of range. */
183
495
  static gmInstrumentName(program: number): string | null {
184
496
  return projectModule().midiGmInstrumentName(program);
@@ -310,9 +622,7 @@ export class Project {
310
622
  channel: number,
311
623
  bend: number,
312
624
  ): ProjectMidiEvent {
313
- if (!Number.isInteger(bend) || bend < 0 || bend > 0x3fff) {
314
- throw new RangeError('Project.midiPitchBend: bend must be an integer in [0, 16383]');
315
- }
625
+ assertBoundedInteger('Project.midiPitchBend', bend, 'bend', 0, 0x3fff);
316
626
  return projectMidi1Event(
317
627
  'Project.midiPitchBend',
318
628
  ppq,
@@ -370,7 +680,11 @@ export class Project {
370
680
  return this.native.toJson();
371
681
  }
372
682
 
373
- /** Set the project sample rate in Hz. Must be > 0. */
683
+ /**
684
+ * Set the project sample rate in Hz. Must be in `[8000, 384000]`; anything
685
+ * outside that range throws. Applied through the edit history, so it is
686
+ * undoable.
687
+ */
374
688
  setSampleRate(sampleRate: number): void {
375
689
  this.native.setSampleRate(sampleRate);
376
690
  }
@@ -462,7 +776,24 @@ export class Project {
462
776
  this.native.setTrackMidiDestination(trackId, destinationId);
463
777
  }
464
778
 
465
- /** Set a track's linear playback gain (1.0 = unity; >= 0) via an undoable edit. */
779
+ /**
780
+ * Set a track's linear playback gain (1.0 = unity; >= 0) via an undoable edit.
781
+ *
782
+ * The value reaches the track's audio and MIDI alike, but the stage it lands
783
+ * on follows the track's channel strip. A strip bound by this track alone
784
+ * (including one synthesized for an unbound track) carries the controls on its
785
+ * own fader and panner. A strip several tracks share processes their sum and
786
+ * carries none of them; each track applies its controls upstream instead — on
787
+ * its own clip schedules for audio, on its track lane for MIDI.
788
+ *
789
+ * A MIDI track's gain/pan on a shared strip ride the track lane, which is fed
790
+ * per source track only by an instrument that preserves source-track identity
791
+ * (see {@link setTrackMidiDestination}). An opaque host-callback instrument, or
792
+ * one reporting non-zero latency, renders one buffer per destination and has no
793
+ * per-track stage on a shared strip, so its gain/pan do not reach the bounce
794
+ * there; bind such an instrument to a track with an exclusive strip. Mute and
795
+ * solo are unaffected: a silenced MIDI track schedules no events at all.
796
+ */
466
797
  setTrackGain(trackId: number, gain: number): void {
467
798
  this.native.setTrackGain(trackId, gain);
468
799
  }
@@ -477,7 +808,17 @@ export class Project {
477
808
  this.native.setTrackSolo(trackId, solo);
478
809
  }
479
810
 
480
- /** Set a track's stereo balance in [-1, +1] (0 = center) via an undoable edit. */
811
+ /**
812
+ * Set a track's stereo balance in [-1, +1] (0 = center) via an undoable edit.
813
+ *
814
+ * See {@link setTrackGain} for which stage a track's controls land on. The pan
815
+ * law that shapes the balance belongs to that stage: the strip's configured law
816
+ * on a channel strip and on the clips of an audio track sharing a strip, and
817
+ * the track lane's law for a MIDI track sharing a strip (the law of whatever
818
+ * strip the host bound to that lane, or a linear balance when none is bound).
819
+ * Every law is normalized so a centered track stays at unity and only the away
820
+ * channel is attenuated, so the difference is a taper, not a level offset.
821
+ */
481
822
  setTrackPan(trackId: number, pan: number): void {
482
823
  this.native.setTrackPan(trackId, pan);
483
824
  }
@@ -518,7 +859,18 @@ export class Project {
518
859
  this.native.setMaxHistoryBytes(bytes);
519
860
  }
520
861
 
521
- /** Replace a MIDI clip's entire event list. */
862
+ /**
863
+ * Replace a MIDI clip's entire event list.
864
+ *
865
+ * @remarks
866
+ * Drops the clip's SysEx, which {@link importSmf} and {@link exportSmf} both
867
+ * keep. A clip's SysEx payloads sit beside the event list and are reached by
868
+ * a handle {@link ProjectMidiEvent} does not carry, so replacing the list
869
+ * leaves nothing referring to them: a GS setup block that survives an import
870
+ * and an export byte for byte is gone after one call here. Nothing reads the
871
+ * handles back either, so a caller that must keep the setup edits the
872
+ * exported file rather than the event list.
873
+ */
522
874
  setMidiEvents(
523
875
  clipId: number,
524
876
  events: ReadonlyArray<ProjectMidiEvent | readonly [number, number, number]>,
@@ -536,8 +888,15 @@ export class Project {
536
888
  return this.native.importSmf(data);
537
889
  }
538
890
 
539
- /** Export the project's tempo map + MIDI clips to an SMF byte buffer. */
540
- exportSmf(): Uint8Array {
891
+ /**
892
+ * Export the project's tempo map + MIDI clips to an SMF byte buffer.
893
+ *
894
+ * @remarks
895
+ * The buffer owns a plain `ArrayBuffer`, which is what the `Blob` / `File`
896
+ * constructors accept — so `new Blob([project.exportSmf()])` compiles without
897
+ * a copy through `new Uint8Array(...)` first.
898
+ */
899
+ exportSmf(): Uint8Array<ArrayBuffer> {
541
900
  return this.native.exportSmf();
542
901
  }
543
902
 
@@ -555,8 +914,12 @@ export class Project {
555
914
  * Export the project's tempo map + MIDI clips to a MIDI 2.0 Clip File
556
915
  * (`SMF2CLIP`) byte buffer. MIDI 2.0-only events are written without loss —
557
916
  * prefer this over {@link exportSmf} when MIDI 2.0 fidelity matters.
917
+ *
918
+ * @remarks
919
+ * As with {@link exportSmf}, the buffer owns a plain `ArrayBuffer` and goes
920
+ * straight into a `Blob`.
558
921
  */
559
- exportClipFile(): Uint8Array {
922
+ exportClipFile(): Uint8Array<ArrayBuffer> {
560
923
  return this.native.exportClipFile();
561
924
  }
562
925
 
@@ -584,8 +947,33 @@ export class Project {
584
947
  * Destructively bake a MIDI-FX chain into all stored events. Large clips are
585
948
  * drained without truncation; failure leaves the original clip unchanged.
586
949
  */
587
- bakeMidiFx(clipId: number, configJson: string): void {
588
- this.native.bakeMidiFx(clipId, configJson);
950
+ bakeMidiFx(clipId: number, configJson: string): void;
951
+ /**
952
+ * Request form. Setting `withSourceIndex` also returns per-event provenance,
953
+ * so a selection or an editorial annotation can be carried across the bake.
954
+ */
955
+ bakeMidiFx(request: ProjectMidiFxBakeRequest): ProjectMidiFxBakeResult;
956
+ bakeMidiFx(
957
+ clipIdOrRequest: number | ProjectMidiFxBakeRequest,
958
+ configJson?: string,
959
+ ): ProjectMidiFxBakeResult {
960
+ const request = normalizeMidiFxBakeRequest(clipIdOrRequest, configJson);
961
+ if (!request.withSourceIndex) {
962
+ this.native.bakeMidiFx(request.clipId, request.configJson);
963
+ return {};
964
+ }
965
+ return {
966
+ sourceIndex: this.native.bakeMidiFxWithSourceIndex(request.clipId, request.configJson),
967
+ };
968
+ }
969
+
970
+ /**
971
+ * Count the events {@link bakeMidiFx} would produce for this clip and
972
+ * configuration, without mutating the project. The transform is
973
+ * deterministic, so the count matches what the bake goes on to produce.
974
+ */
975
+ previewMidiFxCount(request: ProjectMidiFxPreviewRequest): number {
976
+ return this.native.previewMidiFxCount(request.clipId, request.configJson);
589
977
  }
590
978
 
591
979
  /** Backward alias for {@link bakeMidiFx}. */
@@ -603,19 +991,62 @@ export class Project {
603
991
  return this.native.validateMidiNotes(clipId);
604
992
  }
605
993
 
994
+ /**
995
+ * Transcribe mono audio straight into a MIDI clip's event list, **replacing**
996
+ * whatever it held — exactly as {@link setMidiEvents} does.
997
+ *
998
+ * The PPQ grid is this project's own tempo map, which is why there is no
999
+ * `tempoBpm` field: a project whose tempo was installed by {@link autoTempo}
1000
+ * transcribes onto that map rather than onto a second, separately detected
1001
+ * tempo. Use the standalone `transcribe` when you want events without a
1002
+ * project.
1003
+ *
1004
+ * Quantizing, tempo detection and key/chord annotation are not done here —
1005
+ * see `transcribe` for what each belongs to.
1006
+ *
1007
+ * @returns the number of notes written (half the events)
1008
+ * @throws {RangeError} on empty `samples`, a non-finite sample, or a
1009
+ * `sampleRate` outside `[8000, 384000]`
1010
+ * @throws {SonareError} `InvalidParameter` when `clipId` is unknown or not a
1011
+ * MIDI clip, or on an option outside its domain; `NotSupported` when the
1012
+ * library was built without the pitch editor
1013
+ */
1014
+ transcribeToClip(request: ProjectTranscribeRequest): number {
1015
+ assertSamples('Project.transcribeToClip', request.samples, true);
1016
+ assertSampleRate('Project.transcribeToClip', request.sampleRate);
1017
+ return this.native.transcribeToClip(
1018
+ request.clipId,
1019
+ request.samples,
1020
+ request.sampleRate,
1021
+ request,
1022
+ );
1023
+ }
1024
+
606
1025
  /** Return ranked tempo-octave and detected-meter candidates without editing. */
607
- analyzeTempo(audio: Float32Array, sampleRate: number): ProjectTempoCandidate[] {
608
- return this.native.analyzeTempo(audio, sampleRate);
1026
+ analyzeTempo(
1027
+ audio: Float32Array,
1028
+ sampleRate: number,
1029
+ options?: ProjectTempoOptions,
1030
+ ): ProjectTempoCandidate[] {
1031
+ return this.native.analyzeTempo(audio, sampleRate, options);
609
1032
  }
610
1033
 
611
- /** Detect and install a ranked tempo candidate; optionally apply detected meter. */
1034
+ /**
1035
+ * Detect and install a ranked tempo candidate; optionally apply detected meter.
1036
+ *
1037
+ * @remarks
1038
+ * `candidateIndex` indexes the ranking {@link analyzeTempo} produced, so pair
1039
+ * the two on the same `options`. Read the installed map back with
1040
+ * {@link tempoSegmentCount} and {@link tempoSegmentByIndex}.
1041
+ */
612
1042
  autoTempo(
613
1043
  audio: Float32Array,
614
1044
  sampleRate: number,
615
1045
  candidateIndex = 0,
616
1046
  applyTimeSignatures = false,
1047
+ options?: ProjectTempoOptions,
617
1048
  ): number {
618
- return this.native.autoTempo(audio, sampleRate, candidateIndex, applyTimeSignatures);
1049
+ return this.native.autoTempo(audio, sampleRate, candidateIndex, applyTimeSignatures, options);
619
1050
  }
620
1051
 
621
1052
  /** Snap to a bar (`division=0`), beat (`1`), or beat subdivision (`2+`). */
@@ -677,9 +1108,9 @@ export class Project {
677
1108
 
678
1109
  /**
679
1110
  * Compile + render the project offline, routing MIDI tracks through the
680
- * patch-driven NativeSynth — the full synthesizer (subtractive / FM /
681
- * Karplus-Strong / modal / additive / percussion / extended-waveguide-piano
682
- * engines plus the realism layer). Pass a {@link SynthPatch}, a preset-name
1111
+ * patch-driven NativeSynth — the full synthesizer (every
1112
+ * {@link SynthEngineMode} engine plus the realism layer; the modes are
1113
+ * enumerated by {@link SYNTH_ENGINE_MODES}). Pass a {@link SynthPatch}, a preset-name
683
1114
  * string (`'saw-lead'` / `'va:saw-lead'`; see {@link synthPresetNames}), or
684
1115
  * an array of either; each object entry may carry `destinationId` (default
685
1116
  * 0) and `useGmPrograms` (default `false`) binding conveniences, neither of
@@ -690,12 +1121,19 @@ export class Project {
690
1121
  * both create one default binding. Use an explicitly empty array `[]` (or
691
1122
  * runtime `null`) for zero bindings. Unknown preset names throw.
692
1123
  * Deterministic for a fixed project + options + patch.
1124
+ *
1125
+ * An `engineMode: 'sample'` patch reads its PCM from the {@link SampleBank}
1126
+ * passed as `sampleBank`; the bank must still be alive when the bounce runs,
1127
+ * and one bound without a bank renders silence.
693
1128
  */
694
1129
  bounceWithSynthInstrument(
695
1130
  instrument: SynthPatch | string | ReadonlyArray<SynthPatch | string> = {},
696
1131
  options: ProjectBounceOptions = {},
697
1132
  ): Float32Array {
698
- return this.native.bounceWithSynthInstrument(instrument, options);
1133
+ const normalized = Array.isArray(instrument)
1134
+ ? instrument.map((entry) => normalizeSynthInstrument(entry))
1135
+ : normalizeSynthInstrument(instrument);
1136
+ return this.native.bounceWithSynthInstrument(normalized, options);
699
1137
  }
700
1138
 
701
1139
  /**
@@ -845,7 +1283,10 @@ export class Project {
845
1283
  if (desc.targetParamId === 0) {
846
1284
  throw new RangeError('project automation lane targetParamId must be non-zero');
847
1285
  }
848
- const nativeDesc: ProjectAutomationLaneDesc = { ...desc };
1286
+ const nativeDesc: ProjectAutomationLaneDesc = {
1287
+ ...desc,
1288
+ points: desc.points.map(projectAutomationPointValue),
1289
+ };
849
1290
  if (Object.keys(desc).includes('targetKind')) {
850
1291
  nativeDesc.targetKind = projectAutomationTargetKindValue(
851
1292
  desc.targetKind as ProjectAutomationTargetKind,
@@ -863,7 +1304,10 @@ export class Project {
863
1304
  if (desc.targetParamId === 0) {
864
1305
  throw new RangeError('project automation lane targetParamId must be non-zero');
865
1306
  }
866
- const nativeDesc: ProjectAutomationLaneDesc = { ...desc };
1307
+ const nativeDesc: ProjectAutomationLaneDesc = {
1308
+ ...desc,
1309
+ points: desc.points.map(projectAutomationPointValue),
1310
+ };
867
1311
  if (Object.keys(desc).includes('targetKind')) {
868
1312
  nativeDesc.targetKind = projectAutomationTargetKindValue(
869
1313
  desc.targetKind as ProjectAutomationTargetKind,
@@ -1047,6 +1491,28 @@ export class Project {
1047
1491
  return this.native.tempoSegmentCount();
1048
1492
  }
1049
1493
 
1494
+ /**
1495
+ * Reads a tempo segment by index, in stored order.
1496
+ *
1497
+ * @param index - Zero-based index below {@link tempoSegmentCount}
1498
+ * @returns The segment, in the shape {@link setTempoSegments} accepts
1499
+ * @throws When the index is at or past the count
1500
+ */
1501
+ tempoSegmentByIndex(index: number): ProjectTempoSegment {
1502
+ return this.native.tempoSegmentByIndex(index);
1503
+ }
1504
+
1505
+ /**
1506
+ * Reads a time-signature segment by index, in stored order.
1507
+ *
1508
+ * @param index - Zero-based index below {@link timeSignatureCount}
1509
+ * @returns The segment, in the shape {@link setTimeSignatures} accepts
1510
+ * @throws When the index is at or past the count
1511
+ */
1512
+ timeSignatureByIndex(index: number): ProjectTimeSignatureSegment {
1513
+ return this.native.timeSignatureByIndex(index);
1514
+ }
1515
+
1050
1516
  /** Number of time-signature segments on the project. */
1051
1517
  timeSignatureCount(): number {
1052
1518
  return this.native.timeSignatureCount();
@@ -1064,15 +1530,22 @@ export class Project {
1064
1530
 
1065
1531
  /**
1066
1532
  * Compile diagnostics produced by the most recent bounce on this project
1067
- * (e.g. MIDI clips rendering silently without a bound instrument). When no
1068
- * bounce has run, the result is empty with `hasTimeline` set.
1533
+ * (e.g. MIDI clips rendering silently without a bound instrument). On a
1534
+ * project no bounce has ever run on, the result is empty in full:
1535
+ * `hasTimeline` is `false` and `diagnostics` is empty. A failed bounce is
1536
+ * distinguishable from that state, because a bounce only loses its timeline
1537
+ * through an error diagnostic and so always reports at least one.
1069
1538
  */
1070
1539
  lastBounceCompileResult(): ProjectCompileResult {
1071
1540
  return this.native.lastBounceCompileResult();
1072
1541
  }
1073
1542
 
1074
- /** Release the underlying WASM object. Safe to call only once. */
1543
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1075
1544
  delete(): void {
1545
+ if (this.released) {
1546
+ return;
1547
+ }
1548
+ this.released = true;
1076
1549
  this.native.delete();
1077
1550
  }
1078
1551