@libraz/libsonare 1.3.0 → 1.3.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.
- package/README.md +232 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +147 -22
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +52 -2
- package/dist/worklet.js +92 -4
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/effects_mastering.ts +16 -0
- package/src/errors.ts +44 -0
- package/src/index.ts +2 -0
- package/src/live_audio.ts +6 -4
- package/src/mixer.ts +11 -0
- package/src/module_state.ts +115 -4
- package/src/opfs_clip_pages.ts +19 -4
- package/src/sonare.js.d.ts +6 -0
- package/src/web_midi.ts +10 -11
package/README.md
CHANGED
|
@@ -21,6 +21,11 @@ no model weights.
|
|
|
21
21
|
> If you need to read WAV/MP3/M4A files directly in Node, use the native
|
|
22
22
|
> N-API package [`@libraz/libsonare-native`](https://github.com/libraz/libsonare/tree/main/bindings/node) instead.
|
|
23
23
|
|
|
24
|
+
> **Platform constraints:** the WebAssembly build is single-threaded (analysis
|
|
25
|
+
> runs to completion on the calling thread — there is no non-blocking variant),
|
|
26
|
+
> has no host filesystem access, and expects pre-decoded `Float32Array` sample
|
|
27
|
+
> buffers. Drive long-running calls from a Web Worker to keep the UI responsive.
|
|
28
|
+
|
|
24
29
|
## Installation
|
|
25
30
|
|
|
26
31
|
```bash
|
|
@@ -124,6 +129,54 @@ const room = analyzeImpulseResponse(irSamples, sampleRate);
|
|
|
124
129
|
console.log(blind.rt60, room.c50);
|
|
125
130
|
```
|
|
126
131
|
|
|
132
|
+
Acoustic simulation adds `synthesizeRir` (synthesize a shoebox-room impulse
|
|
133
|
+
response from geometry), `estimateRoom` (recover an equivalent room from a
|
|
134
|
+
recording or IR), and `roomMorph` (creatively re-reverberate audio toward a
|
|
135
|
+
target room — not dereverberation).
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { init, synthesizeRir, estimateRoom, roomMorph } from '@libraz/libsonare';
|
|
139
|
+
|
|
140
|
+
await init();
|
|
141
|
+
|
|
142
|
+
const { rir, hasError } = synthesizeRir({
|
|
143
|
+
lengthM: 6,
|
|
144
|
+
widthM: 4,
|
|
145
|
+
heightM: 3,
|
|
146
|
+
absorption: 0.2,
|
|
147
|
+
sampleRate: 48000,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const room = estimateRoom(samples, 48000); // { volume, length, width, height, ... }
|
|
151
|
+
const morphed = roomMorph(samples, sampleRate, { lengthM: 12, widthM: 9, wet: 0.5 });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Error handling
|
|
155
|
+
|
|
156
|
+
Native (C++) failures are thrown as a `SonareError` carrying a numeric `code`
|
|
157
|
+
(an `ErrorCode` value) and its canonical `codeName`, so you can branch on the
|
|
158
|
+
cause instead of matching message text. Use the `isSonareError` type guard in a
|
|
159
|
+
`catch`:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { init, analyze, ErrorCode, isSonareError } from '@libraz/libsonare';
|
|
163
|
+
|
|
164
|
+
await init();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const result = analyze(samples, sampleRate);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (isSonareError(error)) {
|
|
170
|
+
console.error(`${error.codeName} (${error.code}): ${error.message}`);
|
|
171
|
+
if (error.code === ErrorCode.InvalidParameter) {
|
|
172
|
+
// recover...
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
127
180
|
### Decoding files in the browser
|
|
128
181
|
|
|
129
182
|
```typescript
|
|
@@ -331,6 +384,149 @@ try {
|
|
|
331
384
|
}
|
|
332
385
|
```
|
|
333
386
|
|
|
387
|
+
#### Clip warp
|
|
388
|
+
|
|
389
|
+
A clip can be time-warped during an offline `bounce`. `setClipWarpMode` selects
|
|
390
|
+
the playback mode (`ProjectWarpMode`: `'off'` | `'repitch'` | `'tempo-sync'`),
|
|
391
|
+
`setClipWarpRef` binds it to a warp map, and `setWarpMap` registers a first-class
|
|
392
|
+
warp map (anchors mapping warp-timeline samples to source samples).
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
project.setWarpMap({
|
|
396
|
+
id: 1,
|
|
397
|
+
name: 'main',
|
|
398
|
+
anchors: [
|
|
399
|
+
{ warpSample: 0, sourceSample: 0 },
|
|
400
|
+
{ warpSample: 48000, sourceSample: 24000 },
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
project.setClipWarpRef(clipId, 1);
|
|
404
|
+
project.setClipWarpMode(clipId, 'tempo-sync');
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
> Warp is an offline `Project.bounce` feature only. Realtime warp playback is
|
|
408
|
+
> **not** available in `RealtimeEngine`.
|
|
409
|
+
|
|
410
|
+
### Instruments and synthesis
|
|
411
|
+
|
|
412
|
+
MIDI tracks bounce silently unless an instrument is bound. `Project` offers
|
|
413
|
+
three instrument backends, each as a `bounceWith…` variant that takes a binding
|
|
414
|
+
(or array of bindings) plus the usual `ProjectBounceOptions`:
|
|
415
|
+
|
|
416
|
+
- `bounceWithBuiltinInstrument(binding?, options?)` — simple built-in oscillator
|
|
417
|
+
synth (`BuiltinSynthConfig` / `BuiltinSynthBinding`: waveform + ADSR + gain).
|
|
418
|
+
- `bounceWithSynthInstrument(patchOrName?, options?)` — patch-driven NativeSynth
|
|
419
|
+
(`SynthPatch`, or a preset-name string like `'saw-lead'`).
|
|
420
|
+
- `bounceWithSf2Instrument(config?, options?)` — GS-compatible SoundFont player
|
|
421
|
+
(`Sf2InstrumentConfig`), fed by `loadSoundFont()`.
|
|
422
|
+
|
|
423
|
+
Discover NativeSynth presets with `synthPresetNames()` and fetch one as an
|
|
424
|
+
editable patch with `synthPresetPatch(name)`.
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { init, Project, synthPresetNames, synthPresetPatch } from '@libraz/libsonare';
|
|
428
|
+
|
|
429
|
+
await init();
|
|
430
|
+
|
|
431
|
+
const project = new Project();
|
|
432
|
+
try {
|
|
433
|
+
const { clipId } = project.addMidiClip(0, 4);
|
|
434
|
+
project.setMidiEvents(clipId, [
|
|
435
|
+
Project.midiNoteOn(0, 0, 0, 60, 100),
|
|
436
|
+
Project.midiNoteOff(1, 0, 0, 60),
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
// Built-in oscillator synth.
|
|
440
|
+
const a = project.bounceWithBuiltinInstrument({ waveform: 'saw' }, { numChannels: 2 });
|
|
441
|
+
|
|
442
|
+
// NativeSynth from a named preset, tweaked.
|
|
443
|
+
const patch = synthPresetPatch(synthPresetNames()[0]);
|
|
444
|
+
patch.cutoffHz = 4000;
|
|
445
|
+
const b = project.bounceWithSynthInstrument(patch, { numChannels: 2 });
|
|
446
|
+
|
|
447
|
+
// SoundFont player (requires loadSoundFont first).
|
|
448
|
+
project.loadSoundFont(sf2Bytes);
|
|
449
|
+
const c = project.bounceWithSf2Instrument({ gain: 0.6 }, { numChannels: 2 });
|
|
450
|
+
} finally {
|
|
451
|
+
project.delete();
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Real-time engine
|
|
456
|
+
|
|
457
|
+
`RealtimeEngine` is a control-thread-driven transport + render engine: it plays
|
|
458
|
+
a clip/automation timeline, hosts MIDI instruments, accepts live MIDI, and
|
|
459
|
+
renders blocks (or bounces offline). Bind it to an AudioWorklet for browser
|
|
460
|
+
playback — see the [AudioWorklet bridge](#audioworklet-bridge) below; the engine
|
|
461
|
+
is the offline/headless half, the worklet is the audio-thread half.
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
import { init, RealtimeEngine } from '@libraz/libsonare';
|
|
465
|
+
|
|
466
|
+
await init();
|
|
467
|
+
|
|
468
|
+
const engine = new RealtimeEngine(48000, 128); // sampleRate, maxBlockSize
|
|
469
|
+
try {
|
|
470
|
+
engine.setSynthInstrument('saw-lead', 0); // patch (or name), destinationId
|
|
471
|
+
engine.play();
|
|
472
|
+
engine.pushMidiNoteOn(0, 0, 0, 60, 100); // destination, group, channel, note, velocity
|
|
473
|
+
engine.pushMidiNoteOff(0, 0, 0, 60);
|
|
474
|
+
|
|
475
|
+
const blockL = new Float32Array(128);
|
|
476
|
+
const blockR = new Float32Array(128);
|
|
477
|
+
const out = engine.process([blockL, blockR]); // Float32Array[] per channel
|
|
478
|
+
const telemetry = engine.drainTelemetry();
|
|
479
|
+
} finally {
|
|
480
|
+
engine.destroy();
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Capabilities:
|
|
485
|
+
|
|
486
|
+
- **Transport**: `play` / `stop` / `seekSample` / `seekPpq` / `setTempo` /
|
|
487
|
+
`setTimeSignature` / `setLoop`, plus `getTransportState`.
|
|
488
|
+
- **Instruments**: `setBuiltinInstrument` / `setSynthInstrument` /
|
|
489
|
+
`setSf2Instrument` (+ `loadSoundFont`) per MIDI destination id.
|
|
490
|
+
- **Live MIDI**: `pushMidiNoteOn` / `pushMidiNoteOff` / `pushMidiCc` /
|
|
491
|
+
`pushMidiPanic`, and `bindMidiCc(channel, controller, paramId, options?)` to
|
|
492
|
+
map a CC to an automation parameter.
|
|
493
|
+
- **Process / bounce**: `process` (real-time blocks), the allocation-free
|
|
494
|
+
`prepareChannels` + `getChannelBuffer` + `processPrepared` worklet path,
|
|
495
|
+
`renderOffline`, `bounceOffline`, `freezeOffline`.
|
|
496
|
+
- **Clip page providers**: `createClipPageProvider` + `supplyClipPage` for
|
|
497
|
+
streaming large clip audio in pages; pair with the OPFS helpers
|
|
498
|
+
(`createOpfsClipPageProvider`).
|
|
499
|
+
- **Telemetry**: `drainTelemetry` / `drainMeterTelemetry`. Inspect runtime
|
|
500
|
+
capabilities (ABI compatibility, SharedArrayBuffer/Atomics) via
|
|
501
|
+
`engineCapabilities()`.
|
|
502
|
+
|
|
503
|
+
### Real-time voice changer
|
|
504
|
+
|
|
505
|
+
`RealtimeVoiceChanger` runs a block-by-block voice transformation chain (retune,
|
|
506
|
+
formant shaping, EQ, gate, compressor). Construct it from a preset id (see
|
|
507
|
+
`realtimeVoiceChangerPresetNames()`) or a full config object, then process blocks.
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import { init, RealtimeVoiceChanger, voiceChangeRealtime } from '@libraz/libsonare';
|
|
511
|
+
|
|
512
|
+
await init();
|
|
513
|
+
|
|
514
|
+
const changer = new RealtimeVoiceChanger('bright-idol');
|
|
515
|
+
try {
|
|
516
|
+
changer.prepare(48000, 128, 1); // sampleRate, maxBlockSize, channels
|
|
517
|
+
const out = changer.processMono(block);
|
|
518
|
+
} finally {
|
|
519
|
+
changer.delete();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Whole-buffer convenience wrapper (constructs/prepares/disposes internally).
|
|
523
|
+
const processed = voiceChangeRealtime(samples, { preset: 'deep-narrator', sampleRate: 48000 });
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
For a simple offline pitch + formant shift without the full chain, use
|
|
527
|
+
`voiceChange(samples, sampleRate, { pitchSemitones: -2, formantFactor: 1.1 })`.
|
|
528
|
+
Inspect a preset with `realtimeVoiceChangerPresetJson(name)`.
|
|
529
|
+
|
|
334
530
|
### AudioWorklet bridge
|
|
335
531
|
|
|
336
532
|
The package exposes an optional worklet entry that uses the same `sonare.wasm`
|
|
@@ -476,17 +672,50 @@ chain.reset();
|
|
|
476
672
|
chain.delete(); // release WASM memory
|
|
477
673
|
```
|
|
478
674
|
|
|
675
|
+
### Streaming equalizer and retune
|
|
676
|
+
|
|
677
|
+
`StreamingEqualizer` wraps the unified `EqualizerProcessor` (up to 24 bands,
|
|
678
|
+
RBJ/Vicanek biquads, dynamic EQ, linear-phase FIR, mid/side, auto-gain) with
|
|
679
|
+
state maintained across calls. `StreamingRetune` is a block-by-block mono voice
|
|
680
|
+
retune / pitch shifter.
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
import { init, StreamingEqualizer, StreamingRetune } from '@libraz/libsonare';
|
|
684
|
+
|
|
685
|
+
await init();
|
|
686
|
+
|
|
687
|
+
const eq = new StreamingEqualizer({ sampleRate: 48000, maxBlockSize: 512 });
|
|
688
|
+
try {
|
|
689
|
+
eq.setBand(0, { type: 'HighShelf', frequencyHz: 8000, gainDb: 6, enabled: true });
|
|
690
|
+
const { left: eqL, right: eqR } = eq.processStereo(left, right);
|
|
691
|
+
} finally {
|
|
692
|
+
eq.delete();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const retune = new StreamingRetune({ semitones: 2, mix: 1.0 });
|
|
696
|
+
try {
|
|
697
|
+
retune.prepare(48000, 512); // sampleRate, maxBlockSize
|
|
698
|
+
const shifted = retune.processMono(monoBlock);
|
|
699
|
+
} finally {
|
|
700
|
+
retune.delete();
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
479
704
|
## Features
|
|
480
705
|
|
|
481
|
-
- **Detection**:
|
|
706
|
+
- **Detection**: `detectBeats`, `detectOnsets`, `detectDownbeats`, `detectChords`, `detectKey`, `detectKeyCandidates`, `chordFunctionalAnalysis`, sections
|
|
707
|
+
- **Analysis**: `analyze`, `analyzeWithProgress`, `analyzeBpm`, `analyzeRhythm`, `analyzeDynamics`, `analyzeTimbre`; `hasFfmpegSupport` capability check
|
|
482
708
|
- **Effects**: HPSS, HPSS with residual, time stretch, phase vocoder, pitch shift, normalize, trim, remix
|
|
483
709
|
- **Mastering**: EQ, compressor, tape/exciter, air band, stereo imaging,
|
|
484
710
|
true-peak limiting, loudness optimization
|
|
485
711
|
- **Features**: STFT, mel spectrogram, MFCC, chroma, CQT/VQT, spectral contrast, poly features, zero crossings
|
|
486
712
|
- **Pitch**: YIN, pYIN algorithms with optional `fillNa`
|
|
487
713
|
- **Decomposition & loudness**: NMF decomposition, nearest-neighbour filtering, multichannel LUFS, EBU R128 LRA
|
|
488
|
-
- **Streaming**: Real-time analysis with progressive estimates
|
|
489
|
-
- **
|
|
714
|
+
- **Streaming**: Real-time analysis with progressive estimates; streaming mastering chain, equalizer, and retune
|
|
715
|
+
- **Instruments**: built-in synth, patch-driven NativeSynth, SoundFont (SF2) player — bound to `Project` bounces or the `RealtimeEngine`
|
|
716
|
+
- **Real-time**: `RealtimeEngine` transport/MIDI/render, `RealtimeVoiceChanger`, AudioWorklet bridge
|
|
717
|
+
- **Room acoustics**: blind RT60/EDT, impulse-response clarity metrics, RIR synthesis, room estimation, room morphing
|
|
718
|
+
- **Headless DAW**: `Project` arrangement model — audio/MIDI tracks & clips, undo/redo, MIDI sequencing, clip warp, SMF / MIDI 2.0 Clip File I/O, deterministic JSON, offline `bounce`
|
|
490
719
|
- **Conversions**: Hz/mel/MIDI/note, frames/time, resample
|
|
491
720
|
|
|
492
721
|
## Also available
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { A as AcousticOptions, a as AcousticResult, b as AnalysisResult, c as AnalyzeBpmOptions, d as AnalyzeDynamicsOptions, e as AnalyzeRhythmOptions, f as AnalyzeSectionsOptions, g as AnalyzeTimbreOptions, h as AnalyzerStats, i as Audio, j as AutomationCurve, B as BarChord, k as Beat, l as BindMicrophoneInputOptions, m as BindWebMidiOptions, n as BpmAnalysisResult, o as BpmCandidate, p as BrowserAudioDecodeOptions, q as BuiltinSynthBinding, r as BuiltinSynthConfig, s as BuiltinSynthWaveform, C as Chord, t as ChordAnalysisResult, u as ChordChange, v as ChordDetectionOptions, w as ChordQuality, x as ChromaResult, y as ClippingRegion, z as ClippingReport, D as CompressorDetector, E as CompressorOptions, F as CqtResult, G as DeclickOptions, H as DeclipOptions, I as DecomposeResult, J as DecrackleMode, K as DecrackleOptions, L as DehumOptions, M as DenoiseClassicalMode, N as DenoiseClassicalNoiseEstimator, O as DenoiseClassicalOptions, P as DereverbClassicalOptions, Q as DynamicRangeReport, R as Dynamics, S as DynamicsAnalysisResult, T as DynamicsResult, U as EXPECTED_ENGINE_ABI_VERSION, V as EXPECTED_PROJECT_ABI_VERSION, W as EngineAutomationPoint, X as EngineBounceOptions, Y as EngineBounceResult, Z as EngineCapabilities, _ as EngineCaptureStatus, $ as EngineClip, a0 as EngineFreezeOptions, a1 as EngineFreezeResult, a2 as EngineGraphSpec, a3 as EngineMarker, a4 as EngineMeterTelemetry, a5 as EngineMetronomeConfig, a6 as EngineParameterInfo, a7 as EngineTelemetry, a8 as EngineTransportState, a9 as EqBand, aa as EqBandPhase, ab as EqBandType, ac as EqCoeffMode, ad as EqMatchOptions, ae as EqSpectrumSnapshot, af as EqStereoPlacement, ag as
|
|
1
|
+
export { A as AcousticOptions, a as AcousticResult, b as AnalysisResult, c as AnalyzeBpmOptions, d as AnalyzeDynamicsOptions, e as AnalyzeRhythmOptions, f as AnalyzeSectionsOptions, g as AnalyzeTimbreOptions, h as AnalyzerStats, i as Audio, j as AutomationCurve, B as BarChord, k as Beat, l as BindMicrophoneInputOptions, m as BindWebMidiOptions, n as BpmAnalysisResult, o as BpmCandidate, p as BrowserAudioDecodeOptions, q as BuiltinSynthBinding, r as BuiltinSynthConfig, s as BuiltinSynthWaveform, C as Chord, t as ChordAnalysisResult, u as ChordChange, v as ChordDetectionOptions, w as ChordQuality, x as ChromaResult, y as ClippingRegion, z as ClippingReport, D as CompressorDetector, E as CompressorOptions, F as CqtResult, G as DeclickOptions, H as DeclipOptions, I as DecomposeResult, J as DecrackleMode, K as DecrackleOptions, L as DehumOptions, M as DenoiseClassicalMode, N as DenoiseClassicalNoiseEstimator, O as DenoiseClassicalOptions, P as DereverbClassicalOptions, Q as DynamicRangeReport, R as Dynamics, S as DynamicsAnalysisResult, T as DynamicsResult, U as EXPECTED_ENGINE_ABI_VERSION, V as EXPECTED_PROJECT_ABI_VERSION, W as EngineAutomationPoint, X as EngineBounceOptions, Y as EngineBounceResult, Z as EngineCapabilities, _ as EngineCaptureStatus, $ as EngineClip, a0 as EngineFreezeOptions, a1 as EngineFreezeResult, a2 as EngineGraphSpec, a3 as EngineMarker, a4 as EngineMeterTelemetry, a5 as EngineMetronomeConfig, a6 as EngineParameterInfo, a7 as EngineTelemetry, a8 as EngineTransportState, a9 as EqBand, aa as EqBandPhase, ab as EqBandType, ac as EqCoeffMode, ad as EqMatchOptions, ae as EqSpectrumSnapshot, af as EqStereoPlacement, ag as ErrorCode, ah as FrameBuffer, ai as GateOptions, aj as GoniometerPoint, ak as HpssResult, al as HpssWithResidualResult, am as Key, an as KeyCandidate, ao as KeyDetectionOptions, ap as KeyProfile, aq as KeyProfileName, ar as LufsResult, as as MasteringChainConfig, at as MasteringChainResult, au as MasteringOptions, av as MasteringPreset, aw as MasteringProcessorParams, ax as MasteringResult, ay as MasteringStereoChainResult, az as MasteringStereoResult, aA as Matrix2dResult, aB as MelPowerResult, aC as MelSpectrogramResult, aD as MelodyOptions, aE as MelodyPoint, aF as MelodyResult, aG as MeterTap, aH as MeteringDetectClippingOptions, aI as MeteringDynamicRangeOptions, aJ as MfccResult, aK as MicrophoneInputBinding, aL as MidiCcBindOptions, aM as MidiCcLearnOptions, aN as MixMeterSnapshot, aO as MixOptions, aP as MixResult, aQ as Mixer, aR as MixerProcessResult, aS as MixerRealtimeBuffer, aT as Mode, aU as NoteStretchOptions, aV as OpfsClipPageProviderBinding, aW as OpfsClipPageProviderOptions, aX as PairAnalysis, aY as PairProcessor, aZ as PanLaw, a_ as PanMode, a$ as PatternScore, b0 as PhaseScopeReport, b1 as Pitch, b1 as PitchClass, b2 as PitchResult, b3 as ProgressiveEstimate, b4 as Project, b5 as ProjectAssistSidecar, b6 as ProjectAutomationCurve, b7 as ProjectAutomationLaneDesc, b8 as ProjectAutomationPoint, b9 as ProjectBounceOptions, ba as ProjectChordSymbol, bb as ProjectClipCompSegment, bc as ProjectClipDesc, bd as ProjectClipFade, be as ProjectClipTake, bf as ProjectCompileResult, bg as ProjectFadeCurve, bh as ProjectKeySegment, bi as ProjectLoopMode, bj as ProjectLoopRecordingDesc, bk as ProjectLoopRecordingResult, bl as ProjectMidiClipResult, bm as ProjectMidiEvent, bn as ProjectNotePairValidation, bo as ProjectTrackDesc, bp as ProjectTrackKind, bq as ProjectWarpAnchor, br as ProjectWarpMapDesc, bs as RealtimeEngine, bt as RealtimeVoiceChanger, bu as RealtimeVoiceChangerConfigInput, bv as RealtimeVoiceChangerInterleavedBuffer, bw as RealtimeVoiceChangerMonoBuffer, bx as RealtimeVoiceChangerPlanarBuffer, by as RealtimeVoiceChangerPodConfig, bz as RhythmAnalysisResult, bA as RhythmFeatures, bB as RirResult, bC as RirSynthOptions, bD as RoomEstimateOptions, bE as RoomEstimateResult, bF as RoomGeometryOptions, bG as RoomMorphOptions, bH as SYNTH_BODY_TYPES, bI as SYNTH_ENGINE_MODES, bJ as SYNTH_FILTER_MODELS, bK as SYNTH_FILTER_OUTPUTS, bL as SYNTH_MOD_DESTINATIONS, bM as SYNTH_MOD_SOURCES, bN as SYNTH_OSC_WAVEFORMS, bO as Section, bP as SectionType, bQ as SendTiming, bR as Sf2InstrumentConfig, bS as Sf2ProgramStatus, bT as SoloProcessor, bU as SonareError, bV as SourceBackend, bW as SpectrumOptions, bX as SpectrumReport, bY as StereoAnalysis, bZ as StftPowerResult, b_ as StftResult, b$ as StreamAnalyzer, c0 as StreamConfig, c1 as StreamConfigDefaults, c2 as StreamFramesI16, c3 as StreamFramesU8, c4 as StreamQuantizeConfig, c5 as StreamingEqualizer, c6 as StreamingEqualizerConfig, c7 as StreamingMasteringChain, c8 as StreamingMasteringChainConfig, c9 as StreamingPlatform, ca as StreamingRetune, cb as StreamingRetuneConfig, cc as SynthBodyType, cd as SynthEngineMode, ce as SynthEnumTables, cf as SynthFilterModel, cg as SynthFilterOutput, ch as SynthModDestination, ci as SynthModRouting, cj as SynthModSource, ck as SynthOscWaveform, cl as SynthPatch, cm as TempogramMode, cn as Timbre, co as TimbreAnalysisResult, cp as TimbreFrame, cq as TimeSignature, cr as TransientShaperOptions, cs as TrimSilenceMode, ct as TrimSilenceOptions, cu as ValidateOptions, cv as VectorscopeReport, cw as VoiceChangeOptions, cx as VoiceChangeRealtimeOptions, cy as VoicePresetId, cz as WaveformPeakPyramidOptions, cA as WaveformPeaksOptions, cB as WaveformPeaksReport, cC as WebMidiBinding, cD as WebMidiCcBinding, cE as WebMidiInputInfo, cF as amplitudeToDb, cG as analyze, cH as analyzeBpm, cI as analyzeDynamics, cJ as analyzeImpulseResponse, cK as analyzeMelody, cL as analyzeRhythm, cM as analyzeSections, cN as analyzeTimbre, cO as analyzeWithProgress, cP as bassChroma, cQ as bindMicrophoneInput, cR as bindWebMidi, cS as chordFunctionalAnalysis, cT as chroma, cU as chromaCens, cV as cqt, cW as createOpfsClipPageProvider, cX as createOpfsClipPageWorker, cY as cyclicTempogram, cZ as dbToAmplitude, c_ as dbToPower, c$ as decompose, d0 as decomposeWithInit, d1 as deemphasis, d2 as detectAcoustic, d3 as detectBeats, d4 as detectBpm, d5 as detectChords, d6 as detectDownbeats, d7 as detectKey, d8 as detectKeyCandidates, d9 as detectOnsets, da as ebur128LoudnessRange, db as engineAbiVersion, dc as engineCapabilities, dd as estimateRoom, de as estimateTuning, df as fixFrames, dg as fixLength, dh as fourierTempogram, di as frameSignal, dj as framesToSamples, dk as framesToTime, dl as harmonic, dm as hasFfmpegSupport, dn as hpss, dp as hpssWithResidual, dq as hybridCqt, dr as hzToMel, ds as hzToMidi, dt as hzToNote, init, isInitialized, du as isSonareError, dv as isWebMidiAvailable, dw as lufs, dx as lufsInterleaved, dy as masterAudio, dz as masterAudioStereo, dA as masterAudioStereoWithProgress, dB as masterAudioWithProgress, dC as mastering, dD as masteringAssistantSuggest, dE as masteringAudioProfile, dF as masteringChain, dG as masteringChainStereo, dH as masteringChainStereoWithProgress, dI as masteringChainWithProgress, dJ as masteringDynamicsCompressor, dK as masteringDynamicsGate, dL as masteringDynamicsTransientShaper, dM as masteringInsertNames, dN as masteringInsertParamNames, dO as masteringPairAnalysisNames, dP as masteringPairAnalyze, dQ as masteringPairProcess, dR as masteringPairProcessorNames, dS as masteringPresetNames, dT as masteringProcess, dU as masteringProcessStereo, dV as masteringProcessorNames, dW as masteringRepairDeclick, dX as masteringRepairDeclip, dY as masteringRepairDecrackle, dZ as masteringRepairDehum, d_ as masteringRepairDenoiseClassical, d$ as masteringRepairDereverbClassical, e0 as masteringRepairTrimSilence, e1 as masteringStereoAnalysisNames, e2 as masteringStereoAnalyze, e3 as masteringStreamingPreview, e4 as melSpectrogram, e5 as melToAudio, e6 as melToHz, e7 as melToStft, e8 as meteringCrestFactorDb, e9 as meteringDcOffset, ea as meteringDetectClipping, eb as meteringDynamicRange, ec as meteringPeakDb, ed as meteringPhaseScope, ee as meteringPhaseScopeDecimated, ef as meteringRmsDb, eg as meteringSpectrum, eh as meteringSpectrumFrame, ei as meteringStereoCorrelation, ej as meteringStereoWidth, ek as meteringTruePeakDb, el as meteringVectorscope, em as meteringVectorscopeDecimated, en as mfcc, eo as mfccToAudio, ep as mfccToMel, eq as midiToHz, er as mixStereo, es as mixingScenePresetJson, et as mixingScenePresetNames, eu as momentaryLufs, ev as nnFilter, ew as nnlsChroma, ex as normalize, ey as noteStretch, ez as noteToHz, eA as onsetEnvelope, eB as onsetStrengthMulti, eC as opfsClipPageWorkerSource, eD as padCenter, eE as pcen, eF as peakPick, eG as percussive, eH as phaseVocoder, eI as pitchCorrectToMidi, eJ as pitchCorrectToMidiTimevarying, eK as pitchPyin, eL as pitchShift, eM as pitchTuning, eN as pitchYin, eO as plp, eP as polyFeatures, eQ as powerToDb, eR as preemphasis, eS as projectAbiVersion, eT as pseudoCqt, eU as realtimeVoiceChangerPresetConfig, eV as realtimeVoiceChangerPresetJson, eW as realtimeVoiceChangerPresetNames, eX as remix, eY as resample, eZ as rmsEnergy, e_ as roomMorph, e$ as samplesToFrames, f0 as scaleCorrectionSemitones, f1 as scalePitchClassEnabled, f2 as scaleQuantizeMidi, f3 as shortTermLufs, f4 as spectralBandwidth, f5 as spectralCentroid, f6 as spectralContrast, f7 as spectralFlatness, f8 as spectralRolloff, f9 as splitSilence, fa as stft, fb as stftDb, fc as streamAnalyzerConfigDefaults, fd as synthEnumTables, fe as synthPresetNames, ff as synthPresetPatch, fg as synthesizeRir, fh as tempogram, fi as tempogramRatio, fj as timeStretch, fk as timeToFrames, fl as tonnetz, fm as trim, fn as trimSilence, fo as validateRealtimeVoiceChangerPresetJson, fp as vectorNormalize, fq as version, fr as voiceChange, fs as voiceChangeRealtime, ft as voiceChangerAbiVersion, fu as voiceCharacterPresetId, fv as vqt, fw as waveformPeakPyramid, fx as waveformPeaks, fy as zeroCrossingRate, fz as zeroCrossings } from './worklet.js';
|
|
2
2
|
export { ProgressCallback } from './sonare.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,106 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
3
|
+
ErrorCode2[ErrorCode2["Ok"] = 0] = "Ok";
|
|
4
|
+
ErrorCode2[ErrorCode2["FileNotFound"] = 1] = "FileNotFound";
|
|
5
|
+
ErrorCode2[ErrorCode2["InvalidFormat"] = 2] = "InvalidFormat";
|
|
6
|
+
ErrorCode2[ErrorCode2["DecodeFailed"] = 3] = "DecodeFailed";
|
|
7
|
+
ErrorCode2[ErrorCode2["InvalidParameter"] = 4] = "InvalidParameter";
|
|
8
|
+
ErrorCode2[ErrorCode2["OutOfMemory"] = 5] = "OutOfMemory";
|
|
9
|
+
ErrorCode2[ErrorCode2["NotSupported"] = 6] = "NotSupported";
|
|
10
|
+
ErrorCode2[ErrorCode2["InvalidState"] = 7] = "InvalidState";
|
|
11
|
+
ErrorCode2[ErrorCode2["Unknown"] = 99] = "Unknown";
|
|
12
|
+
return ErrorCode2;
|
|
13
|
+
})(ErrorCode || {});
|
|
14
|
+
var SonareError = class extends Error {
|
|
15
|
+
constructor(code, codeName, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "SonareError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.codeName = codeName;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function isSonareError(value) {
|
|
23
|
+
return value instanceof Error && value.name === "SonareError" && typeof value.code === "number";
|
|
24
|
+
}
|
|
25
|
+
|
|
1
26
|
// src/module_state.ts
|
|
2
|
-
var
|
|
27
|
+
var wrappedModule = null;
|
|
28
|
+
function nativeExceptionPtr(error) {
|
|
29
|
+
if (typeof error === "number") {
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
if (error !== null && typeof error === "object") {
|
|
33
|
+
const ptr = error.excPtr;
|
|
34
|
+
if (typeof ptr === "number") {
|
|
35
|
+
return ptr;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function makeSonareError(raw, thrown) {
|
|
41
|
+
let code = 99 /* Unknown */;
|
|
42
|
+
let codeName = "Unknown";
|
|
43
|
+
let message = `libsonare native exception (${thrown})`;
|
|
44
|
+
try {
|
|
45
|
+
const info = raw.sonareExceptionInfo?.(thrown);
|
|
46
|
+
if (info) {
|
|
47
|
+
code = info.code ?? code;
|
|
48
|
+
codeName = info.codeName ?? codeName;
|
|
49
|
+
message = info.message || message;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
return new SonareError(code, codeName, message);
|
|
54
|
+
}
|
|
55
|
+
function wrapModuleErrors(raw) {
|
|
56
|
+
const cache = /* @__PURE__ */ new Map();
|
|
57
|
+
const convert = (error) => {
|
|
58
|
+
const ptr = nativeExceptionPtr(error);
|
|
59
|
+
if (ptr !== null) {
|
|
60
|
+
throw makeSonareError(raw, ptr);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
};
|
|
64
|
+
return new Proxy(raw, {
|
|
65
|
+
get(target, prop, receiver) {
|
|
66
|
+
const value = Reflect.get(target, prop, receiver);
|
|
67
|
+
if (typeof value !== "function") {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
const cached = cache.get(prop);
|
|
71
|
+
if (cached) {
|
|
72
|
+
return cached;
|
|
73
|
+
}
|
|
74
|
+
const fn = value;
|
|
75
|
+
const wrapped = new Proxy(fn, {
|
|
76
|
+
apply(t, thisArg, args) {
|
|
77
|
+
try {
|
|
78
|
+
return Reflect.apply(t, thisArg, args);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return convert(error);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
construct(t, args, newTarget) {
|
|
84
|
+
try {
|
|
85
|
+
return Reflect.construct(t, args, newTarget);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return convert(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
cache.set(prop, wrapped);
|
|
92
|
+
return wrapped;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
3
96
|
function setSonareModule(module2) {
|
|
4
|
-
|
|
97
|
+
wrappedModule = wrapModuleErrors(module2);
|
|
5
98
|
}
|
|
6
99
|
function getSonareModule() {
|
|
7
|
-
if (!
|
|
100
|
+
if (!wrappedModule) {
|
|
8
101
|
throw new Error("Module not initialized. Call init() first.");
|
|
9
102
|
}
|
|
10
|
-
return
|
|
103
|
+
return wrappedModule;
|
|
11
104
|
}
|
|
12
105
|
|
|
13
106
|
// src/codes.ts
|
|
@@ -82,6 +175,16 @@ var Mixer = class _Mixer {
|
|
|
82
175
|
compile() {
|
|
83
176
|
this.mixer.compile();
|
|
84
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Non-fatal warnings captured when this mixer was built from scene JSON: one
|
|
180
|
+
* entry per channel-strip insert that was handed param keys it does not read
|
|
181
|
+
* (a likely typo, or a key meant for a different processor). The scene still
|
|
182
|
+
* loaded; these keys simply took no effect. Empty when every key was consumed.
|
|
183
|
+
* Use {@link masteringInsertParamNames} to discover the keys an insert accepts.
|
|
184
|
+
*/
|
|
185
|
+
sceneWarnings() {
|
|
186
|
+
return this.mixer.sceneWarnings();
|
|
187
|
+
}
|
|
85
188
|
/**
|
|
86
189
|
* Mix one block of per-strip stereo audio into the stereo master.
|
|
87
190
|
*
|
|
@@ -1019,6 +1122,9 @@ function masteringProcessorNames() {
|
|
|
1019
1122
|
function masteringInsertNames() {
|
|
1020
1123
|
return requireModule().masteringInsertNames();
|
|
1021
1124
|
}
|
|
1125
|
+
function masteringInsertParamNames(name) {
|
|
1126
|
+
return requireModule().masteringInsertParamNames(name);
|
|
1127
|
+
}
|
|
1022
1128
|
function masteringPairProcessorNames() {
|
|
1023
1129
|
return requireModule().masteringPairProcessorNames();
|
|
1024
1130
|
}
|
|
@@ -2354,9 +2460,11 @@ var Audio = class _Audio {
|
|
|
2354
2460
|
|
|
2355
2461
|
// src/live_audio.ts
|
|
2356
2462
|
async function bindMicrophoneInput(context, engine, options = {}) {
|
|
2357
|
-
const stream =
|
|
2358
|
-
|
|
2359
|
-
|
|
2463
|
+
const { stream: providedStream, stopTracksOnClose = true, ...constraints } = options;
|
|
2464
|
+
const stream = providedStream ?? await navigator.mediaDevices.getUserMedia({
|
|
2465
|
+
...constraints,
|
|
2466
|
+
audio: constraints.audio ?? true,
|
|
2467
|
+
video: constraints.video ?? false
|
|
2360
2468
|
});
|
|
2361
2469
|
const source = context.createMediaStreamSource(stream);
|
|
2362
2470
|
const node = "node" in engine ? engine.node : engine;
|
|
@@ -2371,7 +2479,7 @@ async function bindMicrophoneInput(context, engine, options = {}) {
|
|
|
2371
2479
|
}
|
|
2372
2480
|
closed = true;
|
|
2373
2481
|
source.disconnect();
|
|
2374
|
-
if (
|
|
2482
|
+
if (stopTracksOnClose) {
|
|
2375
2483
|
for (const track of stream.getAudioTracks()) {
|
|
2376
2484
|
track.stop();
|
|
2377
2485
|
}
|
|
@@ -2527,12 +2635,22 @@ self.onmessage = async (event) => {
|
|
|
2527
2635
|
const frames = Math.min(pageFrames, numSamples - startFrame);
|
|
2528
2636
|
const frameBytes = numChannels * 4;
|
|
2529
2637
|
const bytes = new Uint8Array(frames * frameBytes);
|
|
2530
|
-
|
|
2531
|
-
const
|
|
2532
|
-
|
|
2638
|
+
let bytesReadTotal = 0;
|
|
2639
|
+
const readOffset = dataOffsetBytes + startFrame * frameBytes;
|
|
2640
|
+
while (bytesReadTotal < bytes.byteLength) {
|
|
2641
|
+
const bytesRead = access.read(bytes.subarray(bytesReadTotal), {
|
|
2642
|
+
at: readOffset + bytesReadTotal,
|
|
2643
|
+
});
|
|
2644
|
+
if (bytesRead <= 0) {
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2647
|
+
bytesReadTotal += bytesRead;
|
|
2648
|
+
}
|
|
2649
|
+
if (bytesReadTotal !== bytes.byteLength || bytesReadTotal % frameBytes !== 0) {
|
|
2533
2650
|
self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
|
|
2534
2651
|
return;
|
|
2535
2652
|
}
|
|
2653
|
+
const framesRead = bytesReadTotal / frameBytes;
|
|
2536
2654
|
const view = new DataView(bytes.buffer, 0, framesRead * frameBytes);
|
|
2537
2655
|
const channelBuffers = Array.from({ length: numChannels }, () => new ArrayBuffer(framesRead * 4));
|
|
2538
2656
|
for (let ch = 0; ch < numChannels; ++ch) {
|
|
@@ -2598,7 +2716,12 @@ function createOpfsClipPageProvider(engine, options) {
|
|
|
2598
2716
|
entry.resolve(false);
|
|
2599
2717
|
return;
|
|
2600
2718
|
}
|
|
2601
|
-
|
|
2719
|
+
try {
|
|
2720
|
+
provider.supply(response.pageIndex, channels);
|
|
2721
|
+
} catch {
|
|
2722
|
+
entry.resolve(false);
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2602
2725
|
entry.resolve(true);
|
|
2603
2726
|
};
|
|
2604
2727
|
worker.addEventListener("message", onMessage);
|
|
@@ -3964,15 +4087,15 @@ function isWebMidiAvailable() {
|
|
|
3964
4087
|
return typeof globalThis.navigator?.requestMIDIAccess === "function";
|
|
3965
4088
|
}
|
|
3966
4089
|
async function bindWebMidi(engine, options = {}) {
|
|
3967
|
-
const
|
|
3968
|
-
if (typeof requestMIDIAccess !== "function") {
|
|
4090
|
+
const navigatorWithMidi = globalThis.navigator;
|
|
4091
|
+
if (typeof navigatorWithMidi?.requestMIDIAccess !== "function") {
|
|
3969
4092
|
throw new Error("Web MIDI is not available in this environment");
|
|
3970
4093
|
}
|
|
3971
4094
|
const group = options.group ?? 0;
|
|
3972
4095
|
assertNibble("bindWebMidi", group, "group");
|
|
3973
4096
|
const destinationId = options.destinationId ?? 0;
|
|
3974
4097
|
const selectedIds = new Set(options.inputIds ?? []);
|
|
3975
|
-
const access = await requestMIDIAccess({
|
|
4098
|
+
const access = await navigatorWithMidi.requestMIDIAccess({
|
|
3976
4099
|
sysex: options.sysex ?? false,
|
|
3977
4100
|
software: options.software ?? true
|
|
3978
4101
|
});
|
|
@@ -4003,9 +4126,7 @@ async function bindWebMidi(engine, options = {}) {
|
|
|
4003
4126
|
runningStatus,
|
|
4004
4127
|
options.timestampToSamples
|
|
4005
4128
|
);
|
|
4006
|
-
|
|
4007
|
-
runningStatus = status;
|
|
4008
|
-
}
|
|
4129
|
+
runningStatus = status;
|
|
4009
4130
|
};
|
|
4010
4131
|
if (input.addEventListener) {
|
|
4011
4132
|
input.addEventListener("midimessage", listener);
|
|
@@ -4103,18 +4224,18 @@ function dispatchMidiMessage(engine, event, group, runningStatus, timestampToSam
|
|
|
4103
4224
|
const message = status & 240;
|
|
4104
4225
|
const channel = status & 15;
|
|
4105
4226
|
if (message < 128 || message > 224) {
|
|
4106
|
-
return status;
|
|
4227
|
+
return status >= 248 ? runningStatus : 0;
|
|
4107
4228
|
}
|
|
4108
4229
|
const a = readU7(data, offset);
|
|
4109
4230
|
const b = readU7(data, offset + 1);
|
|
4110
|
-
if (a < 0) {
|
|
4231
|
+
if (a < 0 || b < 0) {
|
|
4111
4232
|
return status;
|
|
4112
4233
|
}
|
|
4113
4234
|
const portTimeSamples = timestampToSamples ? timestampToSamples(event.receivedTime ?? event.timeStamp ?? 0) : 0;
|
|
4114
4235
|
if (message === 128) {
|
|
4115
|
-
engine.pushMidiInputNoteOff(group, channel, a, b
|
|
4236
|
+
engine.pushMidiInputNoteOff(group, channel, a, b, portTimeSamples);
|
|
4116
4237
|
} else if (message === 144) {
|
|
4117
|
-
if (
|
|
4238
|
+
if (b === 0) {
|
|
4118
4239
|
engine.pushMidiInputNoteOff(group, channel, a, 0, portTimeSamples);
|
|
4119
4240
|
} else {
|
|
4120
4241
|
engine.pushMidiInputNoteOn(group, channel, a, b, portTimeSamples);
|
|
@@ -4263,6 +4384,7 @@ export {
|
|
|
4263
4384
|
ChordQuality,
|
|
4264
4385
|
EXPECTED_ENGINE_ABI_VERSION,
|
|
4265
4386
|
EXPECTED_PROJECT_ABI_VERSION,
|
|
4387
|
+
ErrorCode,
|
|
4266
4388
|
KeyProfile,
|
|
4267
4389
|
Mixer,
|
|
4268
4390
|
Mode,
|
|
@@ -4279,6 +4401,7 @@ export {
|
|
|
4279
4401
|
SYNTH_MOD_SOURCES,
|
|
4280
4402
|
SYNTH_OSC_WAVEFORMS,
|
|
4281
4403
|
SectionType,
|
|
4404
|
+
SonareError,
|
|
4282
4405
|
StreamAnalyzer,
|
|
4283
4406
|
StreamingEqualizer,
|
|
4284
4407
|
StreamingMasteringChain,
|
|
@@ -4337,6 +4460,7 @@ export {
|
|
|
4337
4460
|
hzToNote,
|
|
4338
4461
|
init,
|
|
4339
4462
|
isInitialized,
|
|
4463
|
+
isSonareError,
|
|
4340
4464
|
isWebMidiAvailable,
|
|
4341
4465
|
lufs,
|
|
4342
4466
|
lufsInterleaved,
|
|
@@ -4355,6 +4479,7 @@ export {
|
|
|
4355
4479
|
masteringDynamicsGate,
|
|
4356
4480
|
masteringDynamicsTransientShaper,
|
|
4357
4481
|
masteringInsertNames,
|
|
4482
|
+
masteringInsertParamNames,
|
|
4358
4483
|
masteringPairAnalysisNames,
|
|
4359
4484
|
masteringPairAnalyze,
|
|
4360
4485
|
masteringPairProcess,
|