优化&更新依赖
This commit is contained in:
@@ -39,7 +39,7 @@ export const convolutions = [
|
||||
// { name: 'tim_omni_rear_blend', mainGain: 1.8, sendGain: 0.8, source: 'tim-omni-rear-blend.wav' },
|
||||
] as const
|
||||
// 半音
|
||||
export const semitones = [-1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] as const
|
||||
// export const semitones = [-1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] as const
|
||||
|
||||
let convolver: ConvolverNode
|
||||
let convolverSourceGainNode: GainNode
|
||||
@@ -48,7 +48,7 @@ let convolverDynamicsCompressor: DynamicsCompressorNode
|
||||
let gainNode: GainNode
|
||||
let panner: PannerNode
|
||||
let pitchShifterNode: AudioWorkletNode
|
||||
let pitchShifterNodeAudioParam: AudioParam
|
||||
let pitchShifterNodePitchFactor: AudioParam
|
||||
let pitchShifterNodeLoadStatus: 'none' | 'loading' | 'unconnect' | 'connected' = 'none'
|
||||
let pitchShifterNodeTempValue = 1
|
||||
export const soundR = 0.5
|
||||
@@ -225,14 +225,14 @@ const loadPitchShifterNode = () => {
|
||||
// source -> analyser -> biquadFilter -> audioWorklet(pitch shifter) -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain
|
||||
void audioContext.audioWorklet.addModule(new URL(
|
||||
/* webpackChunkName: 'pitch_shifter.audioWorklet' */
|
||||
'@renderer/utils/pitch-shifter/phase-vocoder.js',
|
||||
'./pitch-shifter/phase-vocoder.js',
|
||||
import.meta.url,
|
||||
)).then(() => {
|
||||
console.log('pitch shifter audio worklet loaded')
|
||||
pitchShifterNode = new AudioWorkletNode(audioContext, 'phase-vocoder-processor')
|
||||
let audioParam = pitchShifterNode.parameters.get('pitchFactor')
|
||||
if (!audioParam) return
|
||||
pitchShifterNodeAudioParam = audioParam
|
||||
let pitchFactorParam = pitchShifterNode.parameters.get('pitchFactor')
|
||||
if (!pitchFactorParam) return
|
||||
pitchShifterNodePitchFactor = pitchFactorParam
|
||||
pitchShifterNodeLoadStatus = 'unconnect'
|
||||
if (pitchShifterNodeTempValue == 1) return
|
||||
|
||||
@@ -250,7 +250,7 @@ const connectPitchShifterNode = () => {
|
||||
// convolverDynamicsCompressor.connect(pitchShifterNode)
|
||||
// pitchShifterNode.connect(panner)
|
||||
pitchShifterNodeLoadStatus = 'connected'
|
||||
pitchShifterNodeAudioParam.value = pitchShifterNodeTempValue
|
||||
pitchShifterNodePitchFactor.value = pitchShifterNodeTempValue
|
||||
}
|
||||
// const disconnectPitchShifterNode = () => {
|
||||
// const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
|
||||
@@ -275,7 +275,7 @@ export const setPitchShifter = (val: number) => {
|
||||
case 'connected':
|
||||
// a: 1 = 半音
|
||||
// value = 2 ** (a / 12)
|
||||
pitchShifterNodeAudioParam.value = val
|
||||
pitchShifterNodePitchFactor.value = val
|
||||
break
|
||||
case 'unconnect':
|
||||
connectPitchShifterNode()
|
||||
+2
-352
@@ -1,6 +1,5 @@
|
||||
// https://github.com/olvb/phaze
|
||||
// https://github.com/indutny/fft.js
|
||||
// import OLAProcessor from './ola-processor'
|
||||
|
||||
|
||||
function FFT(size) {
|
||||
this.size = size | 0
|
||||
@@ -498,353 +497,4 @@ FFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff,
|
||||
out[outOff + 7] = FDi
|
||||
}
|
||||
|
||||
|
||||
const WEBAUDIO_BLOCK_SIZE = 128
|
||||
|
||||
/** Overlap-Add Node */
|
||||
// eslint-disable-next-line no-undef
|
||||
class OLAProcessor extends AudioWorkletProcessor {
|
||||
constructor(options) {
|
||||
super(options)
|
||||
|
||||
this.nbInputs = options.numberOfInputs
|
||||
this.nbOutputs = options.numberOfOutputs
|
||||
|
||||
this.blockSize = options.processorOptions.blockSize
|
||||
// TODO for now, the only support hop size is the size of a web audio block
|
||||
this.hopSize = WEBAUDIO_BLOCK_SIZE
|
||||
|
||||
this.nbOverlaps = this.blockSize / this.hopSize
|
||||
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.inputBuffers = new Array(this.nbInputs)
|
||||
this.inputBuffersHead = new Array(this.nbInputs)
|
||||
this.inputBuffersToSend = new Array(this.nbInputs)
|
||||
// default to 1 channel per input until we know more
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
this.allocateInputChannels(i, 1)
|
||||
}
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.outputBuffers = new Array(this.nbOutputs)
|
||||
this.outputBuffersToRetrieve = new Array(this.nbOutputs)
|
||||
// default to 1 channel per output until we know more
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
this.allocateOutputChannels(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles dynamic reallocation of input/output channels buffer
|
||||
(channel numbers may vary during lifecycle) **/
|
||||
reallocateChannelsIfNeeded(inputs, outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
let nbChannels = inputs[i].length
|
||||
if (nbChannels != this.inputBuffers[i].length) {
|
||||
this.allocateInputChannels(i, nbChannels)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
let nbChannels = outputs[i].length
|
||||
if (nbChannels != this.outputBuffers[i].length) {
|
||||
this.allocateOutputChannels(i, nbChannels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allocateInputChannels(inputIndex, nbChannels) {
|
||||
// allocate input buffers
|
||||
|
||||
this.inputBuffers[inputIndex] = new Array(nbChannels)
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE)
|
||||
this.inputBuffers[inputIndex][i].fill(0)
|
||||
}
|
||||
|
||||
// allocate input buffers to send and head pointers to copy from
|
||||
// (cannot directly send a pointer/subarray because input may be modified)
|
||||
this.inputBuffersHead[inputIndex] = new Array(nbChannels)
|
||||
this.inputBuffersToSend[inputIndex] = new Array(nbChannels)
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize)
|
||||
this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize)
|
||||
}
|
||||
}
|
||||
|
||||
allocateOutputChannels(outputIndex, nbChannels) {
|
||||
// allocate output buffers
|
||||
this.outputBuffers[outputIndex] = new Array(nbChannels)
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize)
|
||||
this.outputBuffers[outputIndex][i].fill(0)
|
||||
}
|
||||
|
||||
// allocate output buffers to retrieve
|
||||
// (cannot send a pointer/subarray because new output has to be add to exising output)
|
||||
this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels)
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize)
|
||||
this.outputBuffersToRetrieve[outputIndex][i].fill(0)
|
||||
}
|
||||
}
|
||||
|
||||
/** Read next web audio block to input buffers **/
|
||||
readInputs(inputs) {
|
||||
// when playback is paused, we may stop receiving new samples
|
||||
if (inputs[0].length && inputs[0][0].length == 0) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].fill(0, this.blockSize)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
let webAudioBlock = inputs[i][j]
|
||||
this.inputBuffers[i][j].set(webAudioBlock, this.blockSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Write next web audio block from output buffers **/
|
||||
writeOutputs(outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE)
|
||||
outputs[i][j].set(webAudioBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of input buffers to receive new web audio block **/
|
||||
shiftInputBuffers() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of output buffers to receive new web audio block **/
|
||||
shiftOutputBuffers() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)
|
||||
this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy contents of input buffers to buffer actually sent to process **/
|
||||
prepareInputBuffersToSend() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add contents of output buffers just processed to output buffers **/
|
||||
handleOutputBuffersToRetrieve() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
for (let k = 0; k < this.blockSize; k++) {
|
||||
this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
this.reallocateChannelsIfNeeded(inputs, outputs)
|
||||
|
||||
this.readInputs(inputs)
|
||||
this.shiftInputBuffers()
|
||||
this.prepareInputBuffersToSend()
|
||||
this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params)
|
||||
this.handleOutputBuffersToRetrieve()
|
||||
this.writeOutputs(outputs)
|
||||
this.shiftOutputBuffers()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
processOLA(inputs, outputs, params) {
|
||||
console.assert(false, 'Not overriden')
|
||||
}
|
||||
}
|
||||
|
||||
const BUFFERED_BLOCK_SIZE = 4096
|
||||
|
||||
function genHannWindow(length) {
|
||||
let win = new Float32Array(length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
win[i] = 0.8 * (1 - Math.cos(2 * Math.PI * i / length))
|
||||
}
|
||||
return win
|
||||
}
|
||||
|
||||
class PhaseVocoderProcessor extends OLAProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [{
|
||||
name: 'pitchFactor',
|
||||
defaultValue: 1.0,
|
||||
}]
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
options.processorOptions = {
|
||||
blockSize: BUFFERED_BLOCK_SIZE,
|
||||
}
|
||||
super(options)
|
||||
|
||||
this.fftSize = this.blockSize
|
||||
this.timeCursor = 0
|
||||
|
||||
this.hannWindow = genHannWindow(this.blockSize)
|
||||
|
||||
// prepare FFT and pre-allocate buffers
|
||||
this.fft = new FFT(this.fftSize)
|
||||
this.freqComplexBuffer = this.fft.createComplexArray()
|
||||
this.freqComplexBufferShifted = this.fft.createComplexArray()
|
||||
this.timeComplexBuffer = this.fft.createComplexArray()
|
||||
this.magnitudes = new Float32Array(this.fftSize / 2 + 1)
|
||||
this.peakIndexes = new Int32Array(this.magnitudes.length)
|
||||
this.nbPeaks = 0
|
||||
}
|
||||
|
||||
processOLA(inputs, outputs, parameters) {
|
||||
// no automation, take last value
|
||||
const pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]
|
||||
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < inputs[i].length; j++) {
|
||||
// big assumption here: output is symetric to input
|
||||
let input = inputs[i][j]
|
||||
let output = outputs[i][j]
|
||||
|
||||
this.applyHannWindow(input)
|
||||
|
||||
this.fft.realTransform(this.freqComplexBuffer, input)
|
||||
|
||||
this.computeMagnitudes()
|
||||
this.findPeaks()
|
||||
this.shiftPeaks(pitchFactor)
|
||||
|
||||
this.fft.completeSpectrum(this.freqComplexBufferShifted)
|
||||
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted)
|
||||
this.fft.fromComplexArray(this.timeComplexBuffer, output)
|
||||
|
||||
this.applyHannWindow(output)
|
||||
}
|
||||
}
|
||||
|
||||
this.timeCursor += this.hopSize
|
||||
}
|
||||
|
||||
/** Apply Hann window in-place */
|
||||
applyHannWindow(input) {
|
||||
for (let i = 0; i < this.blockSize; i++) {
|
||||
input[i] = input[i] * this.hannWindow[i]
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute squared magnitudes for peak finding **/
|
||||
computeMagnitudes() {
|
||||
let i = 0; let j = 0
|
||||
while (i < this.magnitudes.length) {
|
||||
let real = this.freqComplexBuffer[j]
|
||||
let imag = this.freqComplexBuffer[j + 1]
|
||||
// no need to sqrt for peak finding
|
||||
this.magnitudes[i] = real ** 2 + imag ** 2
|
||||
i += 1
|
||||
j += 2
|
||||
}
|
||||
}
|
||||
|
||||
/** Find peaks in spectrum magnitudes **/
|
||||
findPeaks() {
|
||||
this.nbPeaks = 0
|
||||
let i = 2
|
||||
let end = this.magnitudes.length - 2
|
||||
|
||||
while (i < end) {
|
||||
let mag = this.magnitudes[i]
|
||||
|
||||
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
this.peakIndexes[this.nbPeaks] = i
|
||||
this.nbPeaks++
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift peaks and regions of influence by pitchFactor into new specturm */
|
||||
shiftPeaks(pitchFactor) {
|
||||
// zero-fill new spectrum
|
||||
this.freqComplexBufferShifted.fill(0)
|
||||
|
||||
for (let i = 0; i < this.nbPeaks; i++) {
|
||||
let peakIndex = this.peakIndexes[i]
|
||||
let peakIndexShifted = Math.round(peakIndex * pitchFactor)
|
||||
|
||||
if (peakIndexShifted > this.magnitudes.length) {
|
||||
break
|
||||
}
|
||||
|
||||
// find region of influence
|
||||
let startIndex = 0
|
||||
let endIndex = this.fftSize
|
||||
if (i > 0) {
|
||||
let peakIndexBefore = this.peakIndexes[i - 1]
|
||||
startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2)
|
||||
}
|
||||
if (i < this.nbPeaks - 1) {
|
||||
let peakIndexAfter = this.peakIndexes[i + 1]
|
||||
endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2)
|
||||
}
|
||||
|
||||
// shift whole region of influence around peak to shifted peak
|
||||
let startOffset = startIndex - peakIndex
|
||||
let endOffset = endIndex - peakIndex
|
||||
for (let j = startOffset; j < endOffset; j++) {
|
||||
let binIndex = peakIndex + j
|
||||
let binIndexShifted = peakIndexShifted + j
|
||||
|
||||
if (binIndexShifted >= this.magnitudes.length) {
|
||||
break
|
||||
}
|
||||
|
||||
// apply phase correction
|
||||
let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize
|
||||
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor)
|
||||
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor)
|
||||
|
||||
let indexReal = binIndex * 2
|
||||
let indexImag = indexReal + 1
|
||||
let valueReal = this.freqComplexBuffer[indexReal]
|
||||
let valueImag = this.freqComplexBuffer[indexImag]
|
||||
|
||||
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag
|
||||
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal
|
||||
|
||||
let indexShiftedReal = binIndexShifted * 2
|
||||
let indexShiftedImag = indexShiftedReal + 1
|
||||
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal
|
||||
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor)
|
||||
|
||||
export default FFT
|
||||
+6
-3
@@ -1,12 +1,13 @@
|
||||
const WEBAUDIO_BLOCK_SIZE = 128
|
||||
|
||||
/** Overlap-Add Node */
|
||||
class OLAProcessor extends window.AudioWorkletProcessor {
|
||||
class OLAProcessor extends globalThis.AudioWorkletProcessor {
|
||||
constructor(options) {
|
||||
super(options)
|
||||
|
||||
this.nbInputs = options.numberOfInputs
|
||||
this.nbOutputs = options.numberOfOutputs
|
||||
this.paused = true
|
||||
|
||||
this.blockSize = options.processorOptions.blockSize
|
||||
// TODO for now, the only support hop size is the size of a web audio block
|
||||
@@ -88,7 +89,8 @@ class OLAProcessor extends window.AudioWorkletProcessor {
|
||||
/** Read next web audio block to input buffers **/
|
||||
readInputs(inputs) {
|
||||
// when playback is paused, we may stop receiving new samples
|
||||
if (inputs[0].length && inputs[0][0].length == 0) {
|
||||
// if (inputs[0].length && inputs[0][0].length == 0) {
|
||||
if (!inputs[0].length || !inputs[0][0].length || inputs[0][0][0] == 0) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].fill(0, this.blockSize)
|
||||
@@ -155,7 +157,8 @@ class OLAProcessor extends window.AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
this.reallocateChannelsIfNeeded(inputs, outputs)
|
||||
// if (!inputs[0].length || !inputs[0][0].length || inputs[0][0][0] == 0) return true
|
||||
// this.reallocateChannelsIfNeeded(inputs, outputs)
|
||||
|
||||
this.readInputs(inputs)
|
||||
this.shiftInputBuffers()
|
||||
@@ -0,0 +1,172 @@
|
||||
// https://github.com/olvb/phaze
|
||||
import FFT from './fft'
|
||||
import OLAProcessor from './ola-processor'
|
||||
|
||||
|
||||
const BUFFERED_BLOCK_SIZE = 4096
|
||||
|
||||
function genHannWindow(length) {
|
||||
let win = new Float32Array(length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
win[i] = 0.8 * (1 - Math.cos(2 * Math.PI * i / length))
|
||||
}
|
||||
return win
|
||||
}
|
||||
|
||||
class PhaseVocoderProcessor extends OLAProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [{
|
||||
name: 'pitchFactor',
|
||||
defaultValue: 1.0,
|
||||
}]
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
options.processorOptions = {
|
||||
blockSize: BUFFERED_BLOCK_SIZE,
|
||||
}
|
||||
super(options)
|
||||
|
||||
this.fftSize = this.blockSize
|
||||
this.timeCursor = 0
|
||||
|
||||
this.hannWindow = genHannWindow(this.blockSize)
|
||||
|
||||
// prepare FFT and pre-allocate buffers
|
||||
this.fft = new FFT(this.fftSize)
|
||||
this.freqComplexBuffer = this.fft.createComplexArray()
|
||||
this.freqComplexBufferShifted = this.fft.createComplexArray()
|
||||
this.timeComplexBuffer = this.fft.createComplexArray()
|
||||
this.magnitudes = new Float32Array(this.fftSize / 2 + 1)
|
||||
this.peakIndexes = new Int32Array(this.magnitudes.length)
|
||||
this.nbPeaks = 0
|
||||
}
|
||||
|
||||
processOLA(inputs, outputs, parameters) {
|
||||
// no automation, take last value
|
||||
// const pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]
|
||||
const pitchFactor = parameters.pitchFactor[0]
|
||||
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < inputs[i].length; j++) {
|
||||
// big assumption here: output is symetric to input
|
||||
let input = inputs[i][j]
|
||||
let output = outputs[i][j]
|
||||
|
||||
this.applyHannWindow(input)
|
||||
|
||||
this.fft.realTransform(this.freqComplexBuffer, input)
|
||||
|
||||
this.computeMagnitudes()
|
||||
this.findPeaks()
|
||||
this.shiftPeaks(pitchFactor)
|
||||
|
||||
this.fft.completeSpectrum(this.freqComplexBufferShifted)
|
||||
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted)
|
||||
this.fft.fromComplexArray(this.timeComplexBuffer, output)
|
||||
|
||||
this.applyHannWindow(output)
|
||||
}
|
||||
}
|
||||
|
||||
this.timeCursor += this.hopSize
|
||||
}
|
||||
|
||||
/** Apply Hann window in-place */
|
||||
applyHannWindow(input) {
|
||||
for (let i = 0; i < this.blockSize; i++) {
|
||||
input[i] = input[i] * this.hannWindow[i]
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute squared magnitudes for peak finding **/
|
||||
computeMagnitudes() {
|
||||
let i = 0; let j = 0
|
||||
while (i < this.magnitudes.length) {
|
||||
let real = this.freqComplexBuffer[j]
|
||||
let imag = this.freqComplexBuffer[j + 1]
|
||||
// no need to sqrt for peak finding
|
||||
this.magnitudes[i] = real ** 2 + imag ** 2
|
||||
i += 1
|
||||
j += 2
|
||||
}
|
||||
}
|
||||
|
||||
/** Find peaks in spectrum magnitudes **/
|
||||
findPeaks() {
|
||||
this.nbPeaks = 0
|
||||
let i = 2
|
||||
let end = this.magnitudes.length - 2
|
||||
|
||||
while (i < end) {
|
||||
let mag = this.magnitudes[i]
|
||||
|
||||
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
this.peakIndexes[this.nbPeaks] = i
|
||||
this.nbPeaks++
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift peaks and regions of influence by pitchFactor into new specturm */
|
||||
shiftPeaks(pitchFactor) {
|
||||
// zero-fill new spectrum
|
||||
this.freqComplexBufferShifted.fill(0)
|
||||
|
||||
for (let i = 0; i < this.nbPeaks; i++) {
|
||||
let peakIndex = this.peakIndexes[i]
|
||||
let peakIndexShifted = Math.round(peakIndex * pitchFactor)
|
||||
|
||||
if (peakIndexShifted > this.magnitudes.length) break
|
||||
|
||||
// find region of influence
|
||||
let startIndex = 0
|
||||
let endIndex = this.fftSize
|
||||
if (i > 0) {
|
||||
startIndex = peakIndex - Math.floor((peakIndex - this.peakIndexes[i - 1]) / 2)
|
||||
}
|
||||
if (i < this.nbPeaks - 1) {
|
||||
endIndex = peakIndex + Math.ceil((this.peakIndexes[i + 1] - peakIndex) / 2)
|
||||
}
|
||||
|
||||
// shift whole region of influence around peak to shifted peak
|
||||
let startOffset = startIndex - peakIndex
|
||||
let endOffset = endIndex - peakIndex
|
||||
for (let j = startOffset; j < endOffset; j++) {
|
||||
let binIndex = peakIndex + j
|
||||
let binIndexShifted = peakIndexShifted + j
|
||||
|
||||
if (binIndexShifted >= this.magnitudes.length) break
|
||||
|
||||
// apply phase correction
|
||||
let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize
|
||||
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor)
|
||||
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor)
|
||||
|
||||
let indexReal = binIndex * 2
|
||||
let indexImag = indexReal + 1
|
||||
let valueReal = this.freqComplexBuffer[indexReal]
|
||||
let valueImag = this.freqComplexBuffer[indexImag]
|
||||
|
||||
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag
|
||||
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal
|
||||
|
||||
let indexShiftedReal = binIndexShifted * 2
|
||||
let indexShiftedImag = indexShiftedReal + 1
|
||||
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal
|
||||
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor)
|
||||
|
||||
Reference in New Issue
Block a user