@libraz/libsonare 1.5.3 → 1.5.4
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.
- package/README.md +69 -740
- package/dist/index.d.ts +15 -9
- package/dist/index.js +114 -10
- package/dist/index.js.map +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.js +82 -5
- package/dist/worklet.js.map +1 -1
- package/package.json +14 -3
- package/src/codes.ts +27 -4
- package/src/effects_transform.ts +20 -14
- package/src/feature_music.ts +14 -4
- package/src/metering.ts +4 -2
- package/src/project_class.ts +13 -0
- package/src/project_internal.ts +2 -0
- package/src/realtime_voice_changer.ts +79 -3
- package/src/sonare.js.d.ts +4 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libraz/libsonare",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
|
-
"description": "Audio analysis
|
|
6
|
+
"description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
@@ -47,6 +47,17 @@
|
|
|
47
47
|
"bpm",
|
|
48
48
|
"key",
|
|
49
49
|
"tempo",
|
|
50
|
+
"chords",
|
|
51
|
+
"audio-processing",
|
|
52
|
+
"dsp",
|
|
53
|
+
"mastering",
|
|
54
|
+
"mixing",
|
|
55
|
+
"loudness",
|
|
56
|
+
"lufs",
|
|
57
|
+
"midi",
|
|
58
|
+
"synthesizer",
|
|
59
|
+
"soundfont",
|
|
60
|
+
"room-acoustics",
|
|
50
61
|
"wasm",
|
|
51
62
|
"webassembly"
|
|
52
63
|
],
|
|
@@ -59,7 +70,7 @@
|
|
|
59
70
|
"bugs": {
|
|
60
71
|
"url": "https://github.com/libraz/libsonare/issues"
|
|
61
72
|
},
|
|
62
|
-
"homepage": "https://
|
|
73
|
+
"homepage": "https://libsonare.libraz.net",
|
|
63
74
|
"engines": {
|
|
64
75
|
"node": ">=18.0.0"
|
|
65
76
|
},
|
package/src/codes.ts
CHANGED
|
@@ -20,6 +20,8 @@ export function panLawCode(panLaw: PanLaw | number): number {
|
|
|
20
20
|
return panLaw;
|
|
21
21
|
}
|
|
22
22
|
switch (panLaw) {
|
|
23
|
+
case 'const3dB':
|
|
24
|
+
return 0;
|
|
23
25
|
case 'const4.5dB':
|
|
24
26
|
return 1;
|
|
25
27
|
case 'const6dB':
|
|
@@ -27,7 +29,7 @@ export function panLawCode(panLaw: PanLaw | number): number {
|
|
|
27
29
|
case 'linear0dB':
|
|
28
30
|
return 3;
|
|
29
31
|
default:
|
|
30
|
-
|
|
32
|
+
throw new Error(`Invalid pan law: ${panLaw}`);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -36,6 +38,8 @@ export function panModeCode(panMode: PanMode | number): number {
|
|
|
36
38
|
return panMode;
|
|
37
39
|
}
|
|
38
40
|
switch (panMode) {
|
|
41
|
+
case 'balance':
|
|
42
|
+
return 0;
|
|
39
43
|
case 'stereoPan':
|
|
40
44
|
case 'stereo-pan':
|
|
41
45
|
return 1;
|
|
@@ -43,19 +47,38 @@ export function panModeCode(panMode: PanMode | number): number {
|
|
|
43
47
|
case 'dual-pan':
|
|
44
48
|
return 2;
|
|
45
49
|
default:
|
|
46
|
-
|
|
50
|
+
throw new Error(`Invalid pan mode: ${panMode}`);
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export function meterTapCode(tap: MeterTap | number): number {
|
|
51
|
-
|
|
55
|
+
if (typeof tap === 'number') {
|
|
56
|
+
return tap;
|
|
57
|
+
}
|
|
58
|
+
switch (tap) {
|
|
59
|
+
case 'preFader':
|
|
60
|
+
return 0;
|
|
61
|
+
case 'postFader':
|
|
62
|
+
return 1;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Invalid meter tap: ${tap}`);
|
|
65
|
+
}
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
export function sendTimingCode(timing: SendTiming | number): number {
|
|
55
69
|
// Mirrors SonareSendTiming: post-fader is 0 (so an omitted/zeroed value is
|
|
56
70
|
// post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
|
|
71
|
+
// An unknown string is rejected rather than silently routed to post-fader,
|
|
72
|
+
// matching the sibling enum-code helpers and Node's sendTimingValue.
|
|
57
73
|
if (typeof timing === 'number') {
|
|
58
74
|
return timing;
|
|
59
75
|
}
|
|
60
|
-
|
|
76
|
+
switch (timing) {
|
|
77
|
+
case 'postFader':
|
|
78
|
+
return 0;
|
|
79
|
+
case 'preFader':
|
|
80
|
+
return 1;
|
|
81
|
+
default:
|
|
82
|
+
throw new Error(`Invalid send timing: ${timing}`);
|
|
83
|
+
}
|
|
61
84
|
}
|
package/src/effects_transform.ts
CHANGED
|
@@ -34,13 +34,13 @@ export interface PercussiveRequest extends ValidateOptions {
|
|
|
34
34
|
|
|
35
35
|
export interface TimeStretchRequest extends ValidateOptions {
|
|
36
36
|
samples: Float32Array;
|
|
37
|
-
sampleRate
|
|
37
|
+
sampleRate?: number;
|
|
38
38
|
rate: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface PitchShiftRequest extends ValidateOptions {
|
|
42
42
|
samples: Float32Array;
|
|
43
|
-
sampleRate
|
|
43
|
+
sampleRate?: number;
|
|
44
44
|
semitones: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -79,7 +79,7 @@ export interface NoteMoveRequest extends NoteMoveOptions, ValidateOptions {
|
|
|
79
79
|
|
|
80
80
|
export interface NormalizeRequest extends ValidateOptions {
|
|
81
81
|
samples: Float32Array;
|
|
82
|
-
sampleRate
|
|
82
|
+
sampleRate?: number;
|
|
83
83
|
targetDb?: number;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -177,7 +177,7 @@ export function percussive(
|
|
|
177
177
|
* Time-stretch audio without changing pitch.
|
|
178
178
|
*
|
|
179
179
|
* @param samples - Audio samples (mono, float32)
|
|
180
|
-
* @param sampleRate - Sample rate in Hz
|
|
180
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
181
181
|
* @param rate - Time stretch rate (0.5 = double duration, 2.0 = half duration)
|
|
182
182
|
* @returns Time-stretched audio
|
|
183
183
|
*/
|
|
@@ -196,17 +196,17 @@ export function timeStretch(
|
|
|
196
196
|
): Float32Array {
|
|
197
197
|
const request: TimeStretchRequest =
|
|
198
198
|
samples instanceof Float32Array
|
|
199
|
-
? { samples, sampleRate
|
|
199
|
+
? { samples, sampleRate, rate: rate as number, ...options }
|
|
200
200
|
: samples;
|
|
201
201
|
assertSamples('timeStretch', request.samples, request.validate !== false);
|
|
202
|
-
return requireModule().timeStretch(request.samples, request.sampleRate, request.rate);
|
|
202
|
+
return requireModule().timeStretch(request.samples, request.sampleRate ?? 22050, request.rate);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* Pitch-shift audio without changing duration.
|
|
207
207
|
*
|
|
208
208
|
* @param samples - Audio samples (mono, float32)
|
|
209
|
-
* @param sampleRate - Sample rate in Hz
|
|
209
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
210
210
|
* @param semitones - Pitch shift in semitones (+12 = one octave up, -12 = one octave down)
|
|
211
211
|
* @returns Pitch-shifted audio
|
|
212
212
|
*/
|
|
@@ -225,10 +225,14 @@ export function pitchShift(
|
|
|
225
225
|
): Float32Array {
|
|
226
226
|
const request: PitchShiftRequest =
|
|
227
227
|
samples instanceof Float32Array
|
|
228
|
-
? { samples, sampleRate
|
|
228
|
+
? { samples, sampleRate, semitones: semitones as number, ...options }
|
|
229
229
|
: samples;
|
|
230
230
|
assertSamples('pitchShift', request.samples, request.validate !== false);
|
|
231
|
-
return requireModule().pitchShift(
|
|
231
|
+
return requireModule().pitchShift(
|
|
232
|
+
request.samples,
|
|
233
|
+
request.sampleRate ?? 22050,
|
|
234
|
+
request.semitones,
|
|
235
|
+
);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
/**
|
|
@@ -459,7 +463,7 @@ export function noteMove(
|
|
|
459
463
|
* Normalize audio to target peak level.
|
|
460
464
|
*
|
|
461
465
|
* @param samples - Audio samples (mono, float32)
|
|
462
|
-
* @param sampleRate - Sample rate in Hz
|
|
466
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
463
467
|
* @param targetDb - Target peak level in dB (default: 0 dB = full scale)
|
|
464
468
|
* @returns Normalized audio
|
|
465
469
|
*/
|
|
@@ -477,11 +481,13 @@ export function normalize(
|
|
|
477
481
|
options: ValidateOptions = {},
|
|
478
482
|
): Float32Array {
|
|
479
483
|
const request: NormalizeRequest =
|
|
480
|
-
samples instanceof Float32Array
|
|
481
|
-
? { samples, sampleRate: sampleRate as number, targetDb, ...options }
|
|
482
|
-
: samples;
|
|
484
|
+
samples instanceof Float32Array ? { samples, sampleRate, targetDb, ...options } : samples;
|
|
483
485
|
assertSamples('normalize', request.samples, request.validate !== false);
|
|
484
|
-
return requireModule().normalize(
|
|
486
|
+
return requireModule().normalize(
|
|
487
|
+
request.samples,
|
|
488
|
+
request.sampleRate ?? 22050,
|
|
489
|
+
request.targetDb ?? 0.0,
|
|
490
|
+
);
|
|
485
491
|
}
|
|
486
492
|
|
|
487
493
|
/**
|
package/src/feature_music.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ErrorCode, SonareError } from './errors';
|
|
1
2
|
import { getSonareModule } from './module_state';
|
|
2
3
|
import type {
|
|
3
4
|
AnalyzeSectionsOptions,
|
|
@@ -614,10 +615,15 @@ export function analyzeMelody(
|
|
|
614
615
|
const fmax = options.fmax ?? 2093.0;
|
|
615
616
|
validateFrequencyBounds('analyzeMelody', fmin, fmax);
|
|
616
617
|
// The melody tracker's fmin is a YIN pitch floor: 0 is meaningless, and the
|
|
617
|
-
// flat C ABI (sonare_analyze_melody) rejects it
|
|
618
|
-
// guards fmin >= 0, so enforce strict positivity
|
|
618
|
+
// flat C ABI (sonare_analyze_melody) rejects it with InvalidParameter.
|
|
619
|
+
// validateFrequencyBounds only guards fmin >= 0, so enforce strict positivity
|
|
620
|
+
// here and report the same branded error the Node/Python surfaces raise.
|
|
619
621
|
if (fmin <= 0) {
|
|
620
|
-
throw new
|
|
622
|
+
throw new SonareError(
|
|
623
|
+
ErrorCode.InvalidParameter,
|
|
624
|
+
'InvalidParameter',
|
|
625
|
+
'analyzeMelody: fmin must be positive',
|
|
626
|
+
);
|
|
621
627
|
}
|
|
622
628
|
validatePositiveIntegers('analyzeMelody', {
|
|
623
629
|
frameLength: options.frameLength ?? 2048,
|
|
@@ -626,7 +632,11 @@ export function analyzeMelody(
|
|
|
626
632
|
const threshold = options.threshold ?? 0.1;
|
|
627
633
|
assertFiniteScalar('analyzeMelody', threshold, 'threshold');
|
|
628
634
|
if (threshold <= 0) {
|
|
629
|
-
throw new
|
|
635
|
+
throw new SonareError(
|
|
636
|
+
ErrorCode.InvalidParameter,
|
|
637
|
+
'InvalidParameter',
|
|
638
|
+
'analyzeMelody: threshold must be positive',
|
|
639
|
+
);
|
|
630
640
|
}
|
|
631
641
|
return requireModule().analyzeMelody(
|
|
632
642
|
samples,
|
package/src/metering.ts
CHANGED
|
@@ -383,8 +383,10 @@ export function meteringStereoCorrelation(
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
/**
|
|
386
|
-
* Side / mid energy ratio
|
|
387
|
-
*
|
|
386
|
+
* Side / mid energy ratio in `[0, +Infinity)`: 0 = pure mono, ~1 = wide stereo,
|
|
387
|
+
* larger = increasingly decorrelated / out-of-phase. The value is unbounded and
|
|
388
|
+
* returns `Infinity` when the mid channel is silent (a mono-collapsed / fully
|
|
389
|
+
* out-of-phase signal).
|
|
388
390
|
*/
|
|
389
391
|
export function meteringStereoWidth(request: MeteringStereoRequest): number;
|
|
390
392
|
export function meteringStereoWidth(
|
package/src/project_class.ts
CHANGED
|
@@ -427,6 +427,19 @@ export class Project {
|
|
|
427
427
|
this.native.redo();
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/** Clear the undo/redo history without changing the current project state. */
|
|
431
|
+
clearHistory(): void {
|
|
432
|
+
this.native.clearHistory();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Cap the undo history depth (clamped to >= 1); evicts oldest entries beyond the cap. */
|
|
436
|
+
setMaxUndoDepth(depth: number): void {
|
|
437
|
+
if (!Number.isInteger(depth) || depth < 1) {
|
|
438
|
+
throw new RangeError('Project.setMaxUndoDepth: depth must be an integer >= 1');
|
|
439
|
+
}
|
|
440
|
+
this.native.setMaxUndoDepth(depth);
|
|
441
|
+
}
|
|
442
|
+
|
|
430
443
|
/** Replace a MIDI clip's entire event list. */
|
|
431
444
|
setMidiEvents(
|
|
432
445
|
clipId: number,
|
package/src/project_internal.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface WasmProject {
|
|
|
58
58
|
setTrackPan: (trackId: number, pan: number) => void;
|
|
59
59
|
undo: () => void;
|
|
60
60
|
redo: () => void;
|
|
61
|
+
clearHistory: () => void;
|
|
62
|
+
setMaxUndoDepth: (depth: number) => void;
|
|
61
63
|
setMidiEvents: (
|
|
62
64
|
clipId: number,
|
|
63
65
|
events: ReadonlyArray<ProjectMidiEvent | readonly [number, number, number]>,
|
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
import { getSonareModule } from './module_state';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RealtimeVoiceChangerConfigInput,
|
|
4
|
+
RealtimeVoiceChangerPodConfig,
|
|
5
|
+
VoicePresetId,
|
|
6
|
+
} from './public_types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True when `config` is a flat POD (the shape `realtimeVoiceChangerPresetConfig`
|
|
10
|
+
* returns) rather than a preset name or a nested preset. `retuneSemitones` only
|
|
11
|
+
* exists on the flat POD; the nested form spells it `retune.semitones`.
|
|
12
|
+
*/
|
|
13
|
+
function isFlatVoiceChangerPod(config: unknown): config is RealtimeVoiceChangerPodConfig {
|
|
14
|
+
return typeof config === 'object' && config !== null && 'retuneSemitones' in config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rewrite the flat 36-field POD into the nested shape the native config parser
|
|
19
|
+
* reads. Passing the flat POD straight to `setConfig` would leave every nested
|
|
20
|
+
* field at its default (only the three root fields are read flat), silently
|
|
21
|
+
* discarding retune/EQ/reverb/etc.
|
|
22
|
+
*/
|
|
23
|
+
function flatVoiceChangerPodToNested(pod: RealtimeVoiceChangerPodConfig): Record<string, unknown> {
|
|
24
|
+
return {
|
|
25
|
+
inputGainDb: pod.inputGainDb,
|
|
26
|
+
outputGainDb: pod.outputGainDb,
|
|
27
|
+
wetMix: pod.wetMix,
|
|
28
|
+
retune: { semitones: pod.retuneSemitones, mix: pod.retuneMix, grainSize: pod.retuneGrainSize },
|
|
29
|
+
formant: {
|
|
30
|
+
factor: pod.formantFactor,
|
|
31
|
+
amount: pod.formantAmount,
|
|
32
|
+
body: pod.formantBody,
|
|
33
|
+
brightness: pod.formantBrightness,
|
|
34
|
+
nasal: pod.formantNasal,
|
|
35
|
+
},
|
|
36
|
+
eq: {
|
|
37
|
+
highpassHz: pod.eqHighpassHz,
|
|
38
|
+
bodyDb: pod.eqBodyDb,
|
|
39
|
+
presenceDb: pod.eqPresenceDb,
|
|
40
|
+
airDb: pod.eqAirDb,
|
|
41
|
+
},
|
|
42
|
+
gate: {
|
|
43
|
+
thresholdDb: pod.gateThresholdDb,
|
|
44
|
+
attackMs: pod.gateAttackMs,
|
|
45
|
+
releaseMs: pod.gateReleaseMs,
|
|
46
|
+
rangeDb: pod.gateRangeDb,
|
|
47
|
+
},
|
|
48
|
+
compressor: {
|
|
49
|
+
thresholdDb: pod.compressorThresholdDb,
|
|
50
|
+
ratio: pod.compressorRatio,
|
|
51
|
+
attackMs: pod.compressorAttackMs,
|
|
52
|
+
releaseMs: pod.compressorReleaseMs,
|
|
53
|
+
makeupGainDb: pod.compressorMakeupGainDb,
|
|
54
|
+
},
|
|
55
|
+
deesser: {
|
|
56
|
+
frequencyHz: pod.deesserFrequencyHz,
|
|
57
|
+
thresholdDb: pod.deesserThresholdDb,
|
|
58
|
+
ratio: pod.deesserRatio,
|
|
59
|
+
rangeDb: pod.deesserRangeDb,
|
|
60
|
+
},
|
|
61
|
+
reverb: {
|
|
62
|
+
mix: pod.reverbMix,
|
|
63
|
+
timeMs: pod.reverbTimeMs,
|
|
64
|
+
damping: pod.reverbDamping,
|
|
65
|
+
seed: pod.reverbSeed,
|
|
66
|
+
},
|
|
67
|
+
limiter: {
|
|
68
|
+
ceilingDb: pod.limiterCeilingDb,
|
|
69
|
+
releaseMs: pod.limiterReleaseMs,
|
|
70
|
+
enableIspLimiter: pod.limiterEnableIspLimiter,
|
|
71
|
+
ispCeilingDbtp: pod.limiterIspCeilingDbtp,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
3
75
|
|
|
4
76
|
/**
|
|
5
77
|
* Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} mono
|
|
@@ -58,8 +130,12 @@ export class RealtimeVoiceChanger {
|
|
|
58
130
|
this.changer.reset();
|
|
59
131
|
}
|
|
60
132
|
|
|
61
|
-
setConfig(config: RealtimeVoiceChangerConfigInput): void {
|
|
62
|
-
|
|
133
|
+
setConfig(config: RealtimeVoiceChangerConfigInput | RealtimeVoiceChangerPodConfig): void {
|
|
134
|
+
// A flat POD (from realtimeVoiceChangerPresetConfig) is rewritten to the
|
|
135
|
+
// nested shape the native parser reads; a preset name or nested preset is
|
|
136
|
+
// passed through unchanged.
|
|
137
|
+
const resolved = isFlatVoiceChangerPod(config) ? flatVoiceChangerPodToNested(config) : config;
|
|
138
|
+
this.changer.setConfig(resolved as Record<string, unknown> | string);
|
|
63
139
|
}
|
|
64
140
|
|
|
65
141
|
configJson(): string {
|
package/src/sonare.js.d.ts
CHANGED
|
@@ -745,6 +745,10 @@ export interface WasmEngineTransportState {
|
|
|
745
745
|
barCount: number;
|
|
746
746
|
/** Time signature in effect at the current PPQ. */
|
|
747
747
|
timeSignature: { numerator: number; denominator: number; confidence: number };
|
|
748
|
+
/** One-based beat within the current bar (`barCount` is zero-based). */
|
|
749
|
+
beat: number;
|
|
750
|
+
/** Fractional position within the current beat, in [0, 1). */
|
|
751
|
+
beatFraction: number;
|
|
748
752
|
}
|
|
749
753
|
|
|
750
754
|
export interface WasmEngineBounceOptions {
|