@audio/neural-separate 0.1.0 → 0.3.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/NOTICE +29 -1
- package/README.md +88 -15
- package/index.d.ts +47 -6
- package/package.json +10 -2
- package/scripts/export-mrx.py +191 -0
- package/scripts/export-scnet.py +274 -0
- package/scripts/export-tiger.py +304 -0
- package/scripts/reference.py +41 -2
- package/separate.js +241 -17
package/separate.js
CHANGED
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
// of sevagh/demucs.onnx. 7.8 s segments overlap-added as demucs.apply.apply_model does; each
|
|
13
13
|
// runs as waveform + complex-as-channels spectrogram in, frequency- and time-branch estimates
|
|
14
14
|
// out, summed after the iSTFT.
|
|
15
|
+
// 'complex': complex spectrogram models (SCNet: Tong, Zhang, Liu, Li, Yu, "SCNet: Sparse Compression
|
|
16
|
+
// Network for Music Source Separation", ICASSP 2024; the Band-Split RoFormer family has the same
|
|
17
|
+
// contract) with their STFT and iSTFT out of the graph: each channel's STFT in, re and im as
|
|
18
|
+
// channels, each source's out. 11 s segments overlap-added under linear fades, as
|
|
19
|
+
// ZFTurbo/Music-Source-Separation-Training's demix() runs them.
|
|
20
|
+
// 'multires': masking models over several STFT resolutions (MRX: Petermann, Wichern, Wang, Le Roux, "The Cocktail
|
|
21
|
+
// Fork Problem: Three-Stem Audio Separation for Real-World Soundtracks", ICASSP 2022), their STFTs and iSTFTs out of
|
|
22
|
+
// the graph: each channel's magnitudes in, a real mask per source and resolution out; a source is the sum of the
|
|
23
|
+
// iSTFTs of its masked spectrograms. The input at the loudness the model trained at, as upstream's separate.py sets it.
|
|
15
24
|
// 'waveform': any [1, C, N] → [1, S, C, N] graph.
|
|
16
25
|
//
|
|
17
26
|
// Long inputs run in overlapping chunks, so memory stays bounded by the chunk, not the file.
|
|
@@ -342,12 +351,40 @@ function complexInverse(Are, Aim, outRe, outIm, n, re, im) {
|
|
|
342
351
|
// Demucs source order (demucs/pretrained.py SOURCES), the S axis of its outputs.
|
|
343
352
|
const DEMUCS = ['drums', 'bass', 'other', 'vocals']
|
|
344
353
|
|
|
354
|
+
// SCNet's STFT (torch.stft's window=None: ones; normalized) and segment, 476 frames of 11 s
|
|
355
|
+
const SCNET = { modelType: 'complex', sampleRate: 44100, targets: DEMUCS, n: 4096, hop: 1024, window: 'ones', normalized: true, segment: 485100, frames: 476 }
|
|
356
|
+
|
|
357
|
+
// MRX's STFTs (mrx.py _stft: Hann, hop 256, centered, normalized) at three resolutions; its sources in its order
|
|
358
|
+
// (music, speech, sfx), named as a soundtrack's stems are; channels encoded apart, so mono stays mono; the input at
|
|
359
|
+
// -27 LUFS (separate.py's input_lufs, the stems scaled back); 20 s chunks (Node's peak 1.9 GB on 60 s, whole 4.4 GB)
|
|
360
|
+
const MRX = {
|
|
361
|
+
modelType: 'multires', sampleRate: 44100, targets: ['dialogue', 'music', 'effects'], sources: ['music', 'dialogue', 'effects'],
|
|
362
|
+
windows: [1024, 2048, 8192], hop: 256, loudness: -27, mono: true, chunk: 20, script: 'export-mrx.py',
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// TIGER's DnR model as scripts/export-tiger.py exports it (three band-split models, each its kept source, one graph): its
|
|
366
|
+
// STFT (Hann, unnormalized, centered), 12 s segments of 1034 frames every 4 s, as TIGERDNR.wav_chunk_inference cuts
|
|
367
|
+
// them; mono, channels apart
|
|
368
|
+
const TIGER = {
|
|
369
|
+
modelType: 'complex', sampleRate: 44100, targets: ['dialogue', 'music', 'effects'], sources: ['dialogue', 'music', 'effects'],
|
|
370
|
+
n: 2048, hop: 512, window: 'hann', normalized: false, segment: 529200, frames: 1034, step: 1 / 3, fade: 0, pad: 'zero',
|
|
371
|
+
standardize: false, mono: true, script: 'export-tiger.py',
|
|
372
|
+
}
|
|
373
|
+
|
|
345
374
|
// Presets: files are <weights>/<name>/<target>.onnx (one graph per target) or
|
|
346
375
|
// <weights>/<name>/<name>.onnx, as scripts/export-*.py write them.
|
|
347
376
|
export const models = {
|
|
348
377
|
umxhq: { modelType: 'openunmix', sampleRate: 44100, targets: ['vocals', 'drums', 'bass', 'other'], perTarget: true },
|
|
349
378
|
htdemucs: { modelType: 'hybrid', sampleRate: 44100, targets: DEMUCS },
|
|
350
379
|
htdemucs_ft: { modelType: 'hybrid', sampleRate: 44100, targets: DEMUCS, perTarget: true },
|
|
380
|
+
// SCNet-large and SCNet (starrytong/SCNet, MIT weights) as scripts/export-scnet.py exports them
|
|
381
|
+
'scnet-large': SCNET,
|
|
382
|
+
scnet: SCNET,
|
|
383
|
+
// MRX (merlresearch/cocktail-fork-separation, MIT weights) as scripts/export-mrx.py exports it: dialogue, music
|
|
384
|
+
// and effects of a soundtrack (Divide and Remaster)
|
|
385
|
+
mrx: MRX,
|
|
386
|
+
// TIGER (JusperLee/TIGER, Apache-2.0 weights): the same three stems, three band-split models, about 50 times MRX's time
|
|
387
|
+
tiger: TIGER,
|
|
351
388
|
}
|
|
352
389
|
|
|
353
390
|
async function cacheDir() {
|
|
@@ -376,7 +413,7 @@ async function checkLocal(graphs, name) {
|
|
|
376
413
|
let [{ existsSync }, { fileURLToPath }] = await Promise.all([import('node:fs'), import('node:url')])
|
|
377
414
|
let missing = files.map(f => fileURLToPath(f)).filter(f => !existsSync(f))
|
|
378
415
|
if (!missing.length) return
|
|
379
|
-
let script = models[name].
|
|
416
|
+
let script = models[name].script ?? { hybrid: 'export-htdemucs.py', complex: 'export-scnet.py' }[models[name].modelType] ?? 'export-openunmix.py'
|
|
380
417
|
throw new Error(`neural-separate: ${name} weights not found: ${missing.join(', ')}. ` +
|
|
381
418
|
`Produce them with python3 node_modules/@audio/neural-separate/scripts/${script} --model ${name}, ` +
|
|
382
419
|
`or pass opts.weights: a URL or directory holding ${name}/<file>.onnx`)
|
|
@@ -395,12 +432,12 @@ async function resolveModel(opts) {
|
|
|
395
432
|
let p = models[model], base = await weightsBase(opts, model)
|
|
396
433
|
if (want) for (let t of want) if (!p.targets.includes(t)) throw new Error(`neural-separate: ${model} has no target '${t}' (has ${p.targets.join(', ')})`)
|
|
397
434
|
let targets = pick(p.targets)
|
|
398
|
-
let sources = p.modelType === 'hybrid' ? DEMUCS : null
|
|
435
|
+
let sources = p.sources ?? (p.modelType === 'hybrid' || p.modelType === 'complex' ? DEMUCS : null)
|
|
399
436
|
let graphs = p.perTarget
|
|
400
437
|
? targets.map(t => ({ spec: `${base}${model}/${t}.onnx`, sources: sources ?? [t], keep: [t] }))
|
|
401
438
|
: [{ spec: `${base}${model}/${model}.onnx`, sources: sources ?? p.targets, keep: targets }]
|
|
402
439
|
if (isNode) await checkLocal(graphs, model)
|
|
403
|
-
return { graphs, modelType: p.modelType, sampleRate: p.sampleRate, targets }
|
|
440
|
+
return { graphs, modelType: p.modelType, sampleRate: p.sampleRate, targets, preset: p }
|
|
404
441
|
}
|
|
405
442
|
|
|
406
443
|
let graphs
|
|
@@ -542,6 +579,23 @@ async function separateSpectral(channels, rate, opts, graphs, modelType, load) {
|
|
|
542
579
|
|
|
543
580
|
// ------------------------------------------------------------------ hybrid
|
|
544
581
|
|
|
582
|
+
// The input over its whole length: (x − mean) / deviation of the channels' mean (demucs.api, and demix's callers
|
|
583
|
+
// with inference.normalize); back(ch) scales a source back and adds the mean, as both do to every source; scale(ch)
|
|
584
|
+
// only scales it back, the mean (the mixture's DC) left to no source.
|
|
585
|
+
function normalize(channels) {
|
|
586
|
+
let C = channels.length, N = channels[0].length, mono = new Float64Array(N), mean = 0, sq = 0
|
|
587
|
+
for (let x of channels) for (let i = 0; i < N; i++) mono[i] += x[i] / C
|
|
588
|
+
for (let i = 0; i < N; i++) mean += mono[i]
|
|
589
|
+
mean /= N
|
|
590
|
+
for (let i = 0; i < N; i++) sq += (mono[i] - mean) ** 2
|
|
591
|
+
let std = Math.sqrt(sq / Math.max(1, N - 1)) + 1e-8
|
|
592
|
+
return {
|
|
593
|
+
norm: channels.map(x => Float32Array.from(x, v => (v - mean) / std)),
|
|
594
|
+
back: ch => { for (let i = 0; i < ch.length; i++) ch[i] = ch[i] * std + mean; return ch },
|
|
595
|
+
scale: ch => { for (let i = 0; i < ch.length; i++) ch[i] *= std; return ch },
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
545
599
|
// One segment through a demucs.onnx-contract graph: STFT and iSTFT as HTDemucs._spec,
|
|
546
600
|
// _magnitude, _mask and _ispec do them (demucs/htdemucs.py, spec.py: torch.stft with
|
|
547
601
|
// normalized=True, n_fft 4096, hop 1024; the Nyquist bin dropped; reflect re-padding by 3/4
|
|
@@ -585,15 +639,7 @@ async function hybridSegment(session, mix, want, n, hop) {
|
|
|
585
639
|
// triangular window normalized by its sum.
|
|
586
640
|
async function separateHybrid(channels, rate, opts, graphs, load) {
|
|
587
641
|
let n = opts.n ?? 4096, hop = opts.hop ?? 1024
|
|
588
|
-
let C = channels.length, N = channels[0].length
|
|
589
|
-
let mono = new Float64Array(N)
|
|
590
|
-
for (let x of channels) for (let i = 0; i < N; i++) mono[i] += x[i] / C
|
|
591
|
-
let mean = 0, sq = 0
|
|
592
|
-
for (let i = 0; i < N; i++) mean += mono[i]
|
|
593
|
-
mean /= N
|
|
594
|
-
for (let i = 0; i < N; i++) sq += (mono[i] - mean) ** 2
|
|
595
|
-
let std = Math.sqrt(sq / Math.max(1, N - 1)) + 1e-8
|
|
596
|
-
let norm = channels.map(x => Float32Array.from(x, v => (v - mean) / std))
|
|
642
|
+
let C = channels.length, N = channels[0].length, { norm, back } = normalize(channels)
|
|
597
643
|
|
|
598
644
|
let result = {}
|
|
599
645
|
for (let g of graphs) {
|
|
@@ -615,7 +661,7 @@ async function separateHybrid(channels, rate, opts, graphs, load) {
|
|
|
615
661
|
g.keep.forEach((name, i) => { res[name] = stems[i].map(ch => ch.subarray(off, off + len)) })
|
|
616
662
|
return res
|
|
617
663
|
}, opts.progress)
|
|
618
|
-
for (let name in out) result[name] = out[name].map(
|
|
664
|
+
for (let name in out) result[name] = out[name].map(back)
|
|
619
665
|
} finally {
|
|
620
666
|
session.free?.()
|
|
621
667
|
}
|
|
@@ -623,6 +669,176 @@ async function separateHybrid(channels, rate, opts, graphs, load) {
|
|
|
623
669
|
return result
|
|
624
670
|
}
|
|
625
671
|
|
|
672
|
+
// ------------------------------------------------------------------ complex
|
|
673
|
+
|
|
674
|
+
// One segment through a complex-spectrogram graph, [1, 2C, F, T] in (channel c's STFT at 2c, re, and 2c + 1, im),
|
|
675
|
+
// [1, 2·S·C, F, T] out (source s, channel c at 2(sC + c), re, and the next, im): the segment zero-padded to
|
|
676
|
+
// (T − 1)·hop samples when shorter (SCNet.forward pads so), centered reflect-padded frames, × `scale` (normalized:
|
|
677
|
+
// 1/√n) and back. seg: C channels of L samples; returns the sources at indices `want`, C channels of L samples each.
|
|
678
|
+
async function complexSegment(session, seg, want, n, hop, win, scale, T) {
|
|
679
|
+
let C = seg.length, L = seg[0].length, F = (n >> 1) + 1, spec = new Float32Array(2 * C * F * T)
|
|
680
|
+
seg.forEach((x, c) => {
|
|
681
|
+
let padded = new Float64Array(Math.max(L, (T - 1) * hop))
|
|
682
|
+
padded.set(x)
|
|
683
|
+
let { re, im } = stftFlat(padded, n, hop, win, true)
|
|
684
|
+
let zr = spec.subarray(2 * c * F * T), zi = spec.subarray((2 * c + 1) * F * T)
|
|
685
|
+
for (let f = 0, k = 0; f < F; f++) for (let t = 0, i = f; t < T; t++, i += F, k++) { zr[k] = re[i] * scale; zi[k] = im[i] * scale }
|
|
686
|
+
})
|
|
687
|
+
let name = session.inputs?.[0]?.name ?? 'mix_spec'
|
|
688
|
+
let z = outputOf(session, await session.run({ [name]: tensor(spec, [1, 2 * C, F, T], 'float32') })).data
|
|
689
|
+
let env = envelope(T, n, hop, win)
|
|
690
|
+
return want.map(s => Array.from({ length: C }, (_, c) => {
|
|
691
|
+
let re = new Float64Array(T * F), im = new Float64Array(T * F), b = 2 * (s * C + c) * F * T
|
|
692
|
+
// the [F, T] planes read in order, frames written strided
|
|
693
|
+
for (let f = 0, k = b; f < F; f++) for (let t = 0, i = f; t < T; t++, i += F, k++) { re[i] = z[k] / scale; im[i] = z[k + F * T] / scale }
|
|
694
|
+
let out = new Float64Array((T - 1) * hop + n)
|
|
695
|
+
ola(rows(re, F), rows(im, F), out, 0, n, hop, win)
|
|
696
|
+
return olaFinish(out, env, n, true, L)
|
|
697
|
+
}))
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// demix() of Music-Source-Separation-Training (utils/model_utils.py, generic mode): segments of L samples every
|
|
701
|
+
// `step` of L, each weighted by a window fading in and out linearly over L/10 (the first not in, the last not out),
|
|
702
|
+
// the sum divided by the summed weights. An input longer than two segments less a step is first reflected L − step
|
|
703
|
+
// samples out on each side; a segment running past the end is reflected out when more than half of it is input,
|
|
704
|
+
// zero-padded when not. The input is normalized by its mean and deviation, and the sources scaled back; demix's
|
|
705
|
+
// callers add the mean back to every source, here to none: the stems sum to the input less its DC, which stays in
|
|
706
|
+
// the residual. And two departures: the segments stop at the first to reach the end (demix runs on while one
|
|
707
|
+
// starts before it: a 7 s song took three 11 s segments, now one, its SDR within 0.02 dB), and the first and last
|
|
708
|
+
// fades are set per segment (demix sets them per batch of 8: its first segment fades in, every one of a last
|
|
709
|
+
// batch holds).
|
|
710
|
+
// TIGER's DnR model (its preset's `step` 1/3, `fade` 0, `pad` 'zero', `standardize` false, `mono`) runs the segments
|
|
711
|
+
// TIGERDNR.wav_chunk_inference cuts: 12 s every 4 s, unweighted, the input zero-padded by 8 s at each end and the last
|
|
712
|
+
// segments with zeros, each channel apart, nothing normalized; its sum divided by the three segments over every
|
|
713
|
+
// sample, as the summed weights divide it here.
|
|
714
|
+
async function separateComplex(channels, rate, opts, graphs, load, p) {
|
|
715
|
+
let n = opts.n ?? p?.n ?? 4096, hop = opts.hop ?? p?.hop ?? 1024
|
|
716
|
+
let win = (opts.window ?? p?.window) === 'ones' ? new Float64Array(n).fill(1) : hann(n), scale = (opts.normalized ?? p?.normalized ?? true) ? 1 / Math.sqrt(n) : 1
|
|
717
|
+
let C = channels.length, N = channels[0].length, zero = (opts.pad ?? p?.pad) === 'zero'
|
|
718
|
+
let { norm, scale: back } = opts.standardize ?? p?.standardize ?? true ? normalize(channels) : { norm: channels, scale: c => c }
|
|
719
|
+
let result = {}
|
|
720
|
+
for (let g of graphs) {
|
|
721
|
+
let session = await load(g.spec)
|
|
722
|
+
try {
|
|
723
|
+
let T = session.inputs?.[0]?.dims?.[3]
|
|
724
|
+
T = typeof T === 'number' && T > 0 ? T : opts.frames ?? p?.frames
|
|
725
|
+
let L = opts.segment ?? p?.segment ?? (T - 1) * hop, step = Math.max(1, Math.floor(L * (opts.step ?? p?.step ?? 0.25)))
|
|
726
|
+
let border = L - step, wide = border > 0 && (zero || N > 2 * border)
|
|
727
|
+
let x = wide ? norm.map(c => Float32Array.from({ length: N + 2 * border }, (_, i) => zero ? (i < border || i >= border + N ? 0 : c[i - border]) : c[reflectIndex(i - border, N)])) : norm
|
|
728
|
+
let M = x[0].length, fade = Math.floor(L * (opts.fade ?? p?.fade ?? 0.1)), starts = []
|
|
729
|
+
for (let s = 0; ; s += step) { starts.push(s); if (s + L >= M) break }
|
|
730
|
+
let keep = g.keep.map(name => g.sources.indexOf(name))
|
|
731
|
+
let acc = g.keep.map(() => Array.from({ length: C }, () => new Float64Array(M))), wsum = new Float64Array(M)
|
|
732
|
+
for (let k = 0; k < starts.length; k++) {
|
|
733
|
+
let start = starts[k], len = Math.min(L, M - start), reflect = !zero && len > L / 2
|
|
734
|
+
opts.progress?.({ chunk: k + 1, totalChunks: starts.length })
|
|
735
|
+
let seg = x.map(c => { let s = new Float32Array(L); for (let i = 0; i < L; i++) s[i] = i < len ? c[start + i] : reflect ? c[start + reflectIndex(i, len)] : 0; return s })
|
|
736
|
+
// a mono model hears each channel apart
|
|
737
|
+
let stems = p?.mono ? [] : await complexSegment(session, seg, keep, n, hop, win, scale, T)
|
|
738
|
+
if (p?.mono) for (let c = 0; c < C; c++) (await complexSegment(session, [seg[c]], keep, n, hop, win, scale, T)).forEach((st, j) => (stems[j] ??= [])[c] = st[0])
|
|
739
|
+
for (let i = 0; i < len; i++) {
|
|
740
|
+
let w = 1
|
|
741
|
+
if (k > 0 && i < fade) w = i / (fade - 1)
|
|
742
|
+
if (k < starts.length - 1 && i >= L - fade) w = Math.min(w, (L - 1 - i) / (fade - 1))
|
|
743
|
+
wsum[start + i] += w
|
|
744
|
+
for (let j = 0; j < keep.length; j++) for (let c = 0; c < C; c++) acc[j][c][start + i] += stems[j][c][i] * w
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
g.keep.forEach((name, j) => {
|
|
748
|
+
result[name] = acc[j].map(a => back(Float32Array.from({ length: N }, (_, i) => wsum[i + (wide ? border : 0)] > 0 ? a[i + (wide ? border : 0)] / wsum[i + (wide ? border : 0)] : 0)))
|
|
749
|
+
})
|
|
750
|
+
} finally {
|
|
751
|
+
session.free?.()
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return result
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ------------------------------------------------------------------ multires
|
|
758
|
+
|
|
759
|
+
// Each channel apart (B = 1), chunk by chunk: its STFT at every resolution (Hann windows of `windows` samples, one
|
|
760
|
+
// hop, centered), the magnitudes scaled 1/√n (torch.stft's normalized=True) in as mag_<n> [1, F, T], masks out as
|
|
761
|
+
// mask_<n> [1, S, F, T] in the graph's source order; a source is the sum over resolutions of the iSTFT of its mask
|
|
762
|
+
// times the spectrogram (the masks are real: the 1/√n and the iSTFT's √n cancel).
|
|
763
|
+
async function separateMultires(channels, rate, opts, graphs, load, p) {
|
|
764
|
+
let windows = opts.windows ?? p?.windows, hop = opts.hop ?? p?.hop ?? 256
|
|
765
|
+
let C = channels.length, N = channels[0].length, result = {}
|
|
766
|
+
for (let g of graphs) {
|
|
767
|
+
let session = await load(g.spec)
|
|
768
|
+
try {
|
|
769
|
+
let { starts, size, weight } = crossfadeChunks(N, rate, { ...opts, chunk: opts.chunk ?? p?.chunk })
|
|
770
|
+
Object.assign(result, await overlapAdd(N, C, starts, size, weight, async (start, len) => {
|
|
771
|
+
let res = Object.fromEntries(g.keep.map(name => [name, []]))
|
|
772
|
+
for (let c = 0; c < C; c++) {
|
|
773
|
+
let x = channels[c].subarray(start, start + len), feeds = {}
|
|
774
|
+
// the spectra kept in float32 (the 8192 window's alone 113 MB a 20 s chunk), one masked copy reused
|
|
775
|
+
let specs = windows.map((n, r) => {
|
|
776
|
+
let { re, im, T, bins: F } = stftFlat(x, n, hop, hann(n), true), mag = new Float32Array(F * T), k = 1 / Math.sqrt(n)
|
|
777
|
+
for (let t = 0, i = 0; t < T; t++) for (let f = 0; f < F; f++, i++) mag[f * T + t] = Math.hypot(re[i], im[i]) * k
|
|
778
|
+
feeds[session.inputs?.[r]?.name ?? `mag_${n}`] = tensor(mag, [1, F, T], 'float32')
|
|
779
|
+
return { re: Float32Array.from(re), im: Float32Array.from(im), T, F }
|
|
780
|
+
})
|
|
781
|
+
let out = await session.run(feeds), acc = g.keep.map(() => new Float64Array(len))
|
|
782
|
+
specs.forEach(({ re, im, T, F }, r) => {
|
|
783
|
+
let n = windows[r], m = outputOf(session, out, r).data, mr = new Float64Array(T * F), mi = new Float64Array(T * F)
|
|
784
|
+
g.keep.forEach((name, j) => {
|
|
785
|
+
let b = g.sources.indexOf(name) * F * T
|
|
786
|
+
for (let f = 0; f < F; f++) for (let t = 0, i = f, q = b + f * T; t < T; t++, i += F, q++) { mr[i] = re[i] * m[q]; mi[i] = im[i] * m[q] }
|
|
787
|
+
let o = new Float64Array((T - 1) * hop + n)
|
|
788
|
+
ola(rows(mr, F), rows(mi, F), o, 0, n, hop, hann(n))
|
|
789
|
+
let y = olaFinish(o, envelope(T, n, hop, hann(n)), n, true, len)
|
|
790
|
+
for (let i = 0; i < len; i++) acc[j][i] += y[i]
|
|
791
|
+
})
|
|
792
|
+
})
|
|
793
|
+
g.keep.forEach((name, j) => res[name].push(acc[j]))
|
|
794
|
+
}
|
|
795
|
+
return res
|
|
796
|
+
}, opts.progress))
|
|
797
|
+
} finally {
|
|
798
|
+
session.free?.()
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return result
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ------------------------------------------------------------------ loudness
|
|
805
|
+
|
|
806
|
+
// Integrated loudness, ITU-R BS.1770-4, as pyloudnorm 0.2 measures it (upstream MRX's separate.py sets its input with
|
|
807
|
+
// it): K-weighting by pyloudnorm's two biquads at any rate (a 4 dB shelf over 1.5 kHz, a 38 Hz high-pass), 400 ms
|
|
808
|
+
// blocks every 100 ms, gated at -70 LUFS, then 10 LU under the mean of the rest; -Infinity for silence.
|
|
809
|
+
function lufs(channels, rate) {
|
|
810
|
+
let shelf = (G, Q, fc) => {
|
|
811
|
+
let A = 10 ** (G / 40), w = 2 * Math.PI * fc / rate, al = Math.sin(w) / (2 * Q), c = Math.cos(w), r = 2 * Math.sqrt(A) * al
|
|
812
|
+
return [A * ((A + 1) + (A - 1) * c + r), -2 * A * ((A - 1) + (A + 1) * c), A * ((A + 1) + (A - 1) * c - r), (A + 1) - (A - 1) * c + r, 2 * ((A - 1) - (A + 1) * c), (A + 1) - (A - 1) * c - r]
|
|
813
|
+
}
|
|
814
|
+
let pass = (Q, fc) => { let w = 2 * Math.PI * fc / rate, al = Math.sin(w) / (2 * Q), c = Math.cos(w); return [(1 + c) / 2, -(1 + c), (1 + c) / 2, 1 + al, -2 * c, 1 - al] }
|
|
815
|
+
let filters = [shelf(4, 1 / Math.sqrt(2), 1500), pass(0.5, 38)], N = channels[0].length, T = 0.4, step = 0.25
|
|
816
|
+
// numpy's round, half to even, as pyloudnorm counts its blocks
|
|
817
|
+
let even = v => { let f = Math.floor(v), d = v - f; return d > 0.5 || (d === 0.5 && f % 2) ? f + 1 : f }
|
|
818
|
+
let blocks = even((N / rate - T) / (T * step)) + 1
|
|
819
|
+
if (!(blocks > 0)) return -Infinity
|
|
820
|
+
let z = new Float64Array(blocks), G = [1, 1, 1, 1.41, 1.41]
|
|
821
|
+
channels.forEach((x, c) => {
|
|
822
|
+
let y = Float64Array.from(x)
|
|
823
|
+
for (let [b0, b1, b2, a0, a1, a2] of filters) {
|
|
824
|
+
// scipy.signal.lfilter from rest: direct form II transposed, coefficients over a0
|
|
825
|
+
b0 /= a0; b1 /= a0; b2 /= a0; a1 /= a0; a2 /= a0
|
|
826
|
+
let z1 = 0, z2 = 0
|
|
827
|
+
for (let i = 0; i < N; i++) { let v = y[i], o = b0 * v + z1; z1 = b1 * v - a1 * o + z2; z2 = b2 * v - a2 * o; y[i] = o }
|
|
828
|
+
}
|
|
829
|
+
for (let j = 0; j < blocks; j++) {
|
|
830
|
+
let lo = Math.trunc(T * (j * step) * rate), hi = Math.min(N, Math.trunc(T * (j * step + 1) * rate)), s = 0
|
|
831
|
+
for (let i = lo; i < hi; i++) s += y[i] * y[i]
|
|
832
|
+
z[j] += (G[c] ?? 1) * s / (T * rate)
|
|
833
|
+
}
|
|
834
|
+
})
|
|
835
|
+
let L = v => -0.691 + 10 * Math.log10(v), mean = v => v.reduce((a, b) => a + b, 0) / v.length
|
|
836
|
+
let abs = [...z].filter(v => L(v) >= -70)
|
|
837
|
+
if (!abs.length) return -Infinity
|
|
838
|
+
let rel = L(mean(abs)) - 10, gated = [...z].filter(v => L(v) > rel && L(v) > -70)
|
|
839
|
+
return gated.length ? L(mean(gated)) : -Infinity
|
|
840
|
+
}
|
|
841
|
+
|
|
626
842
|
// ------------------------------------------------------------------ waveform
|
|
627
843
|
|
|
628
844
|
// [1, C, N] in, [1, S, C, N] out, contiguous per (source, channel).
|
|
@@ -658,8 +874,6 @@ function normalizeAudio(audio, opts) {
|
|
|
658
874
|
else throw new Error('neural-separate: audio must be Float32Array[] or { channelData, sampleRate }')
|
|
659
875
|
if (!sampleRate) throw new Error('neural-separate: sampleRate is required (opts.sampleRate, or audio.sampleRate)')
|
|
660
876
|
if (!channelData.length) throw new Error('neural-separate: audio has no channels')
|
|
661
|
-
// mono → duplicated stereo, as openunmix.utils.preprocess does
|
|
662
|
-
if (channelData.length === 1) channelData = [channelData[0], channelData[0]]
|
|
663
877
|
return { channelData, sampleRate }
|
|
664
878
|
}
|
|
665
879
|
|
|
@@ -677,20 +891,30 @@ export default async function separate(audio, opts = {}) {
|
|
|
677
891
|
if (opts.dtype && opts.dtype !== 'float32') throw new Error(`neural-separate: dtype '${opts.dtype}' not supported: only 'float32' tensor marshalling is implemented`)
|
|
678
892
|
|
|
679
893
|
let { channelData, sampleRate: rate } = normalizeAudio(audio, opts)
|
|
680
|
-
let { graphs, modelType, sampleRate: modelRate, targets } = await resolveModel(opts)
|
|
894
|
+
let { graphs, modelType, sampleRate: modelRate, targets, preset } = await resolveModel(opts)
|
|
895
|
+
// mono → duplicated stereo, as openunmix.utils.preprocess does; a model hearing channels apart takes it as it is
|
|
896
|
+
if (channelData.length === 1 && !preset?.mono) channelData = [channelData[0], channelData[0]]
|
|
681
897
|
let empty = () => channelData.map(() => new Float32Array(0))
|
|
682
898
|
if (!channelData[0].length) return { stems: Object.fromEntries(targets.map(t => [t, empty()])), sampleRate: rate, residual: empty() }
|
|
683
899
|
let targetRate = opts.targetRate ?? modelRate ?? rate
|
|
684
900
|
let proc = targetRate !== rate ? channelData.map(c => resampleSinc(c, { from: rate, to: targetRate })) : channelData
|
|
685
901
|
let load = loader(opts)
|
|
902
|
+
// the input at the loudness the model trained at, its stems scaled back (a silent input as it is)
|
|
903
|
+
let level = preset?.loudness == null ? -Infinity : lufs(proc, targetRate), gain = Number.isFinite(level) ? 10 ** ((preset.loudness - level) / 20) : 1
|
|
904
|
+
if (gain !== 1) proc = proc.map(c => c.map(v => v * gain))
|
|
686
905
|
|
|
687
906
|
let stemsAtRate = modelType === 'hybrid' ? await separateHybrid(proc, targetRate, opts, graphs, load)
|
|
907
|
+
: modelType === 'complex' ? await separateComplex(proc, targetRate, opts, graphs, load, preset)
|
|
908
|
+
: modelType === 'multires' ? await separateMultires(proc, targetRate, opts, graphs, load, preset)
|
|
688
909
|
: modelType === 'waveform' ? await separateWaveform(proc, targetRate, opts, graphs, load)
|
|
689
910
|
: await separateSpectral(proc, targetRate, opts, graphs, modelType, load)
|
|
690
911
|
|
|
691
912
|
let N = channelData[0].length
|
|
692
913
|
let stems = {}
|
|
693
|
-
for (let name of targets) stems[name] = stemsAtRate[name].map(c =>
|
|
914
|
+
for (let name of targets) stems[name] = stemsAtRate[name].map(c => {
|
|
915
|
+
if (gain !== 1) c = Float32Array.from(c, v => v / gain)
|
|
916
|
+
return fitLength(targetRate !== rate ? resampleSinc(c, { from: targetRate, to: rate }) : c, N)
|
|
917
|
+
})
|
|
694
918
|
|
|
695
919
|
let residual = channelData.map((c, ci) => {
|
|
696
920
|
let r = Float32Array.from(c)
|