@audio/stretch-sms 1.0.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/LICENSE +25 -0
- package/README.md +29 -0
- package/package.json +48 -0
- package/sms.d.ts +17 -0
- package/sms.js +344 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Dmitry Iv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This work is offered under the Krishnized License (https://github.com/krishnized/license).
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @audio/stretch-sms
|
|
2
|
+
|
|
3
|
+
Sinusoidal Modeling Synthesis (Serra 1989, McAulay-Quatieri 1986). Decomposes audio into individually tracked sinusoidal partials and resynthesizes at the new time rate — frequency and magnitude of each partial interpolated independently, no bin-by-bin smearing.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import sms from '@audio/stretch-sms'
|
|
7
|
+
|
|
8
|
+
sms(data, { factor: 2 })
|
|
9
|
+
sms(data, { factor: 0.5, maxTracks: 80 })
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Param | Default | |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `factor` | `1` | Time stretch ratio |
|
|
15
|
+
| `frameSize` | `2048` | FFT frame size |
|
|
16
|
+
| `hopSize` | `frameSize/4` | Hop between frames |
|
|
17
|
+
| `maxTracks` | `60` | Max simultaneous sinusoidal tracks |
|
|
18
|
+
| `minMag` | `1e-4` | Peak detection threshold (linear) |
|
|
19
|
+
| `freqDev` | `3` | Max frequency deviation (bins) for track continuation |
|
|
20
|
+
| `residualMix` | `1` | Stochastic residual blended into output |
|
|
21
|
+
|
|
22
|
+
**Use when:** harmonic / tonal content — instruments, chords, vocals — where the phase vocoder smears.<br>
|
|
23
|
+
**Not for:** noise-dominated material.
|
|
24
|
+
|
|
25
|
+
Part of [`@audio/stretch`](../..).
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
[MIT](./LICENSE) · [ॐ](https://github.com/krishnized/license/)
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/stretch-sms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sinusoidal Modeling Synthesis time stretching — harmonic/tonal material",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "sms.js",
|
|
8
|
+
"types": "sms.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./sms.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"sms.js",
|
|
15
|
+
"sms.d.ts",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@audio/stretch-core": "^1.0.0",
|
|
21
|
+
"fourier-transform": "^2.3.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"audio",
|
|
25
|
+
"dsp",
|
|
26
|
+
"time-stretch",
|
|
27
|
+
"sms",
|
|
28
|
+
"sinusoidal",
|
|
29
|
+
"partial-tracking",
|
|
30
|
+
"serra",
|
|
31
|
+
"mcaulay-quatieri"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
38
|
+
"homepage": "https://github.com/audiojs/stretch/tree/main/packages/stretch-sms",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/audiojs/stretch.git",
|
|
42
|
+
"directory": "packages/stretch-sms"
|
|
43
|
+
},
|
|
44
|
+
"bugs": "https://github.com/audiojs/stretch/issues",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/sms.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StreamWriter } from '@audio/stretch-core'
|
|
2
|
+
|
|
3
|
+
export interface SmsOpts {
|
|
4
|
+
factor?: number
|
|
5
|
+
frameSize?: number
|
|
6
|
+
hopSize?: number
|
|
7
|
+
maxTracks?: number
|
|
8
|
+
minMag?: number
|
|
9
|
+
freqDev?: number
|
|
10
|
+
residualMix?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare const sms: {
|
|
14
|
+
(data: Float32Array, opts?: SmsOpts): Float32Array
|
|
15
|
+
(opts?: SmsOpts): StreamWriter
|
|
16
|
+
}
|
|
17
|
+
export default sms
|
package/sms.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Sinusoidal Modeling Synthesis time stretching.
|
|
2
|
+
// Tracks individual sinusoidal partials across frames, resynthesizes at new rate.
|
|
3
|
+
// Each harmonic is independently controlled — no phase spreading or bin-by-bin artifacts.
|
|
4
|
+
//
|
|
5
|
+
// References:
|
|
6
|
+
// - Serra, X. (1989). "A System for Sound Analysis/Transformation/Synthesis
|
|
7
|
+
// Based on a Deterministic plus Stochastic Decomposition." PhD thesis, Stanford.
|
|
8
|
+
// - McAulay, R.J. & Quatieri, T.F. (1986). "Speech analysis/synthesis based on
|
|
9
|
+
// a sinusoidal representation." IEEE Trans. ASSP, 34(4).
|
|
10
|
+
|
|
11
|
+
import { fft, ifft } from 'fourier-transform'
|
|
12
|
+
import { hannWindow, clamp, normalize, writer, makeStreamBufs, PI2 } from '@audio/stretch-core'
|
|
13
|
+
|
|
14
|
+
function createNoiseState(seed = 0x12345678) {
|
|
15
|
+
return { seed: seed >>> 0 }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nextNoisePhase(state) {
|
|
19
|
+
state.seed = (state.seed * 1664525 + 1013904223) >>> 0
|
|
20
|
+
return state.seed / 0x100000000 * PI2
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Tracks and peaks are structure-of-arrays: parallel bins/mags Float64Arrays.
|
|
24
|
+
// Frame data (tracks, residual) persists per frame; everything else below is
|
|
25
|
+
// module-level scratch reused across frames — the per-frame hot path allocates nothing.
|
|
26
|
+
function makeFrame(nTracks, half) {
|
|
27
|
+
return {
|
|
28
|
+
tracks: { bins: new Float64Array(nTracks), mags: new Float64Array(nTracks) },
|
|
29
|
+
residual: new Float64Array(half + 1),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let _buf = new Float64Array(0) // windowed analysis frame
|
|
34
|
+
let _mag = new Float64Array(0) // magnitude spectrum
|
|
35
|
+
let _pre = new Float64Array(0) // pre-smooth residual
|
|
36
|
+
let _pkBin = new Float64Array(0) // detected peaks
|
|
37
|
+
let _pkMag = new Float64Array(0)
|
|
38
|
+
let _pkBin2 = new Float64Array(0) // permute scratch
|
|
39
|
+
let _pkMag2 = new Float64Array(0)
|
|
40
|
+
let _order = [] // reusable index-sort scratch
|
|
41
|
+
const byMagDesc = (a, b) => _pkMag2[b] - _pkMag2[a]
|
|
42
|
+
const byCostAsc = (a, b) => _pairC[a] - _pairC[b]
|
|
43
|
+
let _pairT = new Int32Array(0) // candidate track/peak pairs
|
|
44
|
+
let _pairP = new Int32Array(0)
|
|
45
|
+
let _pairC = new Float64Array(0)
|
|
46
|
+
let _taken = new Uint8Array(0)
|
|
47
|
+
let _assigned = new Uint8Array(0)
|
|
48
|
+
let _empty = new Int32Array(0)
|
|
49
|
+
let _sre = new Float64Array(0) // synthesis spectrum
|
|
50
|
+
let _sim = new Float64Array(0)
|
|
51
|
+
|
|
52
|
+
// Smooth `pre[0..half]` with a triangular kernel into `out`.
|
|
53
|
+
function smoothResidual(pre, half, width, out) {
|
|
54
|
+
for (let k = 0; k <= half; k++) {
|
|
55
|
+
let sum = 0, weightSum = 0
|
|
56
|
+
let start = Math.max(0, k - width), end = Math.min(half, k + width)
|
|
57
|
+
for (let j = start; j <= end; j++) {
|
|
58
|
+
let weight = width + 1 - Math.abs(j - k)
|
|
59
|
+
sum += pre[j] * weight; weightSum += weight
|
|
60
|
+
}
|
|
61
|
+
out[k] = weightSum ? sum / weightSum : 0
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Subtract top-nTracks peaks from the spectrum, smooth → noise envelope into `out`.
|
|
66
|
+
function residualEnvelope(mag, nPeaks, half, nTracks, out) {
|
|
67
|
+
if (_pre.length < half + 1) _pre = new Float64Array(half + 1)
|
|
68
|
+
let residual = _pre
|
|
69
|
+
for (let k = 0; k <= half; k++) residual[k] = mag[k]
|
|
70
|
+
|
|
71
|
+
let count = Math.min(nTracks, nPeaks)
|
|
72
|
+
for (let i = 0; i < count; i++) {
|
|
73
|
+
let bin = _pkBin[i], radius = 3.5
|
|
74
|
+
let start = Math.max(1, Math.floor(bin - radius))
|
|
75
|
+
let end = Math.min(half - 1, Math.ceil(bin + radius))
|
|
76
|
+
for (let k = start; k <= end; k++) {
|
|
77
|
+
let weight = Math.max(0, 1 - Math.abs(k - bin) / radius)
|
|
78
|
+
residual[k] = Math.max(0, residual[k] - _pkMag[i] * weight)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
residual[0] = 0
|
|
83
|
+
if (half > 0) residual[half] *= 0.5
|
|
84
|
+
smoothResidual(residual, half, 4, out)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Spectral peak detection with parabolic interpolation for sub-bin accuracy.
|
|
88
|
+
// Fills _pkBin/_pkMag sorted by magnitude desc (stable); returns peak count.
|
|
89
|
+
function detectPeaks(mag, half, thresh) {
|
|
90
|
+
if (_pkBin.length < half + 1) {
|
|
91
|
+
_pkBin = new Float64Array(half + 1)
|
|
92
|
+
_pkMag = new Float64Array(half + 1)
|
|
93
|
+
_pkBin2 = new Float64Array(half + 1)
|
|
94
|
+
_pkMag2 = new Float64Array(half + 1)
|
|
95
|
+
}
|
|
96
|
+
let n = 0
|
|
97
|
+
for (let k = 2; k < half - 1; k++) {
|
|
98
|
+
if (mag[k] <= mag[k - 1] || mag[k] <= mag[k + 1] || mag[k] < thresh) continue
|
|
99
|
+
let a = mag[k - 1], b = mag[k], c = mag[k + 1]
|
|
100
|
+
let d = a - 2 * b + c
|
|
101
|
+
let p = d ? 0.5 * (a - c) / d : 0
|
|
102
|
+
_pkBin2[n] = k + p
|
|
103
|
+
_pkMag2[n] = b - 0.25 * (a - c) * p
|
|
104
|
+
n++
|
|
105
|
+
}
|
|
106
|
+
_order.length = n
|
|
107
|
+
for (let i = 0; i < n; i++) _order[i] = i
|
|
108
|
+
_order.sort(byMagDesc)
|
|
109
|
+
for (let i = 0; i < n; i++) {
|
|
110
|
+
_pkBin[i] = _pkBin2[_order[i]]
|
|
111
|
+
_pkMag[i] = _pkMag2[_order[i]]
|
|
112
|
+
}
|
|
113
|
+
return n
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Cost-weighted sinusoidal tracking: sorted assignment by frequency + magnitude continuity.
|
|
117
|
+
// Writes the continued track set into `out`.
|
|
118
|
+
function trackPeaks(prev, nPeaks, nTracks, maxDev, out) {
|
|
119
|
+
out.bins.fill(0)
|
|
120
|
+
out.mags.fill(0)
|
|
121
|
+
|
|
122
|
+
let cap = nTracks * Math.max(nPeaks, 1)
|
|
123
|
+
if (_pairT.length < cap) {
|
|
124
|
+
_pairT = new Int32Array(cap)
|
|
125
|
+
_pairP = new Int32Array(cap)
|
|
126
|
+
_pairC = new Float64Array(cap)
|
|
127
|
+
}
|
|
128
|
+
let nPairs = 0
|
|
129
|
+
for (let i = 0; i < nTracks; i++) {
|
|
130
|
+
if (!prev.bins[i]) continue
|
|
131
|
+
for (let j = 0; j < nPeaks; j++) {
|
|
132
|
+
let fd = Math.abs(_pkBin[j] - prev.bins[i])
|
|
133
|
+
if (fd >= maxDev) continue
|
|
134
|
+
let mr = prev.mags[i] > 1e-10 ? _pkMag[j] / prev.mags[i] : 1
|
|
135
|
+
_pairT[nPairs] = i
|
|
136
|
+
_pairP[nPairs] = j
|
|
137
|
+
_pairC[nPairs] = fd / maxDev + 0.25 * Math.abs(Math.log(clamp(mr, 0.01, 100)))
|
|
138
|
+
nPairs++
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
_order.length = nPairs
|
|
142
|
+
for (let i = 0; i < nPairs; i++) _order[i] = i
|
|
143
|
+
_order.sort(byCostAsc)
|
|
144
|
+
|
|
145
|
+
if (_taken.length < nPeaks) _taken = new Uint8Array(nPeaks)
|
|
146
|
+
if (_assigned.length < nTracks) {
|
|
147
|
+
_assigned = new Uint8Array(nTracks)
|
|
148
|
+
_empty = new Int32Array(nTracks)
|
|
149
|
+
}
|
|
150
|
+
_taken.fill(0); _assigned.fill(0)
|
|
151
|
+
|
|
152
|
+
for (let s = 0; s < nPairs; s++) {
|
|
153
|
+
let t = _pairT[_order[s]], p = _pairP[_order[s]]
|
|
154
|
+
if (_assigned[t] || _taken[p]) continue
|
|
155
|
+
out.bins[t] = _pkBin[p]
|
|
156
|
+
out.mags[t] = _pkMag[p]
|
|
157
|
+
_assigned[t] = 1; _taken[p] = 1
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let nEmpty = 0
|
|
161
|
+
for (let i = 0; i < nTracks; i++) if (!_assigned[i]) _empty[nEmpty++] = i
|
|
162
|
+
let e = 0
|
|
163
|
+
for (let j = 0; j < nPeaks && e < nEmpty; j++) {
|
|
164
|
+
if (!_taken[j]) {
|
|
165
|
+
let t = _empty[e++]
|
|
166
|
+
out.bins[t] = _pkBin[j]
|
|
167
|
+
out.mags[t] = _pkMag[j]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Analyze one frame into `frame`: window+FFT, detect+track peaks, residual envelope
|
|
173
|
+
// (blended 70/30 with the previous frame's when given).
|
|
174
|
+
function analyzeFrame(data, pos, win, N, half, thresh, prev, nTracks, maxDev, prevResidual, frame) {
|
|
175
|
+
if (_buf.length !== N) _buf = new Float64Array(N)
|
|
176
|
+
if (_mag.length < half + 1) _mag = new Float64Array(half + 1)
|
|
177
|
+
let buf = _buf
|
|
178
|
+
for (let i = 0; i < N; i++) buf[i] = data[pos + i] * win[i]
|
|
179
|
+
let [re, im] = fft(buf)
|
|
180
|
+
let mag = _mag
|
|
181
|
+
for (let k = 0; k <= half; k++) mag[k] = Math.sqrt(re[k] * re[k] + im[k] * im[k])
|
|
182
|
+
|
|
183
|
+
let nPeaks = detectPeaks(mag, half, thresh)
|
|
184
|
+
trackPeaks(prev, nPeaks, nTracks, maxDev, frame.tracks)
|
|
185
|
+
|
|
186
|
+
let residual = frame.residual
|
|
187
|
+
residualEnvelope(mag, nPeaks, half, nTracks, residual)
|
|
188
|
+
if (prevResidual) for (let k = 0; k <= half; k++) residual[k] = 0.7 * residual[k] + 0.3 * prevResidual[k]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build synthesis spectrum from interpolated tracks, IFFT to time domain
|
|
192
|
+
function synthFrame(t0, t1, r0, r1, alpha, nTracks, phi, half, N, hop, noiseState, residualMix) {
|
|
193
|
+
if (_sre.length !== half + 1) {
|
|
194
|
+
_sre = new Float64Array(half + 1)
|
|
195
|
+
_sim = new Float64Array(half + 1)
|
|
196
|
+
}
|
|
197
|
+
let re = _sre, im = _sim
|
|
198
|
+
re.fill(0); im.fill(0)
|
|
199
|
+
let dphi = PI2 * hop / N
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < nTracks; i++) {
|
|
202
|
+
let b0 = t0.bins[i], m0 = t0.mags[i], b1 = t1.bins[i], m1 = t1.mags[i]
|
|
203
|
+
if (!b0 && !b1) continue
|
|
204
|
+
|
|
205
|
+
let bin, mag
|
|
206
|
+
if (!b0) { bin = b1; mag = m1 * alpha }
|
|
207
|
+
else if (!b1) { bin = b0; mag = m0 * (1 - alpha) }
|
|
208
|
+
else { bin = b0 + (b1 - b0) * alpha; mag = m0 + (m1 - m0) * alpha }
|
|
209
|
+
|
|
210
|
+
phi[i] += bin * dphi
|
|
211
|
+
let k = Math.round(bin)
|
|
212
|
+
if (k > 0 && k < half) {
|
|
213
|
+
re[k] += 2 * mag * Math.cos(phi[i])
|
|
214
|
+
im[k] += 2 * mag * Math.sin(phi[i])
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (residualMix > 0) {
|
|
219
|
+
for (let k = 1; k < half; k++) {
|
|
220
|
+
let resMag = (r0[k] + (r1[k] - r0[k]) * alpha) * residualMix
|
|
221
|
+
if (resMag <= 1e-8) continue
|
|
222
|
+
let phase = nextNoisePhase(noiseState)
|
|
223
|
+
re[k] += 2 * resMag * Math.cos(phase)
|
|
224
|
+
im[k] += 2 * resMag * Math.sin(phase)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ifft(re, im)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default function sms(data, opts = {}) {
|
|
232
|
+
if (!(data instanceof Float32Array)) return writer(smsStream(data))
|
|
233
|
+
|
|
234
|
+
let factor = opts.factor ?? 1
|
|
235
|
+
let N = opts.frameSize ?? 2048
|
|
236
|
+
let hop = opts.hopSize ?? (N >> 2)
|
|
237
|
+
let half = N >> 1
|
|
238
|
+
let nTracks = opts.maxTracks ?? 60
|
|
239
|
+
let thresh = opts.minMag ?? 1e-4
|
|
240
|
+
let maxDev = opts.freqDev ?? 3
|
|
241
|
+
let residualMix = clamp(opts.residualMix ?? 1, 0, 1)
|
|
242
|
+
let win = hannWindow(N)
|
|
243
|
+
let noiseState = createNoiseState()
|
|
244
|
+
|
|
245
|
+
if (factor === 1) return new Float32Array(data)
|
|
246
|
+
|
|
247
|
+
// Analysis pass — frames persist (synthesis interpolates between any two)
|
|
248
|
+
let nAna = Math.max(1, Math.floor((data.length - N) / hop) + 1)
|
|
249
|
+
let frames = new Array(nAna)
|
|
250
|
+
let prev = { bins: new Float64Array(nTracks), mags: new Float64Array(nTracks) }
|
|
251
|
+
let prevResidual = null
|
|
252
|
+
for (let f = 0; f < nAna; f++) {
|
|
253
|
+
let frame = makeFrame(nTracks, half)
|
|
254
|
+
analyzeFrame(data, f * hop, win, N, half, thresh, prev, nTracks, maxDev, prevResidual, frame)
|
|
255
|
+
frames[f] = frame; prev = frame.tracks; prevResidual = frame.residual
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Synthesis pass
|
|
259
|
+
let outLen = Math.round(data.length * factor)
|
|
260
|
+
let out = new Float32Array(outLen), nrm = new Float32Array(outLen)
|
|
261
|
+
let phi = new Float64Array(nTracks)
|
|
262
|
+
|
|
263
|
+
for (let s = 0; ; s++) {
|
|
264
|
+
let sPos = s * hop
|
|
265
|
+
if (sPos + N > outLen) break
|
|
266
|
+
let af = Math.min(s / factor, nAna - 1)
|
|
267
|
+
let f0 = Math.floor(af), f1 = Math.min(f0 + 1, nAna - 1), alpha = af - f0
|
|
268
|
+
let fr = synthFrame(frames[f0].tracks, frames[f1].tracks, frames[f0].residual, frames[f1].residual, alpha, nTracks, phi, half, N, hop, noiseState, residualMix)
|
|
269
|
+
for (let i = 0; i < N && sPos + i < outLen; i++) {
|
|
270
|
+
let w2 = win[i] * win[i]
|
|
271
|
+
out[sPos + i] += fr[i] * w2; nrm[sPos + i] += w2
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
normalize(out, nrm)
|
|
276
|
+
return out
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function smsStream(opts = {}) {
|
|
280
|
+
let factor = opts.factor ?? 1
|
|
281
|
+
let N = opts.frameSize ?? 2048
|
|
282
|
+
let hop = opts.hopSize ?? (N >> 2)
|
|
283
|
+
let half = N >> 1
|
|
284
|
+
let nTracks = opts.maxTracks ?? 60
|
|
285
|
+
let thresh = opts.minMag ?? 1e-4
|
|
286
|
+
let maxDev = opts.freqDev ?? 3
|
|
287
|
+
let residualMix = clamp(opts.residualMix ?? 1, 0, 1)
|
|
288
|
+
let win = hannWindow(N)
|
|
289
|
+
let noiseState = createNoiseState()
|
|
290
|
+
|
|
291
|
+
let st = makeStreamBufs(N)
|
|
292
|
+
// Two frame slots ping-ponged: each analysis writes into the retiring slot,
|
|
293
|
+
// so the steady-state stream allocates nothing per frame.
|
|
294
|
+
let slotA = makeFrame(nTracks, half)
|
|
295
|
+
let slotB = makeFrame(nTracks, half)
|
|
296
|
+
let prevFrame = slotA
|
|
297
|
+
let currFrame = null
|
|
298
|
+
let phi = new Float64Array(nTracks)
|
|
299
|
+
let anaIdx = 0, synIdx = 0, anaPos = 0
|
|
300
|
+
|
|
301
|
+
function synthOne(alpha) {
|
|
302
|
+
st.growOut(st.pos + N)
|
|
303
|
+
let ob = st.ob, nb = st.nb, base = st.pos
|
|
304
|
+
let fr = synthFrame(prevFrame.tracks, currFrame.tracks, prevFrame.residual, currFrame.residual, alpha, nTracks, phi, half, N, hop, noiseState, residualMix)
|
|
305
|
+
for (let i = 0; i < N && base + i < ob.length; i++) {
|
|
306
|
+
let w2 = win[i] * win[i]; ob[base + i] += fr[i] * w2; nb[base + i] += w2
|
|
307
|
+
}
|
|
308
|
+
st.pos += hop; synIdx++
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function emitSynth() {
|
|
312
|
+
if (anaIdx < 2) return
|
|
313
|
+
while (synIdx / factor < anaIdx - 1) synthOne(synIdx / factor - (anaIdx - 2))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function processInput() {
|
|
317
|
+
while (anaPos + N <= st.il) {
|
|
318
|
+
let source = currFrame || prevFrame
|
|
319
|
+
let target = source === slotA ? slotB : slotA
|
|
320
|
+
analyzeFrame(st.ib, anaPos, win, N, half, thresh, source.tracks, nTracks, maxDev, source.residual, target)
|
|
321
|
+
prevFrame = source; currFrame = target
|
|
322
|
+
anaIdx++; anaPos += hop
|
|
323
|
+
emitSynth()
|
|
324
|
+
}
|
|
325
|
+
if (anaPos > N * 2) { let trim = anaPos - N; st.compactIn(trim); anaPos -= trim }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
write(chunk) {
|
|
330
|
+
st.appendIn(chunk)
|
|
331
|
+
processInput()
|
|
332
|
+
return st.take(Math.max(0, st.pos - N + hop))
|
|
333
|
+
},
|
|
334
|
+
flush() {
|
|
335
|
+
if (anaIdx >= 2 && currFrame) {
|
|
336
|
+
while (synIdx / factor < anaIdx) {
|
|
337
|
+
let af = Math.min(synIdx / factor, anaIdx - 1)
|
|
338
|
+
synthOne(Math.min(af - (anaIdx - 2), 1))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return st.take(st.pos)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|