@libraz/libsonare 1.8.1 → 1.8.2

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 (107) hide show
  1. package/README.md +5 -5
  2. package/dist/analysis.js +2 -0
  3. package/dist/analysis.js.map +1 -1
  4. package/dist/index.d.ts +8 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1927 -140
  7. package/dist/index.js.map +1 -1
  8. package/dist/instrument_types.d.ts +27 -3
  9. package/dist/instrument_types.d.ts.map +1 -1
  10. package/dist/mastering_core.d.ts +3 -2
  11. package/dist/mastering_core.d.ts.map +1 -1
  12. package/dist/mixer.d.ts +2 -2
  13. package/dist/project.d.ts +5 -4
  14. package/dist/project.d.ts.map +1 -1
  15. package/dist/project_class.d.ts +57 -3
  16. package/dist/project_class.d.ts.map +1 -1
  17. package/dist/project_internal.d.ts +35 -1
  18. package/dist/project_internal.d.ts.map +1 -1
  19. package/dist/project_types.d.ts +10 -1
  20. package/dist/project_types.d.ts.map +1 -1
  21. package/dist/public_types.d.ts +1 -0
  22. package/dist/public_types.d.ts.map +1 -1
  23. package/dist/public_types_mastering.d.ts +1 -1
  24. package/dist/public_types_mastering.d.ts.map +1 -1
  25. package/dist/public_types_vocal_edit.d.ts +250 -0
  26. package/dist/public_types_vocal_edit.d.ts.map +1 -0
  27. package/dist/public_types_vocal_project.d.ts +63 -0
  28. package/dist/public_types_vocal_project.d.ts.map +1 -0
  29. package/dist/realtime_engine.d.ts +57 -10
  30. package/dist/realtime_engine.d.ts.map +1 -1
  31. package/dist/repair_dereverb.d.ts +1 -0
  32. package/dist/repair_dereverb.d.ts.map +1 -1
  33. package/dist/repair_noise.d.ts +1 -0
  34. package/dist/repair_noise.d.ts.map +1 -1
  35. package/dist/sonare-analysis.wasm +0 -0
  36. package/dist/sonare.d.ts +53 -0
  37. package/dist/sonare.js +1 -1
  38. package/dist/sonare.wasm +0 -0
  39. package/dist/vocal_edit.d.ts +131 -0
  40. package/dist/vocal_edit.d.ts.map +1 -0
  41. package/dist/vocal_edit_worker.d.ts +51 -0
  42. package/dist/vocal_edit_worker.d.ts.map +1 -0
  43. package/dist/vocal_edit_worker.js +1462 -0
  44. package/dist/vocal_edit_worker.js.map +1 -0
  45. package/dist/vocal_edit_worker_client.d.ts +118 -0
  46. package/dist/vocal_edit_worker_client.d.ts.map +1 -0
  47. package/dist/vocal_edit_worker_protocol.d.ts +139 -0
  48. package/dist/vocal_edit_worker_protocol.d.ts.map +1 -0
  49. package/dist/vocal_project.d.ts +17 -0
  50. package/dist/vocal_project.d.ts.map +1 -0
  51. package/dist/web_midi.d.ts +4 -0
  52. package/dist/web_midi.d.ts.map +1 -1
  53. package/dist/worker.js.map +1 -1
  54. package/dist/worklet/engine-mixer-facade.d.ts +2 -1
  55. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -1
  56. package/dist/worklet/engine-node.d.ts +8 -0
  57. package/dist/worklet/engine-node.d.ts.map +1 -1
  58. package/dist/worklet/engine-processor.d.ts +2 -1
  59. package/dist/worklet/engine-processor.d.ts.map +1 -1
  60. package/dist/worklet/engine-register.d.ts.map +1 -1
  61. package/dist/worklet/engine-strips.d.ts +4 -2
  62. package/dist/worklet/engine-strips.d.ts.map +1 -1
  63. package/dist/worklet/engine.d.ts +15 -3
  64. package/dist/worklet/engine.d.ts.map +1 -1
  65. package/dist/worklet/guards.d.ts +3 -1
  66. package/dist/worklet/guards.d.ts.map +1 -1
  67. package/dist/worklet/messages.d.ts +50 -5
  68. package/dist/worklet/messages.d.ts.map +1 -1
  69. package/dist/worklet/protocol.d.ts +10 -0
  70. package/dist/worklet/protocol.d.ts.map +1 -1
  71. package/dist/worklet.d.ts +1 -1
  72. package/dist/worklet.d.ts.map +1 -1
  73. package/dist/worklet.js +223 -16
  74. package/dist/worklet.js.map +1 -1
  75. package/package.json +6 -1
  76. package/src/index.ts +33 -0
  77. package/src/instrument_types.ts +29 -3
  78. package/src/mastering_core.ts +3 -2
  79. package/src/mixer.ts +2 -2
  80. package/src/project.ts +8 -1
  81. package/src/project_class.ts +136 -1
  82. package/src/project_internal.ts +90 -0
  83. package/src/project_types.ts +11 -1
  84. package/src/public_types.ts +1 -0
  85. package/src/public_types_mastering.ts +2 -0
  86. package/src/public_types_vocal_edit.ts +289 -0
  87. package/src/public_types_vocal_project.ts +69 -0
  88. package/src/realtime_engine.ts +86 -9
  89. package/src/repair_dereverb.ts +1 -0
  90. package/src/repair_noise.ts +1 -0
  91. package/src/sonare.js.d.ts +53 -0
  92. package/src/vocal_edit.ts +885 -0
  93. package/src/vocal_edit_worker.ts +832 -0
  94. package/src/vocal_edit_worker_client.ts +812 -0
  95. package/src/vocal_edit_worker_protocol.ts +206 -0
  96. package/src/vocal_project.ts +327 -0
  97. package/src/web_midi.ts +100 -5
  98. package/src/worklet/engine-mixer-facade.ts +2 -1
  99. package/src/worklet/engine-node.ts +47 -0
  100. package/src/worklet/engine-processor.ts +46 -1
  101. package/src/worklet/engine-register.ts +3 -0
  102. package/src/worklet/engine-strips.ts +31 -4
  103. package/src/worklet/engine.ts +27 -3
  104. package/src/worklet/guards.ts +26 -0
  105. package/src/worklet/messages.ts +59 -2
  106. package/src/worklet/protocol.ts +10 -0
  107. package/src/worklet.ts +2 -0
