Files

234 lines
9.4 KiB
JavaScript

// Copyright (c) 2026 Nexmoe
const assert = require('assert');
const { createRequire } = require('module');
const { MessageChannel } = require('worker_threads');
const modulePath = process.argv[3] || require.resolve('../lib/sherpa-onnx.js');
const sherpa_onnx = require(modulePath);
const addon = createRequire(modulePath)('./addon.js');
/** Validate every native entry point without loading a model. */
function testValidation() {
assert.strictEqual(typeof sherpa_onnx.OfflineSourceSeparation, 'function');
for (const config of [undefined, null, true, 1, 'model', []]) {
assert.throws(
() => new sherpa_onnx.OfflineSourceSeparation(config), TypeError);
}
assert.throws(() => addon.createOfflineSourceSeparation(), TypeError);
assert.throws(
() => new sherpa_onnx.OfflineSourceSeparation({ model: {} }),
/source separation config/);
for (const numThreads of [0, -1, NaN, Infinity, 1.5, 2 ** 31]) {
assert.throws(
() => new sherpa_onnx.OfflineSourceSeparation({ model: { numThreads } }),
RangeError);
}
assert.throws(
() => new sherpa_onnx.OfflineSourceSeparation({ model: { numThreads: '1' } }),
TypeError);
const getterError = new Error('Config getter failed');
assert.throws(() => new sherpa_onnx.OfflineSourceSeparation({
model: {
spleeter: {
vocals: 'partially-allocated.onnx',
get accompaniment() { throw getterError; },
},
},
}), error => error === getterError);
const foreignHandle = addon.createLinearResampler(16000, 44100);
for (const handle of [undefined, null, {}, 1, foreignHandle]) {
assert.throws(
() => addon.offlineSourceSeparationGetOutputSampleRate(handle),
TypeError);
assert.throws(
() => addon.offlineSourceSeparationGetNumberOfStems(handle), TypeError);
assert.throws(
() => addon.offlineSourceSeparationProcess(handle, {}), TypeError);
}
assert.throws(
() => addon.offlineSourceSeparationGetOutputSampleRate(), TypeError);
assert.throws(
() => addon.offlineSourceSeparationGetNumberOfStems(), TypeError);
assert.throws(() => addon.offlineSourceSeparationProcess(), TypeError);
}
/** Reject malformed audio before it reaches native inference. */
function testInvalidAudio(separator) {
for (const request of [undefined, null, 1, [], {}]) {
assert.throws(() => separator.process(request), TypeError);
}
const valid = { samples: [new Float32Array(44100)], sampleRate: 44100 };
for (const samples of [null, {}, new Float32Array(10), [0],
[new Int16Array(10)], [new Float64Array(10)]]) {
assert.throws(() => separator.process({ ...valid, samples }), TypeError);
}
for (const samples of [[], [new Float32Array(0)],
[new Float32Array(10), new Float32Array(11)],
Array(3).fill(new Float32Array(10))]) {
assert.throws(() => separator.process({ ...valid, samples }), RangeError);
}
for (const sampleRate of [undefined, '44100', null]) {
assert.throws(() => separator.process({ ...valid, sampleRate }), TypeError);
}
for (const sampleRate of [0, -1, NaN, Infinity, 44100.5, 2 ** 31]) {
assert.throws(() => separator.process({ ...valid, sampleRate }), RangeError);
}
assert.throws(
() => separator.process({ ...valid, enableExternalBuffer: 1 }), TypeError);
for (const value of [NaN, Infinity, -Infinity]) {
assert.throws(
() => separator.process({ ...valid, samples: [new Float32Array([value])] }),
RangeError);
}
assert.throws(
() => addon.offlineSourceSeparationProcess(separator.handle), TypeError);
const samples = new Float32Array(10);
const { port1, port2 } = new MessageChannel();
try {
port1.postMessage(samples.buffer, [samples.buffer]);
assert.throws(
() => separator.process({ ...valid, samples: [samples] }), RangeError);
const first = new Float32Array(10);
const channels = [first];
Object.defineProperty(channels, 1, {
get() {
port1.postMessage(first.buffer, [first.buffer]);
return new Float32Array(10);
},
});
assert.throws(
() => separator.process({ ...valid, samples: channels }), RangeError);
} finally {
port1.close();
port2.close();
}
}
/** Check stem shape, duration, finite samples, and non-silent output. */
function checkOutput(output, separator, duration) {
assert.strictEqual(output.sampleRate, separator.sampleRate);
assert.strictEqual(output.stems.length, separator.numStems);
let energy = 0;
for (const stem of output.stems) {
assert.strictEqual(stem.length, 2);
assert.strictEqual(stem[0].length, stem[1].length);
for (const channel of stem) {
assert(channel instanceof Float32Array);
assert(channel.length > 0);
assert(Math.abs(channel.length / output.sampleRate - duration) < 0.1);
for (const sample of channel) {
assert(Number.isFinite(sample));
energy += sample * sample;
}
}
}
assert(energy > 0);
assert.notDeepStrictEqual(output.stems[0], output.stems[1]);
}
/** Let native finalizers run while a single output channel remains live. */
async function collectGarbage() {
assert.strictEqual(typeof global.gc, 'function', 'Run with --expose-gc');
for (let i = 0; i < 3; ++i) {
global.gc();
await new Promise(resolve => setImmediate(resolve));
}
}
/** Exercise real Spleeter or UVR inference through the freshly built addon. */
async function testModel(kind) {
assert(['spleeter', 'uvr'].includes(kind), 'Expected spleeter or uvr');
const model = kind === 'spleeter' ? {
spleeter: {
vocals: './sherpa-onnx-spleeter-2stems-fp16/vocals.fp16.onnx',
accompaniment:
'./sherpa-onnx-spleeter-2stems-fp16/accompaniment.fp16.onnx',
},
numThreads: 1,
provider: 'cpu',
debug: false,
} : {
uvr: { model: './UVR-MDX-NET-Voc_FT.onnx' },
numThreads: 1,
provider: 'cpu',
debug: 0,
};
let separator = new sherpa_onnx.OfflineSourceSeparation({ model });
assert.strictEqual(separator.numStems, 2);
assert.strictEqual(separator.sampleRate, 44100);
testInvalidAudio(separator);
const wave = sherpa_onnx.readWave('./qi-feng-le-zh.wav', false);
const n = Math.min(wave.samples.length, 3 * wave.sampleRate);
// Nonzero byte offsets catch bindings that ignore TypedArray views.
const left = new Float32Array(n + 4).subarray(1, n + 1);
const right = new Float32Array(n + 6).subarray(3, n + 3);
left.set(wave.samples.subarray(0, n));
right.set(left.map(sample => -0.5 * sample));
const pristine = [left.slice(), right.slice()];
const request = { samples: [left, right], sampleRate: wave.sampleRate };
let external = separator.process(request);
checkOutput(external, separator, n / wave.sampleRate);
let copied = separator.process({
...request, samples: pristine, enableExternalBuffer: false,
});
checkOutput(copied, separator, n / wave.sampleRate);
assert.deepStrictEqual(copied, external);
assert.deepStrictEqual([left, right], pristine);
let mono = separator.process({ ...request, samples: [left] });
let stereo = separator.process({ ...request, samples: [left, left] });
assert.deepStrictEqual(mono, stereo);
// The test wav is real stereo; readWaveMultiChannel must keep both channels
// while readWave keeps only the first one.
const multi = sherpa_onnx.readWaveMultiChannel('./qi-feng-le-zh.wav', false);
assert.strictEqual(multi.sampleRate, wave.sampleRate);
assert.deepStrictEqual(
multi.samples.map(channel => channel.length),
[wave.samples.length, wave.samples.length]);
assert.deepStrictEqual(multi.samples[0], wave.samples);
assert.notDeepStrictEqual(multi.samples[0], multi.samples[1]);
checkOutput(
separator.process({
samples: multi.samples.map(channel => channel.subarray(0, n)),
sampleRate: multi.sampleRate,
enableExternalBuffer: false,
}),
separator, n / multi.sampleRate);
const resampler = new sherpa_onnx.LinearResampler(wave.sampleRate, 16000);
const resampled = resampler.flush(left);
let resampledOutput = separator.process({
samples: [resampled], sampleRate: 16000, enableExternalBuffer: false,
});
checkOutput(resampledOutput, separator, resampled.length / 16000);
const retained = external.stems[1][1];
const expected = retained.slice();
const copiedChannel = copied.stems[0][0];
const expectedCopy = copiedChannel.slice();
separator = external = copied = mono = stereo = resampledOutput = null;
await collectGarbage();
assert.deepStrictEqual(retained, expected);
assert.deepStrictEqual(copiedChannel, expectedCopy);
console.log(`${kind}: inference, validation, mono/stereo, buffers, resampling, and GC passed`);
}
/** Run model-free checks, then optional real-model regression checks. */
async function main() {
testValidation();
if (process.argv[2]) await testModel(process.argv[2]);
console.log('Source separation tests passed');
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});