forked from mikolalysenko/pitch-shift
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpitchshift.js
165 lines (141 loc) · 3.9 KB
/
pitchshift.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"use strict"
var frameHop = require("frame-hop")
var overlapAdd = require("overlap-add")
var detectPitch = require("detect-pitch")
var pool = require("typedarray-pool")
function createWindow(n) {
var result = new Float32Array(n)
for(var i=0; i<n; ++i) {
var t = i / (n-1)
result[i] = 0.5 * (1.0 - Math.cos(2.0*Math.PI * t))
}
return result
}
function normalizeWindow(w, hop_size) {
var n = w.length
var nh = (n / hop_size)|0
var scale = pool.mallocFloat32(n)
for(var i=0; i<n; ++i) {
var s = 0.0
for(var j=0; j<nh; ++j) {
s += w[(i + j*hop_size)%n]
}
scale[i] = s
}
for(var i=0; i<n; ++i) {
w[i] /= scale[i]
}
pool.freeFloat32(scale)
}
//Applies window to signal
function applyWindow(X, W, frame) {
var i, n = frame.length
for(i=0; i<n; ++i) {
X[i] = W[i] * frame[i]
}
}
//Performs the actual pitch scaling
function scalePitch(out, x, nx, period, scale, shift, w) {
var no = out.length
for(var i=0; i<no; ++i) {
var t = i * scale + shift
var ti = Math.floor(t)|0
var tf = t - ti
var x1 = x[ti%nx]
var x2 = x[(ti+1)%nx]
var v = (1.0 - tf) * x1 + tf * x2
out[i] = w[i] * v
}
}
//Match start/end points of signal to avoid popping artefacts
function findMatch(x, start, step) {
var a = x[0], b = x[step], c = x[2*step]
var n = x.length
var best_d = 8
var best_i = start
for(var i=start; i<n-2*step; ++i) {
var s = x[i]-a, t = x[i+step]-b, u=x[i+2*step]-c
var d = s*s + t*t + u*u
if( d < best_d ) {
best_d = d
best_i = i
}
}
return best_i
}
function roundToNearestDivisor(n, period) {
var lo = period, hi = period
while(2*lo > period) {
if(n % lo === 0) {
return lo
}
--lo
if(n % hi === 0) {
return hi
}
++hi
}
return period
}
function pitchShift(onData, onTune, options) {
options = options || {}
var frame_size = options.frameSize || 2048
var hop_size = options.hopSize || (frame_size>>>2)
var sample_rate = options.sampleRate || 44100
var data_size = options.maxDataSize || undefined
var a_window = options.analysisWindow || createWindow(frame_size)
var s_window = options.synthesisWindow || createWindow(frame_size)
var threshold = options.freqThreshold || 0.9
var start_bin = options.minPeriod || Math.max(16, Math.floor(sample_rate / 1000))|0
var scale_threshold = options.harmonicScale || 0.2
var detect_params = {
threshold: threshold,
start_bin: start_bin
}
var t = 0
var cur = new Float32Array(frame_size)
if(frame_size % hop_size !== 0) {
throw new Error("Hop size must divide frame size")
}
//Normalize synthesis window
normalizeWindow(s_window, hop_size)
var addFrame = overlapAdd(frame_size, hop_size, onData)
var delay = 0
var pfreq = 0.0
function doPitchShift(frame) {
//Apply window
applyWindow(cur, a_window, frame)
//Compute pitch, period and sample rate
var period = detectPitch(cur, detect_params)
//Calculate frame size
var fsize = frame_size>>1
if(period > 0) {
fsize = (Math.max(1, Math.floor(0.5*frame_size/period)) * period)|0
}
fsize = findMatch(frame, fsize|0, Math.max(1, period/20)|0)
//Compute pitch
var pitch = 0.0
if(period > 0) {
pitch = sample_rate / period
}
for(var k=10; k>0; --k) {
var s = +(1<<k)
if(Math.abs(s*pfreq - pitch) < scale_threshold * pitch) {
pitch = pitch / s
}
}
pfreq = pitch
//Apply scale factor
var scale_f = onTune(t / sample_rate, pitch)
//Apply scaling
delay = ((delay % fsize) + fsize) % fsize
scalePitch(cur, frame, fsize, period|0, scale_f, delay, s_window)
//Update counters
delay += hop_size * (scale_f - 1.0)
t += hop_size
//Add frame
addFrame(cur)
}
return frameHop(frame_size, hop_size, doPitchShift, data_size)
}
module.exports = pitchShift