@@ -0,0 +1,206 @@
1
+ import type {
2
+ VocalAnalysis,
3
+ VocalApplyRequest,
4
+ VocalCapabilities,
5
+ VocalCreateRequest,
6
+ VocalEditResult,
7
+ VocalHistoryState,
8
+ VocalNotesResult,
9
+ VocalPitchEvaluation,
10
+ VocalRenderRequest,
11
+ VocalRenderResult,
12
+ VocalRestoreRequest,
13
+ VocalStateBytes,
14
+ VocalStateToken,
15
+ VocalUint64,
16
+ } from './public_types_vocal_edit';
17
+
18
+ export type VocalWorkerMutation =
19
+ | { kind: 'beginEdit'; expectedRevision?: VocalUint64 }
20
+ | { kind: 'apply'; request: VocalApplyRequest }
21
+ | { kind: 'commit'; expectedRevision: VocalUint64 }
22
+ | { kind: 'cancel' }
23
+ | { kind: 'undo'; expectedRevision?: VocalUint64 }
24
+ | { kind: 'redo'; expectedRevision?: VocalUint64 }
25
+ | { kind: 'notes' }
26
+ | { kind: 'analysis' }
27
+ | { kind: 'capabilities' }
28
+ | { kind: 'exportState' }
29
+ | { kind: 'outputLength' }
30
+ | { kind: 'history' }
31
+ | { kind: 'draftToken' }
32
+ | { kind: 'evaluatePitch'; noteId: number; draft?: boolean }
33
+ | {
34
+ kind: 'mapCoordinate';
35
+ noteId: number;
36
+ sample: number;
37
+ /** `false` maps source to destination; `true` maps destination to source. */
38
+ inverse: boolean;
39
+ draft?: boolean;
40
+ };
41
+
42
+ export type VocalWorkerCreateRequest =
43
+ | { kind: 'create'; request: VocalCreateRequest }
44
+ | { kind: 'restore'; request: VocalRestoreRequest };
45
+
46
+ export interface VocalWorkerCreateMessage {
47
+ type: 'sonare:vocal-create';
48
+ id: number;
49
+ sessionId: string;
50
+ clientIntentSequence: VocalUint64;
51
+ request: VocalWorkerCreateRequest;
52
+ cancelBuffer?: SharedArrayBuffer;
53
+ }
54
+
55
+ export interface VocalWorkerMutateMessage {
56
+ type: 'sonare:vocal-mutate';
57
+ id: number;
58
+ sessionId: string;
59
+ clientIntentSequence: VocalUint64;
60
+ baseToken?: VocalStateToken;
61
+ mutation: VocalWorkerMutation;
62
+ cancelBuffer?: SharedArrayBuffer;
63
+ }
64
+
65
+ export interface VocalWorkerPreviewMessage {
66
+ type: 'sonare:vocal-preview';
67
+ id: number;
68
+ sessionId: string;
69
+ clientIntentSequence: VocalUint64;
70
+ baseToken?: VocalStateToken;
71
+ request: VocalRenderRequest;
72
+ cancelBuffer?: SharedArrayBuffer;
73
+ }
74
+
75
+ export interface VocalWorkerDisposeMessage {
76
+ type: 'sonare:vocal-dispose';
77
+ id: number;
78
+ sessionId: string;
79
+ clientIntentSequence: VocalUint64;
80
+ }
81
+
82
+ export interface VocalWorkerCancelMessage {
83
+ type: 'sonare:vocal-cancel';
84
+ id: number;
85
+ sessionId: string;
86
+ clientIntentSequence: VocalUint64;
87
+ }
88
+
89
+ export type VocalWorkerRequestMessage =
90
+ | VocalWorkerCreateMessage
91
+ | VocalWorkerMutateMessage
92
+ | VocalWorkerPreviewMessage
93
+ | VocalWorkerDisposeMessage
94
+ | VocalWorkerCancelMessage;
95
+
96
+ export interface VocalWorkerCreateResult {
97
+ sessionId: string;
98
+ token: VocalStateToken;
99
+ notes: VocalNotesResult;
100
+ analysis: VocalAnalysis;
101
+ capabilities: VocalCapabilities;
102
+ }
103
+
104
+ export interface VocalWorkerResponseBase {
105
+ id: number;
106
+ sessionId: string;
107
+ clientIntentSequence: VocalUint64;
108
+ token?: VocalStateToken;
109
+ }
110
+
111
+ export type VocalWorkerResult =
112
+ | VocalWorkerCreateResult
113
+ | VocalEditResult
114
+ | VocalNotesResult
115
+ | VocalAnalysis
116
+ | VocalCapabilities
117
+ | VocalStateBytes
118
+ | VocalRenderResult
119
+ | VocalStateToken
120
+ | VocalHistoryState
121
+ | VocalPitchEvaluation
122
+ | number
123
+ | null;
124
+
125
+ export interface VocalWorkerResultMessage extends VocalWorkerResponseBase {
126
+ type: 'sonare:vocal-result';
127
+ result: VocalWorkerResult;
128
+ }
129
+
130
+ export interface VocalWorkerProgressMessage extends VocalWorkerResponseBase {
131
+ type: 'sonare:vocal-progress';
132
+ complete: boolean;
133
+ }
134
+
135
+ export interface VocalWorkerErrorMessage extends VocalWorkerResponseBase {
136
+ type: 'sonare:vocal-error';
137
+ error: {
138
+ name: string;
139
+ message: string;
140
+ code?: number;
141
+ codeName?: string;
142
+ reason?: number;
143
+ field?: string;
144
+ expected?: VocalUint64;
145
+ actual?: VocalUint64;
146
+ expectedText?: string;
147
+ actualText?: string;
148
+ };
149
+ }
150
+
151
+ export type VocalWorkerResponseMessage =
152
+ | VocalWorkerResultMessage
153
+ | VocalWorkerProgressMessage
154
+ | VocalWorkerErrorMessage;
155
+
156
+ export function tokenEquals(
157
+ a: VocalStateToken | undefined,
158
+ b: VocalStateToken | undefined,
159
+ ): boolean {
160
+ if (!a || !b) {
161
+ return a === b;
162
+ }
163
+ return (
164
+ a.sessionEpoch === b.sessionEpoch &&
165
+ a.revision === b.revision &&
166
+ a.draftId === b.draftId &&
167
+ a.generation === b.generation &&
168
+ a.requestId === b.requestId &&
169
+ a.profileId === b.profileId
170
+ );
171
+ }
172
+
173
+ /** Compare the mutable session state portion of a token.
174
+ *
175
+ * Render results add request/profile metadata to the state token. That
176
+ * metadata identifies the render artifact, so it must not make a snapshot's
177
+ * state appear different from the session state used to create it.
178
+ */
179
+ export function stateTokenEquals(
180
+ a: VocalStateToken | undefined,
181
+ b: VocalStateToken | undefined,
182
+ ): boolean {
183
+ if (!a || !b) {
184
+ return a === b;
185
+ }
186
+ return (
187
+ a.sessionEpoch === b.sessionEpoch &&
188
+ a.revision === b.revision &&
189
+ a.draftId === b.draftId &&
190
+ a.generation === b.generation &&
191
+ a.profileId === b.profileId
192
+ );
193
+ }
194
+
195
+ /** Decimal sequence comparison without converting a uint64 to Number. */
196
+ export function compareUint64(a: VocalUint64, b: VocalUint64): number {
197
+ const left = a.replace(/^0+(?=\d)/, '');
198
+ const right = b.replace(/^0+(?=\d)/, '');
199
+ if (left.length !== right.length) {
200
+ return left.length < right.length ? -1 : 1;
201
+ }
202
+ if (left === right) {
203
+ return 0;
204
+ }
205
+ return left < right ? -1 : 1;
206
+ }
@@ -0,0 +1,327 @@
1
+ import type { VocalStateToken, VocalUint64 } from './public_types_vocal_edit';
2
+ import type {
3
+ ProjectVocalEditApplyRequest,
4
+ ProjectVocalEditApplyResult,
5
+ ProjectVocalEditDependency,
6
+ ProjectVocalOriginalSource,
7
+ ProjectVocalReason,
8
+ ProjectVocalRehydrateItem,
9
+ } from './public_types_vocal_project';
10
+ import { assertSampleRate } from './validation';
11
+
12
+ /** The embind Project object is deliberately structural here. */
13
+ export interface VocalProjectNative {
14
+ applyVocalEdit: (request: unknown) => unknown;
15
+ getVocalEditDependencies: () => unknown;
16
+ rehydrateVocalEdits: (originals: unknown, cancel?: unknown) => unknown;
17
+ }
18
+
19
+ const UINT32_MAX = 0xffff_ffff;
20
+ const PROJECT_ID_MAX = UINT32_MAX - 1;
21
+ const UINT64_MAX = '18446744073709551615';
22
+
23
+ function requireObject(value: unknown, field: string): Record<string, unknown> {
24
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
25
+ throw new TypeError(`${field} must be an object`);
26
+ }
27
+ return value as Record<string, unknown>;
28
+ }
29
+
30
+ function uint32(value: unknown, field: string, allowZero = true): number {
31
+ if (
32
+ typeof value !== 'number' ||
33
+ !Number.isSafeInteger(value) ||
34
+ value < (allowZero ? 0 : 1) ||
35
+ value > PROJECT_ID_MAX
36
+ ) {
37
+ throw new RangeError(`${field} must be a uint32 project id`);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function nativeUint32(value: unknown, field: string): number {
43
+ if (
44
+ typeof value !== 'number' ||
45
+ !Number.isSafeInteger(value) ||
46
+ value < 0 ||
47
+ value > UINT32_MAX
48
+ ) {
49
+ throw new RangeError(`${field} must be a native uint32`);
50
+ }
51
+ return value;
52
+ }
53
+
54
+ function nativeInt64(value: unknown, field: string): number {
55
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
56
+ throw new RangeError(`${field} must be a safe integer`);
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function positiveSampleCount(value: unknown, field: string): number {
62
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
63
+ throw new RangeError(`${field} must be a positive safe integer`);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ function finite(value: unknown, field: string): number {
69
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
70
+ throw new RangeError(`${field} must be finite`);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ function decimalUint64(value: unknown, field: string): VocalUint64 {
76
+ if (typeof value !== 'string' || !/^(0|[1-9]\d*)$/.test(value)) {
77
+ throw new RangeError(`${field} must be a canonical decimal uint64 string`);
78
+ }
79
+ if (
80
+ value.length > UINT64_MAX.length ||
81
+ (value.length === UINT64_MAX.length && value > UINT64_MAX)
82
+ ) {
83
+ throw new RangeError(`${field} must fit uint64`);
84
+ }
85
+ return value;
86
+ }
87
+
88
+ function hexSha256(value: unknown, field: string): string {
89
+ if (typeof value !== 'string' || !/^[0-9a-fA-F]{64}$/.test(value)) {
90
+ throw new TypeError(`${field} must be 64 hexadecimal characters`);
91
+ }
92
+ return value.toLowerCase();
93
+ }
94
+
95
+ function finiteMono(value: unknown, field: string): Float32Array {
96
+ if (!(value instanceof Float32Array) || value.length === 0) {
97
+ throw new TypeError(`${field} must be a non-empty Float32Array`);
98
+ }
99
+ for (let index = 0; index < value.length; index += 1) {
100
+ if (!Number.isFinite(value[index])) {
101
+ throw new RangeError(`${field} must contain only finite samples`);
102
+ }
103
+ }
104
+ return new Float32Array(value);
105
+ }
106
+
107
+ function sve1(value: unknown): Uint8Array {
108
+ if (!(value instanceof Uint8Array) || value.length === 0) {
109
+ throw new TypeError('sve1 must be a non-empty Uint8Array');
110
+ }
111
+ return new Uint8Array(value);
112
+ }
113
+
114
+ function copyToken(value: unknown): VocalStateToken {
115
+ const source = requireObject(value, 'renderToken');
116
+ return {
117
+ sessionEpoch: decimalUint64(source.sessionEpoch, 'renderToken.sessionEpoch'),
118
+ revision: decimalUint64(source.revision, 'renderToken.revision'),
119
+ draftId: decimalUint64(source.draftId, 'renderToken.draftId'),
120
+ generation: decimalUint64(source.generation, 'renderToken.generation'),
121
+ requestId: decimalUint64(source.requestId, 'renderToken.requestId'),
122
+ profileId: uint32(source.profileId, 'renderToken.profileId'),
123
+ };
124
+ }
125
+
126
+ function copyApplyRequest(request: ProjectVocalEditApplyRequest): ProjectVocalEditApplyRequest {
127
+ const source = requireObject(request, 'request');
128
+ const renderedMono = finiteMono(source.renderedMono, 'renderedMono');
129
+ const expectedSourceSampleCount = positiveSampleCount(
130
+ source.expectedSourceSampleCount,
131
+ 'expectedSourceSampleCount',
132
+ );
133
+ if (renderedMono.length !== expectedSourceSampleCount) {
134
+ throw new RangeError(
135
+ `renderedMono length must equal expectedSourceSampleCount (${renderedMono.length} != ${expectedSourceSampleCount})`,
136
+ );
137
+ }
138
+ const expectedSourceSampleRate = finite(
139
+ source.expectedSourceSampleRate,
140
+ 'expectedSourceSampleRate',
141
+ );
142
+ assertSampleRate('Project.applyVocalEdit', expectedSourceSampleRate);
143
+ const renderedSampleRate = finite(source.renderedSampleRate, 'renderedSampleRate');
144
+ assertSampleRate('Project.applyVocalEdit', renderedSampleRate, 'renderedSampleRate');
145
+ if (renderedSampleRate !== expectedSourceSampleRate) {
146
+ throw new RangeError('renderedSampleRate must equal expectedSourceSampleRate');
147
+ }
148
+ const renderedStartSample = source.renderedStartSample ?? 0;
149
+ if (renderedStartSample !== 0) {
150
+ throw new RangeError('renderedStartSample must be zero for the v1 Project vocal API');
151
+ }
152
+ const renderToken = copyToken(source.renderToken);
153
+ if (renderToken.draftId !== '0' || renderToken.generation !== '0') {
154
+ throw new RangeError('renderToken must be a committed session token');
155
+ }
156
+ return {
157
+ clipId: uint32(source.clipId, 'clipId', false),
158
+ takeId: uint32(source.takeId ?? 0, 'takeId'),
159
+ expectedSourceId: uint32(source.expectedSourceId, 'expectedSourceId', false),
160
+ expectedSourceSampleRate,
161
+ expectedSourceSampleCount,
162
+ expectedSourceSha256: hexSha256(source.expectedSourceSha256, 'expectedSourceSha256'),
163
+ expectedClipLengthPpq: finite(source.expectedClipLengthPpq, 'expectedClipLengthPpq'),
164
+ expectedSourceOffsetPpq: finite(source.expectedSourceOffsetPpq, 'expectedSourceOffsetPpq'),
165
+ renderedMono,
166
+ renderedSampleRate,
167
+ renderedStartSample: 0,
168
+ renderToken,
169
+ sve1: sve1(source.sve1),
170
+ };
171
+ }
172
+
173
+ function copyApplyResult(value: unknown): ProjectVocalEditApplyResult {
174
+ const source = requireObject(value, 'applyVocalEdit result');
175
+ return {
176
+ clipId: uint32(source.clipId, 'result.clipId', false),
177
+ takeId: uint32(source.takeId, 'result.takeId'),
178
+ originalSourceId: uint32(source.originalSourceId, 'result.originalSourceId', false),
179
+ derivedSourceId: uint32(source.derivedSourceId, 'result.derivedSourceId', false),
180
+ committedRevision: decimalUint64(source.committedRevision, 'result.committedRevision'),
181
+ profileId: uint32(source.profileId, 'result.profileId'),
182
+ derivedSourceSha256: hexSha256(source.derivedSourceSha256, 'result.derivedSourceSha256'),
183
+ sidecarKey:
184
+ typeof source.sidecarKey === 'string'
185
+ ? source.sidecarKey
186
+ : (() => {
187
+ throw new TypeError('result.sidecarKey must be a string');
188
+ })(),
189
+ };
190
+ }
191
+
192
+ function copyDependency(value: unknown): ProjectVocalEditDependency {
193
+ const source = requireObject(value, 'vocal edit dependency');
194
+ const reason = nativeUint32(source.reason, 'dependency.reason');
195
+ return {
196
+ clipId: nativeUint32(source.clipId, 'dependency.clipId'),
197
+ takeId: nativeUint32(source.takeId, 'dependency.takeId'),
198
+ originalSourceId: nativeUint32(source.originalSourceId, 'dependency.originalSourceId'),
199
+ derivedSourceId: nativeUint32(source.derivedSourceId, 'dependency.derivedSourceId'),
200
+ sourceSampleRate: nativeUint32(source.sourceSampleRate, 'dependency.sourceSampleRate'),
201
+ profileId: nativeUint32(source.profileId, 'dependency.profileId'),
202
+ sourceSampleCount: nativeInt64(source.sourceSampleCount, 'dependency.sourceSampleCount'),
203
+ committedRevision: decimalUint64(source.committedRevision, 'dependency.committedRevision'),
204
+ originalSourceSha256: hexSha256(source.originalSourceSha256, 'dependency.originalSourceSha256'),
205
+ derivedSourceSha256: hexSha256(source.derivedSourceSha256, 'dependency.derivedSourceSha256'),
206
+ originalPcmAvailable: source.originalPcmAvailable === true,
207
+ derivedPcmAvailable: source.derivedPcmAvailable === true,
208
+ reason: reason as ProjectVocalReason | number,
209
+ sidecarKey:
210
+ typeof source.sidecarKey === 'string'
211
+ ? source.sidecarKey
212
+ : (() => {
213
+ throw new TypeError('dependency.sidecarKey must be a string');
214
+ })(),
215
+ };
216
+ }
217
+
218
+ function copyRehydrateItem(value: unknown): ProjectVocalRehydrateItem {
219
+ const source = requireObject(value, 'vocal rehydrate item');
220
+ const status = source.status;
221
+ if (typeof status !== 'number' || !Number.isSafeInteger(status) || status < 0 || status > 2) {
222
+ throw new RangeError('rehydrate.status must be one of 0, 1, or 2');
223
+ }
224
+ const reason = nativeUint32(source.reason, 'rehydrate.reason');
225
+ return {
226
+ clipId: nativeUint32(source.clipId, 'rehydrate.clipId'),
227
+ takeId: nativeUint32(source.takeId, 'rehydrate.takeId'),
228
+ derivedSourceId: nativeUint32(source.derivedSourceId, 'rehydrate.derivedSourceId'),
229
+ status: status as ProjectVocalRehydrateItem['status'],
230
+ reason: reason as ProjectVocalReason | number,
231
+ };
232
+ }
233
+
234
+ function copyOriginalSources(
235
+ originals: readonly ProjectVocalOriginalSource[],
236
+ ): ProjectVocalOriginalSource[] {
237
+ if (!Array.isArray(originals)) {
238
+ throw new TypeError('originals must be an array');
239
+ }
240
+ const seen = new Set<number>();
241
+ return originals.map((original, index) => {
242
+ const source = requireObject(original, `originals[${index}]`);
243
+ const sourceId = uint32(source.sourceId, `originals[${index}].sourceId`, false);
244
+ if (seen.has(sourceId)) {
245
+ throw new RangeError(`originals contains duplicate sourceId ${sourceId}`);
246
+ }
247
+ seen.add(sourceId);
248
+ const sampleRate = finite(source.sampleRate, `originals[${index}].sampleRate`);
249
+ assertSampleRate('Project.rehydrateVocalEdits', sampleRate, `originals[${index}].sampleRate`);
250
+ return {
251
+ sourceId,
252
+ mono: finiteMono(source.mono, `originals[${index}].mono`),
253
+ sampleRate,
254
+ };
255
+ });
256
+ }
257
+
258
+ /** Apply a complete committed vocal render to a native Project object. */
259
+ export function projectApplyVocalEdit(
260
+ native: VocalProjectNative,
261
+ request: ProjectVocalEditApplyRequest,
262
+ ): ProjectVocalEditApplyResult {
263
+ const normalized = copyApplyRequest(request);
264
+ return copyApplyResult(native.applyVocalEdit(normalized));
265
+ }
266
+
267
+ /** Return copied vocal sidecar dependencies in native storage order. */
268
+ export function projectGetVocalEditDependencies(
269
+ native: VocalProjectNative,
270
+ ): ProjectVocalEditDependency[] {
271
+ const value = native.getVocalEditDependencies();
272
+ if (!Array.isArray(value)) {
273
+ throw new TypeError('native vocal dependencies must be an array');
274
+ }
275
+ return value.map(copyDependency);
276
+ }
277
+
278
+ /**
279
+ * Rehydrate unresolved vocal PCM without creating a Project history entry.
280
+ * Any truthy return from `cancel` cancels the call.
281
+ */
282
+ export function projectRehydrateVocalEdits(
283
+ native: VocalProjectNative,
284
+ originals: readonly ProjectVocalOriginalSource[],
285
+ cancel?: () => boolean,
286
+ ): ProjectVocalRehydrateItem[] {
287
+ if (cancel !== undefined && typeof cancel !== 'function') {
288
+ throw new TypeError('cancel must be a function');
289
+ }
290
+ const normalized = copyOriginalSources(originals);
291
+ let callbackFailed = false;
292
+ let callbackFailure: unknown;
293
+ const guardedCancel =
294
+ cancel === undefined
295
+ ? undefined
296
+ : () => {
297
+ try {
298
+ return cancel();
299
+ } catch (error) {
300
+ callbackFailed = true;
301
+ callbackFailure = error;
302
+ // Stop native work without allowing a JS exception to cross the C ABI.
303
+ return true;
304
+ }
305
+ };
306
+ let value: unknown;
307
+ try {
308
+ value = native.rehydrateVocalEdits(normalized, guardedCancel);
309
+ } catch (error) {
310
+ if (callbackFailed) {
311
+ throw callbackFailure;
312
+ }
313
+ throw error;
314
+ }
315
+ if (callbackFailed) {
316
+ throw callbackFailure;
317
+ }
318
+ // The adapter reports a throwing callback as data instead of throwing
319
+ // through C++ frames, whose destructors a JS exception would skip.
320
+ if (value !== null && typeof value === 'object' && 'cancelFailure' in value) {
321
+ throw (value as { cancelFailure: unknown }).cancelFailure;
322
+ }
323
+ if (!Array.isArray(value)) {
324
+ throw new TypeError('native vocal rehydrate result must be an array');
325
+ }
326
+ return value.map(copyRehydrateItem);
327
+ }
package/src/web_midi.ts CHANGED
@@ -31,6 +31,21 @@ export interface WebMidiEngine {
31
31
  value: number,
32
32
  time?: number,
33
33
  ): void;
34
+ pushMidiInputPitchBend?(group: number, channel: number, bend14: number, time?: number): void;
35
+ pushMidiInputChannelPressure?(
36
+ group: number,
37
+ channel: number,
38
+ pressure: number,
39
+ time?: number,
40
+ ): void;
41
+ pushMidiInputPolyPressure?(
42
+ group: number,
43
+ channel: number,
44
+ note: number,
45
+ pressure: number,
46
+ time?: number,
47
+ ): void;
48
+ pushMidiInputUmp?(words: Uint32Array | readonly number[], time?: number): void;
34
49
  }
35
50
 
36
51
  type MidiInputState = 'connected' | 'disconnected';
@@ -285,8 +300,8 @@ function dispatchMidiMessage(
285
300
  return 0;
286
301
  }
287
302
  const first = data[0];
288
- if (first > 0xff) {
289
- dispatchUmpMessage(
303
+ if (first > 0xff || data instanceof Uint32Array) {
304
+ dispatchUmpMessages(
290
305
  engine,
291
306
  data,
292
307
  timestampToSamples?.(event.receivedTime ?? event.timeStamp ?? 0) ?? 0,
@@ -312,8 +327,12 @@ function dispatchMidiMessage(
312
327
  }
313
328
 
314
329
  const a = readU7(data, offset);
315
- const b = readU7(data, offset + 1);
316
- if (a < 0 || b < 0) {
330
+ if (a < 0) {
331
+ return status;
332
+ }
333
+ const oneDataByteMessage = message === 0xc0 || message === 0xd0;
334
+ const b = oneDataByteMessage ? -1 : readU7(data, offset + 1);
335
+ if (!oneDataByteMessage && b < 0) {
317
336
  return status;
318
337
  }
319
338
 
@@ -331,20 +350,56 @@ function dispatchMidiMessage(
331
350
  }
332
351
  } else if (message === 0xb0 && b >= 0) {
333
352
  engine.pushMidiInputCc(group, channel, a, b, portTimeSamples);
353
+ } else if (message === 0xa0 && b >= 0) {
354
+ engine.pushMidiInputPolyPressure?.(group, channel, a, b, portTimeSamples);
355
+ } else if (message === 0xd0) {
356
+ engine.pushMidiInputChannelPressure?.(group, channel, a, portTimeSamples);
357
+ } else if (message === 0xe0 && b >= 0) {
358
+ engine.pushMidiInputPitchBend?.(group, channel, a | (b << 7), portTimeSamples);
334
359
  }
335
360
 
336
361
  return status;
337
362
  }
338
363
 
339
- function dispatchUmpMessage(
364
+ function dispatchUmpMessages(
340
365
  engine: WebMidiEngine,
341
366
  words: ArrayLike<number>,
342
367
  portTimeSamples: number,
368
+ ): void {
369
+ let offset = 0;
370
+ while (offset < words.length) {
371
+ const word0 = words[offset] >>> 0;
372
+ const wordCount = umpWordCount(word0 >>> 28);
373
+ if (words.length - offset < wordCount) {
374
+ return;
375
+ }
376
+ const packet = new Uint32Array(wordCount);
377
+ for (let index = 0; index < wordCount; index++) {
378
+ packet[index] = words[offset + index] >>> 0;
379
+ }
380
+ dispatchUmpMessage(engine, packet, portTimeSamples);
381
+ offset += wordCount;
382
+ }
383
+ }
384
+
385
+ function dispatchUmpMessage(
386
+ engine: WebMidiEngine,
387
+ words: Uint32Array,
388
+ portTimeSamples: number,
343
389
  ): void {
344
390
  const word0 = words[0] >>> 0;
345
391
  const messageType = word0 >>> 28;
346
392
  const group = (word0 >>> 24) & 0x0f;
347
393
 
394
+ // Data packets (MT 0x3/0x5) need the owned SysEx path; raw input refuses them.
395
+ if (messageType === 0x3 || messageType === 0x5) {
396
+ return;
397
+ }
398
+ if (engine.pushMidiInputUmp) {
399
+ engine.pushMidiInputUmp(words, portTimeSamples);
400
+ return;
401
+ }
402
+
348
403
  if (messageType === 0x2) {
349
404
  const status = (word0 >>> 16) & 0xff;
350
405
  const message = status & 0xf0;
@@ -361,6 +416,12 @@ function dispatchUmpMessage(
361
416
  }
362
417
  } else if (message === 0xb0) {
363
418
  engine.pushMidiInputCc(group, channel, a, b, portTimeSamples);
419
+ } else if (message === 0xa0) {
420
+ engine.pushMidiInputPolyPressure?.(group, channel, a, b, portTimeSamples);
421
+ } else if (message === 0xd0) {
422
+ engine.pushMidiInputChannelPressure?.(group, channel, a, portTimeSamples);
423
+ } else if (message === 0xe0) {
424
+ engine.pushMidiInputPitchBend?.(group, channel, a | (b << 7), portTimeSamples);
364
425
  }
365
426
  return;
366
427
  }
@@ -380,10 +441,44 @@ function dispatchUmpMessage(
380
441
  engine.pushMidiInputNoteOn(group, channel, data1, velocity, portTimeSamples);
381
442
  } else if (status === 0xb) {
382
443
  engine.pushMidiInputCc(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
444
+ } else if (status === 0xa) {
445
+ engine.pushMidiInputPolyPressure?.(
446
+ group,
447
+ channel,
448
+ data1,
449
+ (word1 >>> 25) & 0x7f,
450
+ portTimeSamples,
451
+ );
452
+ } else if (status === 0xd) {
453
+ engine.pushMidiInputChannelPressure?.(group, channel, (word1 >>> 25) & 0x7f, portTimeSamples);
454
+ } else if (status === 0xe) {
455
+ engine.pushMidiInputPitchBend?.(group, channel, word1 >>> 18, portTimeSamples);
383
456
  }
384
457
  }
385
458
  }
386
459
 
460
+ function umpWordCount(messageType: number): number {
461
+ switch (messageType & 0x0f) {
462
+ case 0x0:
463
+ case 0x1:
464
+ case 0x2:
465
+ case 0x6:
466
+ case 0x7:
467
+ return 1;
468
+ case 0x3:
469
+ case 0x4:
470
+ case 0x8:
471
+ case 0x9:
472
+ case 0xa:
473
+ return 2;
474
+ case 0xb:
475
+ case 0xc:
476
+ return 3;
477
+ default:
478
+ return 4;
479
+ }
480
+ }
481
+
387
482
  function readU7(data: ArrayLike<number>, index: number): number {
388
483
  if (index >= data.length) {
389
484
  return -1;
@@ -727,7 +727,8 @@ export function setTrackOutputBus(
727
727
  /**
728
728
  * Keys one insert of a lane strip from another lane's post-strip pre-fader
729
729
  * audio (ducking/sidechainRouter inserts). sourceTarget null removes the
730
- * binding.
730
+ * binding. The binding is cached for resync only once the engine has accepted
731
+ * it, so a refused (self-keyed or cyclic) binding throws and is never replayed.
731
732
  */
732
733
  export function setLaneSidechain(
733
734
  ctx: EngineMixerContext,