@libraz/libsonare 1.5.5 → 1.7.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.
- package/README.md +109 -20
- package/dist/analysis.d.ts +5809 -0
- package/dist/analysis.js +2574 -0
- package/dist/analysis.js.map +1 -0
- package/dist/index.d.ts +884 -55
- package/dist/index.js +1302 -292
- package/dist/index.js.map +1 -1
- package/dist/schemas/realtime-voice-changer-preset-pack.schema.json +27 -0
- package/dist/schemas/realtime-voice-changer-preset.schema.json +140 -0
- package/dist/sonare-analysis.js +2 -0
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worker.d.ts +65 -0
- package/dist/worker.js +734 -0
- package/dist/worker.js.map +1 -0
- package/dist/worklet.d.ts +628 -88
- package/dist/worklet.js +1816 -266
- package/dist/worklet.js.map +1 -1
- package/package.json +23 -11
- package/src/_chain_config.ts +5 -3
- package/src/analysis.ts +137 -0
- package/src/analysis_helpers.ts +40 -29
- package/src/audio.ts +7 -2
- package/src/clip_page_streamer.ts +62 -8
- package/src/codes.ts +87 -64
- package/src/effects_mastering.ts +1 -0
- package/src/effects_transform.ts +179 -10
- package/src/errors.ts +1 -0
- package/src/feature_core.ts +140 -2
- package/src/feature_music.ts +25 -3
- package/src/feature_pitch.ts +70 -1
- package/src/feature_spectral.ts +295 -7
- package/src/feature_spectrogram.ts +203 -3
- package/src/features.ts +16 -1
- package/src/index.ts +120 -8
- package/src/mastering_chain.ts +20 -8
- package/src/mastering_core.ts +5 -0
- package/src/mastering_dynamics.ts +7 -1
- package/src/metering.ts +6 -1
- package/src/mixer.ts +3 -3
- package/src/opfs_clip_pages.ts +26 -1
- package/src/project.ts +13 -0
- package/src/project_class.ts +204 -23
- package/src/project_internal.ts +38 -34
- package/src/project_types.ts +114 -11
- package/src/public_types.ts +68 -0
- package/src/public_types_mastering.ts +22 -0
- package/src/public_types_mixing.ts +27 -2
- package/src/public_types_music.ts +9 -0
- package/src/public_types_realtime.ts +23 -5
- package/src/public_types_spectral.ts +33 -0
- package/src/quick_analysis.ts +46 -10
- package/src/realtime_engine.ts +167 -6
- package/src/realtime_voice_changer.ts +51 -77
- package/src/sonare-analysis.js.d.ts +8 -0
- package/src/sonare.js.d.ts +340 -6
- package/src/stream_analyzer.ts +6 -1
- package/src/streaming_processors.ts +15 -0
- package/src/web_midi.ts +46 -11
- package/src/worker.ts +196 -0
- package/src/worker_client.ts +352 -0
- package/src/worker_protocol.ts +59 -0
- package/src/worklet/engine-automation.ts +2 -1
- package/src/worklet/engine-capture-facade.ts +13 -4
- package/src/worklet/engine-clips.ts +12 -3
- package/src/worklet/engine-node.ts +264 -21
- package/src/worklet/engine-offline.ts +7 -2
- package/src/worklet/engine-parameter-facade.ts +55 -5
- package/src/worklet/engine-processor.ts +435 -62
- package/src/worklet/engine-register.ts +9 -1
- package/src/worklet/engine-strips.ts +19 -3
- package/src/worklet/engine-sync.ts +2 -6
- package/src/worklet/engine-tempo-facade.ts +8 -6
- package/src/worklet/engine.ts +390 -13
- package/src/worklet/guards.ts +121 -8
- package/src/worklet/messages.ts +166 -8
- package/src/worklet/protocol.ts +305 -17
- package/src/worklet/voice-changer-processor.ts +12 -15
- package/src/worklet.ts +20 -1
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libraz/libsonare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"packageManager": "yarn@4.
|
|
5
|
+
"packageManager": "yarn@4.18.0",
|
|
6
6
|
"description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -11,11 +11,21 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./analysis": {
|
|
15
|
+
"types": "./dist/analysis.d.ts",
|
|
16
|
+
"import": "./dist/analysis.js"
|
|
17
|
+
},
|
|
14
18
|
"./worklet": {
|
|
15
19
|
"types": "./dist/worklet.d.ts",
|
|
16
20
|
"import": "./dist/worklet.js"
|
|
17
21
|
},
|
|
22
|
+
"./worker": {
|
|
23
|
+
"types": "./dist/worker.d.ts",
|
|
24
|
+
"import": "./dist/worker.js"
|
|
25
|
+
},
|
|
18
26
|
"./wasm": "./dist/sonare.wasm",
|
|
27
|
+
"./schemas/realtime-voice-changer-preset.schema.json": "./dist/schemas/realtime-voice-changer-preset.schema.json",
|
|
28
|
+
"./schemas/realtime-voice-changer-preset-pack.schema.json": "./dist/schemas/realtime-voice-changer-preset-pack.schema.json",
|
|
19
29
|
"./package.json": "./package.json"
|
|
20
30
|
},
|
|
21
31
|
"files": [
|
|
@@ -25,19 +35,24 @@
|
|
|
25
35
|
"sideEffects": false,
|
|
26
36
|
"scripts": {
|
|
27
37
|
"build": "yarn build:wasm && yarn build:js",
|
|
28
|
-
"build:wasm": "
|
|
38
|
+
"build:wasm": "yarn build:wasm:full && yarn build:wasm:analysis",
|
|
39
|
+
"build:wasm:full": "mkdir -p build-wasm/em-cache && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm/em-cache emcmake cmake -S ../.. -B build-wasm -DBUILD_WASM=ON -DSONARE_WASM_ANALYSIS_ONLY=OFF -DSONARE_WASM_OUTPUT_NAME=sonare -DBUILD_MASTERING=ON -DBUILD_MIXING=ON -DBUILD_GRAPH=ON -DBUILD_FX=ON -DBUILD_ACOUSTIC_SIM=ON -DBUILD_PITCH_EDITOR=ON -DBUILD_VOICE_CHANGER=ON -DBUILD_ARRANGEMENT=ON -DBUILD_ASSIST=ON -DCMAKE_BUILD_TYPE=Release && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm/em-cache cmake --build build-wasm",
|
|
40
|
+
"build:wasm:analysis": "mkdir -p build-wasm-analysis/em-cache && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm-analysis/em-cache emcmake cmake -S ../.. -B build-wasm-analysis -DBUILD_WASM=ON -DSONARE_WASM_ANALYSIS_ONLY=ON -DSONARE_WASM_OUTPUT_NAME=sonare-analysis -DBUILD_MASTERING=OFF -DBUILD_MIXING=OFF -DBUILD_GRAPH=OFF -DBUILD_FX=OFF -DBUILD_ACOUSTIC_SIM=OFF -DBUILD_VOICE_CHANGER=OFF -DBUILD_ARRANGEMENT=OFF -DBUILD_ASSIST=OFF -DCMAKE_BUILD_TYPE=Release && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm-analysis/em-cache cmake --build build-wasm-analysis --target sonare",
|
|
41
|
+
"check:wasm-size": "python3 ../../tools/check_wasm_size.py --baseline wasm-size-budget.json --artifact sonare.wasm=dist/sonare.wasm --artifact sonare-analysis.wasm=dist/sonare-analysis.wasm",
|
|
29
42
|
"bench:wasm:isp": "mkdir -p build-wasm-bench/em-cache /tmp/libsonare-wasm-bench && EM_CACHE=$PWD/build-wasm-bench/em-cache emcmake cmake -S ../.. -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_MASTERING=ON -DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm-bench/em-cache cmake --build build-wasm-bench --target sonare_mastering_isp_bench && cp build-wasm-bench/bin/sonare_mastering_isp_bench.js /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.cjs && cp build-wasm-bench/bin/sonare_mastering_isp_bench.wasm /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.wasm && node /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.cjs",
|
|
30
43
|
"bench:wasm:phase-vocoder": "mkdir -p build-wasm-bench/em-cache /tmp/libsonare-wasm-bench && EM_CACHE=$PWD/build-wasm-bench/em-cache emcmake cmake -S ../.. -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm-bench/em-cache cmake --build build-wasm-bench --target sonare_phase_vocoder_streaming_bench && cp build-wasm-bench/bin/sonare_phase_vocoder_streaming_bench.js /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.cjs && cp build-wasm-bench/bin/sonare_phase_vocoder_streaming_bench.wasm /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.wasm && node /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.cjs",
|
|
31
|
-
"build:js": "tsup",
|
|
32
|
-
"clean": "rm -rf dist build-wasm LICENSE",
|
|
44
|
+
"build:js": "tsup && node scripts/copy-schemas.mjs",
|
|
45
|
+
"clean": "rm -rf dist build-wasm build-wasm-analysis LICENSE",
|
|
33
46
|
"lint": "biome check src/ tests/",
|
|
34
47
|
"lint:fix": "biome check --write src/ tests/",
|
|
35
48
|
"format": "biome format --write src/ tests/",
|
|
36
49
|
"test": "vitest run",
|
|
50
|
+
"test:types": "tsc --noEmit --ignoreConfig --strict --module ESNext --moduleResolution bundler --target ES2020 tests/typing/analysis-entry.ts tests/typing/worklet-capture-response.ts",
|
|
37
51
|
"test:browser": "node scripts/audio-worklet-smoke.mjs",
|
|
38
|
-
"test:browser:
|
|
52
|
+
"test:browser:worker": "node scripts/offline-worker-smoke.mjs",
|
|
53
|
+
"test:browser:boundary": "node scripts/boundary-smoke.mjs",
|
|
39
54
|
"test:watch": "vitest",
|
|
40
|
-
"prepublishOnly": "yarn clean && cp ../../LICENSE LICENSE && yarn build && yarn lint && yarn test"
|
|
55
|
+
"prepublishOnly": "yarn clean && cp ../../LICENSE LICENSE && yarn build && yarn lint && yarn test && yarn test:types"
|
|
41
56
|
},
|
|
42
57
|
"keywords": [
|
|
43
58
|
"audio",
|
|
@@ -76,12 +91,9 @@
|
|
|
76
91
|
},
|
|
77
92
|
"devDependencies": {
|
|
78
93
|
"@biomejs/biome": "^2.4.16",
|
|
94
|
+
"ajv": "^8.17.1",
|
|
79
95
|
"tsup": "^8.5.1",
|
|
80
96
|
"typescript": "^6.0.3",
|
|
81
97
|
"vitest": "^4.1.8"
|
|
82
|
-
},
|
|
83
|
-
"volta": {
|
|
84
|
-
"node": "22.22.3",
|
|
85
|
-
"yarn": "4.15.0"
|
|
86
98
|
}
|
|
87
99
|
}
|
package/src/_chain_config.ts
CHANGED
|
@@ -4,9 +4,9 @@ type ChainSection = { [key: string]: number | boolean | ChainSection | undefined
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Flattens a nested {@link MasteringChainConfig} into the dot-notation
|
|
7
|
-
* `{ "module.processor.param": value }` map the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* `{ "module.processor.param": value }` map the core consumes. Internal helper
|
|
8
|
+
* shared by the mastering-chain / master-audio entry points. The core owns
|
|
9
|
+
* legacy flat-key aliases, so this remains a structural flattening step.
|
|
10
10
|
*/
|
|
11
11
|
export function flattenChainConfig(config: MasteringChainConfig): Record<string, number | boolean> {
|
|
12
12
|
const out: Record<string, number | boolean> = {};
|
|
@@ -17,6 +17,8 @@ export function flattenChainConfig(config: MasteringChainConfig): Record<string,
|
|
|
17
17
|
out[path] = value;
|
|
18
18
|
} else if (value !== null && typeof value === 'object') {
|
|
19
19
|
walk(value, path);
|
|
20
|
+
} else if (value !== undefined) {
|
|
21
|
+
throw new TypeError(`Mastering override '${path}' must be a number or boolean.`);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
};
|
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis and feature-extraction entry point for the smaller WASM binary.
|
|
3
|
+
*
|
|
4
|
+
* This entry intentionally omits mastering, mixing, realtime engines, Project,
|
|
5
|
+
* and other native-handle APIs. Import the package root when those surfaces are
|
|
6
|
+
* required.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { setSonareModule } from './module_state';
|
|
10
|
+
import type { SonareCapabilities } from './public_types';
|
|
11
|
+
import type { SonareModule } from './sonare.js';
|
|
12
|
+
|
|
13
|
+
export { ErrorCode, isSonareError, SonareError } from './errors';
|
|
14
|
+
export * from './feature_core';
|
|
15
|
+
export * from './feature_music';
|
|
16
|
+
export * from './feature_pitch';
|
|
17
|
+
export * from './feature_resample';
|
|
18
|
+
export * from './feature_spectral';
|
|
19
|
+
export * from './feature_spectrogram';
|
|
20
|
+
export * from './metering';
|
|
21
|
+
export * from './public_types';
|
|
22
|
+
export type {
|
|
23
|
+
AnalyzeBpmRequest,
|
|
24
|
+
AnalyzeDynamicsRequest,
|
|
25
|
+
AnalyzeImpulseResponseRequest,
|
|
26
|
+
AnalyzeRhythmRequest,
|
|
27
|
+
AnalyzeTimbreRequest,
|
|
28
|
+
AnalyzeWithProgressRequest,
|
|
29
|
+
BpmAnalysisResult,
|
|
30
|
+
BpmCandidate,
|
|
31
|
+
ChordFunctionalAnalysisRequest,
|
|
32
|
+
DetectAcousticRequest,
|
|
33
|
+
DetectChordsRequest,
|
|
34
|
+
DetectKeyRequest,
|
|
35
|
+
DynamicsAnalysisResult,
|
|
36
|
+
MusicAnalyzeOptions,
|
|
37
|
+
MusicAnalyzeRequest,
|
|
38
|
+
RhythmAnalysisResult,
|
|
39
|
+
SamplesRequest,
|
|
40
|
+
TimbreAnalysisResult,
|
|
41
|
+
TimbreFrame,
|
|
42
|
+
} from './quick_analysis';
|
|
43
|
+
export {
|
|
44
|
+
analyze,
|
|
45
|
+
analyzeBpm,
|
|
46
|
+
analyzeDynamics,
|
|
47
|
+
analyzeImpulseResponse,
|
|
48
|
+
analyzeRhythm,
|
|
49
|
+
analyzeTimbre,
|
|
50
|
+
analyzeWithProgress,
|
|
51
|
+
chordFunctionalAnalysis,
|
|
52
|
+
detectAcoustic,
|
|
53
|
+
detectBeats,
|
|
54
|
+
detectBpm,
|
|
55
|
+
detectChords,
|
|
56
|
+
detectDownbeats,
|
|
57
|
+
detectKey,
|
|
58
|
+
detectKeyCandidates,
|
|
59
|
+
detectOnsets,
|
|
60
|
+
hasFfmpegSupport,
|
|
61
|
+
} from './quick_analysis';
|
|
62
|
+
|
|
63
|
+
let module: SonareModule | null = null;
|
|
64
|
+
let initPromise: Promise<void> | null = null;
|
|
65
|
+
|
|
66
|
+
/** Initialize the analysis-only WASM module. */
|
|
67
|
+
export async function init(options?: {
|
|
68
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
69
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
70
|
+
moduleFactory?: (options?: {
|
|
71
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
72
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
73
|
+
}) => Promise<SonareModule>;
|
|
74
|
+
}): Promise<void> {
|
|
75
|
+
if (module) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (initPromise) {
|
|
79
|
+
return initPromise;
|
|
80
|
+
}
|
|
81
|
+
initPromise = (async () => {
|
|
82
|
+
try {
|
|
83
|
+
const createModule = options?.moduleFactory ?? (await import('./sonare-analysis.js')).default;
|
|
84
|
+
module = await createModule(options);
|
|
85
|
+
setSonareModule(module);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
initPromise = null;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
return initPromise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether this analysis entry has loaded its WASM module. */
|
|
95
|
+
export function isInitialized(): boolean {
|
|
96
|
+
return module !== null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Version reported by the loaded analysis WASM module. */
|
|
100
|
+
export function version(): string {
|
|
101
|
+
if (!module) {
|
|
102
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
103
|
+
}
|
|
104
|
+
return module.version();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Build capabilities for the loaded analysis-only module. */
|
|
108
|
+
export function capabilities(): SonareCapabilities {
|
|
109
|
+
if (!module) {
|
|
110
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
111
|
+
}
|
|
112
|
+
return module.capabilities();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Packed C-ABI version for compatibility checks. */
|
|
116
|
+
export function abiVersion(): number {
|
|
117
|
+
if (!module) {
|
|
118
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
119
|
+
}
|
|
120
|
+
return module.abiVersion();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Realtime command-queue ABI version shared with the full entry. */
|
|
124
|
+
export function engineAbiVersion(): number {
|
|
125
|
+
if (!module) {
|
|
126
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
127
|
+
}
|
|
128
|
+
return module.engineAbiVersion();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Voice-changer ABI version retained for cross-entry compatibility checks. */
|
|
132
|
+
export function voiceChangerAbiVersion(): number {
|
|
133
|
+
if (!module) {
|
|
134
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
135
|
+
}
|
|
136
|
+
return module.voiceChangerAbiVersion();
|
|
137
|
+
}
|
package/src/analysis_helpers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveEnumOrdinal } from './codes';
|
|
1
2
|
import type {
|
|
2
3
|
AnalysisResult,
|
|
3
4
|
ChordAnalysisResult,
|
|
@@ -48,6 +49,37 @@ export function convertKeyCandidate(wasm: WasmKeyCandidateResult): KeyCandidate
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Consolidated spelling -> ordinal tables, resolved through the same
|
|
53
|
+
// resolveEnumOrdinal() primitive as panModeCode/panLawCode/etc. in ./codes.
|
|
54
|
+
// An unmapped string or an out-of-range numeric ordinal throws RangeError
|
|
55
|
+
// instead of silently resolving to `undefined` (which used to reach the WASM
|
|
56
|
+
// layer as a mistyped argument) or passing an unvalidated raw number through.
|
|
57
|
+
const KEY_MODE_VALUES: Readonly<Record<string, number>> = {
|
|
58
|
+
major: Mode.Major,
|
|
59
|
+
minor: Mode.Minor,
|
|
60
|
+
dorian: Mode.Dorian,
|
|
61
|
+
phrygian: Mode.Phrygian,
|
|
62
|
+
lydian: Mode.Lydian,
|
|
63
|
+
mixolydian: Mode.Mixolydian,
|
|
64
|
+
locrian: Mode.Locrian,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const KEY_PROFILE_VALUES: Readonly<Record<KeyProfileName, number>> = {
|
|
68
|
+
ks: KeyProfileValues.KrumhanslSchmuckler,
|
|
69
|
+
krumhansl: KeyProfileValues.KrumhanslSchmuckler,
|
|
70
|
+
temperley: KeyProfileValues.Temperley,
|
|
71
|
+
shaath: KeyProfileValues.Shaath,
|
|
72
|
+
keyfinder: KeyProfileValues.Shaath,
|
|
73
|
+
'faraldo-edmt': KeyProfileValues.FaraldoEDMT,
|
|
74
|
+
edmt: KeyProfileValues.FaraldoEDMT,
|
|
75
|
+
'faraldo-edma': KeyProfileValues.FaraldoEDMA,
|
|
76
|
+
edma: KeyProfileValues.FaraldoEDMA,
|
|
77
|
+
'faraldo-edmm': KeyProfileValues.FaraldoEDMM,
|
|
78
|
+
edmm: KeyProfileValues.FaraldoEDMM,
|
|
79
|
+
'bellman-budge': KeyProfileValues.BellmanBudge,
|
|
80
|
+
bellman: KeyProfileValues.BellmanBudge,
|
|
81
|
+
};
|
|
82
|
+
|
|
51
83
|
export function keyModeValues(modes: KeyDetectionOptions['modes'] | undefined): number[] {
|
|
52
84
|
if (!modes) {
|
|
53
85
|
return [];
|
|
@@ -66,41 +98,14 @@ export function keyModeValues(modes: KeyDetectionOptions['modes'] | undefined):
|
|
|
66
98
|
Mode.Locrian,
|
|
67
99
|
];
|
|
68
100
|
}
|
|
69
|
-
|
|
70
|
-
major: Mode.Major,
|
|
71
|
-
minor: Mode.Minor,
|
|
72
|
-
dorian: Mode.Dorian,
|
|
73
|
-
phrygian: Mode.Phrygian,
|
|
74
|
-
lydian: Mode.Lydian,
|
|
75
|
-
mixolydian: Mode.Mixolydian,
|
|
76
|
-
locrian: Mode.Locrian,
|
|
77
|
-
} as const;
|
|
78
|
-
return modes.map((mode) => (typeof mode === 'number' ? mode : names[mode]));
|
|
101
|
+
return modes.map((mode) => resolveEnumOrdinal(mode, KEY_MODE_VALUES, 'key mode'));
|
|
79
102
|
}
|
|
80
103
|
|
|
81
104
|
export function keyProfileValue(profile: KeyDetectionOptions['profile'] | undefined): number {
|
|
82
105
|
if (profile === undefined) {
|
|
83
106
|
return -1;
|
|
84
107
|
}
|
|
85
|
-
|
|
86
|
-
return profile;
|
|
87
|
-
}
|
|
88
|
-
const names: Record<KeyProfileName, number> = {
|
|
89
|
-
ks: KeyProfileValues.KrumhanslSchmuckler,
|
|
90
|
-
krumhansl: KeyProfileValues.KrumhanslSchmuckler,
|
|
91
|
-
temperley: KeyProfileValues.Temperley,
|
|
92
|
-
shaath: KeyProfileValues.Shaath,
|
|
93
|
-
keyfinder: KeyProfileValues.Shaath,
|
|
94
|
-
'faraldo-edmt': KeyProfileValues.FaraldoEDMT,
|
|
95
|
-
edmt: KeyProfileValues.FaraldoEDMT,
|
|
96
|
-
'faraldo-edma': KeyProfileValues.FaraldoEDMA,
|
|
97
|
-
edma: KeyProfileValues.FaraldoEDMA,
|
|
98
|
-
'faraldo-edmm': KeyProfileValues.FaraldoEDMM,
|
|
99
|
-
edmm: KeyProfileValues.FaraldoEDMM,
|
|
100
|
-
'bellman-budge': KeyProfileValues.BellmanBudge,
|
|
101
|
-
bellman: KeyProfileValues.BellmanBudge,
|
|
102
|
-
};
|
|
103
|
-
return names[profile];
|
|
108
|
+
return resolveEnumOrdinal(profile, KEY_PROFILE_VALUES, 'key profile');
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
export function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): ChordAnalysisResult {
|
|
@@ -137,6 +142,11 @@ export function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult
|
|
|
137
142
|
return {
|
|
138
143
|
bpm: wasm.bpm,
|
|
139
144
|
bpmConfidence: wasm.bpmConfidence,
|
|
145
|
+
bpmCandidates: wasm.bpmCandidates.map((candidate) => ({
|
|
146
|
+
value: candidate.value,
|
|
147
|
+
confidence: candidate.confidence,
|
|
148
|
+
relation: candidate.relation,
|
|
149
|
+
})),
|
|
140
150
|
key: {
|
|
141
151
|
root: wasm.key.root as PitchClass,
|
|
142
152
|
mode: wasm.key.mode as Mode,
|
|
@@ -145,6 +155,7 @@ export function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult
|
|
|
145
155
|
shortName: wasm.key.shortName,
|
|
146
156
|
},
|
|
147
157
|
timeSignature: wasm.timeSignature,
|
|
158
|
+
timeSignatureCandidates: wasm.timeSignatureCandidates,
|
|
148
159
|
beatTimes,
|
|
149
160
|
beats: wasm.beats,
|
|
150
161
|
chords: wasm.chords.map((c) => ({
|
package/src/audio.ts
CHANGED
|
@@ -239,9 +239,14 @@ export class Audio {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* A copy of the raw audio samples. Mirrors Node's `getData()` contract: the
|
|
244
|
+
* returned array is independent of the Audio's internal buffer, so mutating
|
|
245
|
+
* it (or transferring it to a Worker) does not affect subsequent facade
|
|
246
|
+
* calls, which all read the internal snapshot directly.
|
|
247
|
+
*/
|
|
243
248
|
get data(): Float32Array {
|
|
244
|
-
return this._samples;
|
|
249
|
+
return this._samples.slice();
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
/** Number of samples. */
|
|
@@ -11,7 +11,17 @@ import type { ClipPageProvider, ClipPageRequest, RealtimeEngine } from './realti
|
|
|
11
11
|
*/
|
|
12
12
|
export interface ClipPageStreamerEngine {
|
|
13
13
|
/** Drain one pending audio-thread page-miss request, or `null` when empty. */
|
|
14
|
-
popClipPageRequest():
|
|
14
|
+
popClipPageRequest(): ClipPageStreamerRequest | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A page miss reported by either a direct engine (`sample`) or the worklet
|
|
19
|
+
* bridge (`pageIndex`). Exactly one position form is required.
|
|
20
|
+
*/
|
|
21
|
+
export interface ClipPageStreamerRequest {
|
|
22
|
+
clipId: number;
|
|
23
|
+
sample?: number;
|
|
24
|
+
pageIndex?: number;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
/** A paged clip the streamer keeps fed from its backing store. */
|
|
@@ -153,7 +163,13 @@ export class ClipPageStreamer {
|
|
|
153
163
|
if (!state) {
|
|
154
164
|
continue;
|
|
155
165
|
}
|
|
156
|
-
const page =
|
|
166
|
+
const page =
|
|
167
|
+
request.pageIndex !== undefined
|
|
168
|
+
? request.pageIndex
|
|
169
|
+
: Math.floor((request.sample ?? Number.NaN) / state.source.pageFrames);
|
|
170
|
+
if (!Number.isInteger(page) || page < 0 || page > state.lastPage) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
157
173
|
frontiers.set(request.clipId, page);
|
|
158
174
|
}
|
|
159
175
|
|
|
@@ -197,7 +213,7 @@ export class ClipPageStreamer {
|
|
|
197
213
|
// the bound by transiently holding old pages alongside new ones.
|
|
198
214
|
for (const page of state.resident.keys()) {
|
|
199
215
|
if (page < low || page > high) {
|
|
200
|
-
|
|
216
|
+
this.clearPage(state, page);
|
|
201
217
|
state.resident.delete(page);
|
|
202
218
|
}
|
|
203
219
|
}
|
|
@@ -217,7 +233,7 @@ export class ClipPageStreamer {
|
|
|
217
233
|
if (state.generation !== generation) {
|
|
218
234
|
// The fetch crossed a seek/reset boundary. supplyPage may have
|
|
219
235
|
// installed it after resetState's clear pass, so clear it again.
|
|
220
|
-
|
|
236
|
+
this.clearPage(state, pageIndex);
|
|
221
237
|
} else if (!ok && state.resident.get(pageIndex) === generation) {
|
|
222
238
|
state.resident.delete(pageIndex);
|
|
223
239
|
}
|
|
@@ -239,10 +255,20 @@ export class ClipPageStreamer {
|
|
|
239
255
|
state.generation += 1;
|
|
240
256
|
state.lastFrontier = null;
|
|
241
257
|
for (const page of state.resident.keys()) {
|
|
242
|
-
|
|
258
|
+
this.clearPage(state, page);
|
|
243
259
|
}
|
|
244
260
|
state.resident.clear();
|
|
245
261
|
}
|
|
262
|
+
|
|
263
|
+
private clearPage(state: SourceState, pageIndex: number): void {
|
|
264
|
+
// `clearPage` mirrors eviction into the AudioWorklet. Keep the older
|
|
265
|
+
// provider-only form working for direct-engine callers and test doubles.
|
|
266
|
+
if (state.source.binding.clearPage) {
|
|
267
|
+
state.source.binding.clearPage(pageIndex);
|
|
268
|
+
} else {
|
|
269
|
+
state.source.binding.provider.clear(pageIndex);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
246
272
|
}
|
|
247
273
|
|
|
248
274
|
export interface OpfsClipStreamOptions extends OpfsClipPageProviderOptions {
|
|
@@ -257,25 +283,53 @@ export interface OpfsClipStreamOptions extends OpfsClipPageProviderOptions {
|
|
|
257
283
|
|
|
258
284
|
export interface OpfsClipStream {
|
|
259
285
|
binding: OpfsClipPageProviderBinding;
|
|
260
|
-
/** Pass
|
|
286
|
+
/** Pass as `addClip`'s buffer argument to schedule the streaming clip. */
|
|
261
287
|
provider: ClipPageProvider;
|
|
262
288
|
}
|
|
263
289
|
|
|
290
|
+
/** Structural seam implemented by the AudioWorklet-backed `SonareEngine`. */
|
|
291
|
+
export interface WorkletOpfsClipStreamHost {
|
|
292
|
+
attachOpfsClipStream(options: OpfsClipStreamOptions): Promise<OpfsClipStream>;
|
|
293
|
+
}
|
|
294
|
+
|
|
264
295
|
/**
|
|
265
296
|
* One-call wiring of an OPFS-backed streaming clip: creates the page provider,
|
|
266
297
|
* primes the leading pages, and registers it with `streamer` so later misses are
|
|
267
298
|
* serviced within the bounded window. Returns the binding (for `close`) and the
|
|
268
|
-
* provider to schedule via `
|
|
299
|
+
* provider to schedule via `addClip(trackId, provider, startPpq, { id: clipId })`.
|
|
269
300
|
*
|
|
270
301
|
* @param streamer Shared streamer pumped on the control thread.
|
|
271
302
|
* @param engine Engine the provider is created on (the same one `streamer` drives).
|
|
272
303
|
* @param options Provider options plus `clipId` and optional `primePages`.
|
|
273
304
|
*/
|
|
274
|
-
export
|
|
305
|
+
export function attachOpfsClipStream(
|
|
275
306
|
streamer: ClipPageStreamer,
|
|
276
307
|
engine: RealtimeEngine,
|
|
277
308
|
options: OpfsClipStreamOptions,
|
|
309
|
+
): Promise<OpfsClipStream>;
|
|
310
|
+
/**
|
|
311
|
+
* Worklet overload: delegates to `SonareEngine.attachOpfsClipStream`, which
|
|
312
|
+
* creates the remote provider and drives this same sliding-window policy from
|
|
313
|
+
* worklet page-miss messages.
|
|
314
|
+
*/
|
|
315
|
+
export function attachOpfsClipStream(
|
|
316
|
+
engine: WorkletOpfsClipStreamHost,
|
|
317
|
+
options: OpfsClipStreamOptions,
|
|
318
|
+
): Promise<OpfsClipStream>;
|
|
319
|
+
export async function attachOpfsClipStream(
|
|
320
|
+
streamerOrEngine: ClipPageStreamer | WorkletOpfsClipStreamHost,
|
|
321
|
+
engineOrOptions: RealtimeEngine | OpfsClipStreamOptions,
|
|
322
|
+
maybeOptions?: OpfsClipStreamOptions,
|
|
278
323
|
): Promise<OpfsClipStream> {
|
|
324
|
+
if (!(streamerOrEngine instanceof ClipPageStreamer)) {
|
|
325
|
+
return streamerOrEngine.attachOpfsClipStream(engineOrOptions as OpfsClipStreamOptions);
|
|
326
|
+
}
|
|
327
|
+
const streamer = streamerOrEngine;
|
|
328
|
+
const engine = engineOrOptions as RealtimeEngine;
|
|
329
|
+
const options = maybeOptions;
|
|
330
|
+
if (!options) {
|
|
331
|
+
throw new Error('attachOpfsClipStream requires options.');
|
|
332
|
+
}
|
|
279
333
|
const { clipId, primePages = 1, ...providerOptions } = options;
|
|
280
334
|
const binding = createOpfsClipPageProvider(engine, providerOptions);
|
|
281
335
|
const lastPage = Math.ceil(providerOptions.numSamples / providerOptions.pageFrames) - 1;
|
package/src/codes.ts
CHANGED
|
@@ -1,68 +1,96 @@
|
|
|
1
|
-
import type { AutomationCurve, MeterTap,
|
|
1
|
+
import type { AutomationCurve, MeterTap, PanLawInput, PanMode, SendTiming } from './public_types';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return 3;
|
|
13
|
-
default:
|
|
14
|
-
throw new Error(`Invalid automation curve: ${curve}`);
|
|
3
|
+
/** Resolve a numeric ordinal in an inclusive range without coercion. */
|
|
4
|
+
export function resolveOrdinalInRange(
|
|
5
|
+
value: unknown,
|
|
6
|
+
min: number,
|
|
7
|
+
max: number,
|
|
8
|
+
enumName: string,
|
|
9
|
+
): number {
|
|
10
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
11
|
+
throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
|
|
15
12
|
}
|
|
13
|
+
return value;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/** Resolve a public enum spelling or ordinal without permitting unknown values. */
|
|
17
|
+
export function resolveEnumOrdinal(
|
|
18
|
+
value: unknown,
|
|
19
|
+
values: Readonly<Record<string, number>>,
|
|
20
|
+
enumName: string,
|
|
21
|
+
): number {
|
|
22
|
+
if (typeof value === 'number') {
|
|
23
|
+
const ordinals = Object.values(values);
|
|
24
|
+
const ordinal = resolveOrdinalInRange(
|
|
25
|
+
value,
|
|
26
|
+
Math.min(...ordinals),
|
|
27
|
+
Math.max(...ordinals),
|
|
28
|
+
enumName,
|
|
29
|
+
);
|
|
30
|
+
if (!ordinals.includes(ordinal)) {
|
|
31
|
+
throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
|
|
32
|
+
}
|
|
33
|
+
return ordinal;
|
|
21
34
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
case 'const6dB':
|
|
28
|
-
return 2;
|
|
29
|
-
case 'linear0dB':
|
|
30
|
-
return 3;
|
|
31
|
-
default:
|
|
32
|
-
throw new Error(`Invalid pan law: ${panLaw}`);
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
const ordinal = values[value];
|
|
37
|
+
if (ordinal !== undefined) {
|
|
38
|
+
return ordinal;
|
|
39
|
+
}
|
|
33
40
|
}
|
|
41
|
+
throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const AUTOMATION_CURVE_VALUES = {
|
|
45
|
+
linear: 0,
|
|
46
|
+
exponential: 1,
|
|
47
|
+
hold: 2,
|
|
48
|
+
's-curve': 3,
|
|
49
|
+
} as const;
|
|
50
|
+
const PAN_LAW_VALUES: Readonly<Record<string, number>> = {
|
|
51
|
+
const3db: 0,
|
|
52
|
+
'const-3db': 0,
|
|
53
|
+
'-3db': 0,
|
|
54
|
+
'const4.5db': 1,
|
|
55
|
+
'const-4.5db': 1,
|
|
56
|
+
'-4.5db': 1,
|
|
57
|
+
const6db: 2,
|
|
58
|
+
'const-6db': 2,
|
|
59
|
+
'-6db': 2,
|
|
60
|
+
linear0db: 3,
|
|
61
|
+
'linear-0db': 3,
|
|
62
|
+
linear: 3,
|
|
63
|
+
'0db': 3,
|
|
64
|
+
} as const;
|
|
65
|
+
const PAN_MODE_VALUES = {
|
|
66
|
+
balance: 0,
|
|
67
|
+
pan: 1,
|
|
68
|
+
stereopan: 1,
|
|
69
|
+
'stereo-pan': 1,
|
|
70
|
+
dualpan: 2,
|
|
71
|
+
'dual-pan': 2,
|
|
72
|
+
} as const;
|
|
73
|
+
const METER_TAP_VALUES = { preFader: 0, postFader: 1 } as const;
|
|
74
|
+
const SEND_TIMING_VALUES = { postFader: 0, preFader: 1 } as const;
|
|
75
|
+
const TRACK_MONITOR_MODE_VALUES = { off: 0, pfl: 1, afl: 2 } as const;
|
|
76
|
+
|
|
77
|
+
export function automationCurveCode(curve: AutomationCurve): number {
|
|
78
|
+
return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, 'automation curve');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function panLawCode(panLaw: PanLawInput): number {
|
|
82
|
+
const normalized = typeof panLaw === 'string' ? panLaw.toLowerCase().replace(/_/g, '-') : panLaw;
|
|
83
|
+
return resolveEnumOrdinal(normalized, PAN_LAW_VALUES, 'pan law');
|
|
34
84
|
}
|
|
35
85
|
|
|
36
86
|
export function panModeCode(panMode: PanMode | number): number {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
switch (panMode) {
|
|
41
|
-
case 'balance':
|
|
42
|
-
return 0;
|
|
43
|
-
case 'stereoPan':
|
|
44
|
-
case 'stereo-pan':
|
|
45
|
-
return 1;
|
|
46
|
-
case 'dualPan':
|
|
47
|
-
case 'dual-pan':
|
|
48
|
-
return 2;
|
|
49
|
-
default:
|
|
50
|
-
throw new Error(`Invalid pan mode: ${panMode}`);
|
|
51
|
-
}
|
|
87
|
+
const normalized =
|
|
88
|
+
typeof panMode === 'string' ? panMode.replace(/_/g, '-').toLowerCase() : panMode;
|
|
89
|
+
return resolveEnumOrdinal(normalized, PAN_MODE_VALUES, 'pan mode');
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
export function meterTapCode(tap: MeterTap | number): number {
|
|
55
|
-
|
|
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
|
-
}
|
|
93
|
+
return resolveEnumOrdinal(tap, METER_TAP_VALUES, 'meter tap');
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
export function sendTimingCode(timing: SendTiming | number): number {
|
|
@@ -70,15 +98,10 @@ export function sendTimingCode(timing: SendTiming | number): number {
|
|
|
70
98
|
// post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
|
|
71
99
|
// An unknown string is rejected rather than silently routed to post-fader,
|
|
72
100
|
// matching the sibling enum-code helpers and Node's sendTimingValue.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
case 'preFader':
|
|
80
|
-
return 1;
|
|
81
|
-
default:
|
|
82
|
-
throw new Error(`Invalid send timing: ${timing}`);
|
|
83
|
-
}
|
|
101
|
+
return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, 'send timing');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve a per-track PFL/AFL monitor mode to its C-ABI ordinal. */
|
|
105
|
+
export function trackMonitorModeCode(mode: unknown): number {
|
|
106
|
+
return resolveEnumOrdinal(mode, TRACK_MONITOR_MODE_VALUES, 'track monitor mode');
|
|
84
107
|
}
|
package/src/effects_mastering.ts
CHANGED