mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-28 05:58:28 +00:00
copied files
This commit is contained in:
parent
e62d5759e7
commit
e6f3b1f1ef
178
packages/superdough/ola-processor.js
Normal file
178
packages/superdough/ola-processor.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const WEBAUDIO_BLOCK_SIZE = 128;
|
||||||
|
|
||||||
|
/** Overlap-Add Node */
|
||||||
|
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 lety 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OLAProcessor;
|
||||||
@ -1,6 +1,10 @@
|
|||||||
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
|
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
|
||||||
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
|
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
|
||||||
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
|
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
|
||||||
|
|
||||||
|
import OLAProcessor from './ola-processor.js';
|
||||||
|
import FFT from 'fft.js';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const _mod = (n, m) => ((n % m) + m) % m;
|
const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
@ -463,4 +467,178 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const BUFFERED_BLOCK_SIZE = 2048;
|
||||||
|
|
||||||
|
function genHannWindow(length) {
|
||||||
|
let win = new Float32Array(length);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
win[i] = 0.5 * (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 (var i = 0; i < this.nbInputs; i++) {
|
||||||
|
for (var j = 0; j < inputs[i].length; j++) {
|
||||||
|
// big assumption here: output is symetric to input
|
||||||
|
var input = inputs[i][j];
|
||||||
|
var 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 (var i = 0; i < this.blockSize; i++) {
|
||||||
|
input[i] = input[i] * this.hannWindow[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compute squared magnitudes for peak finding **/
|
||||||
|
computeMagnitudes() {
|
||||||
|
var i = 0, 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;
|
||||||
|
var 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 (var 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
|
||||||
|
var startIndex = 0;
|
||||||
|
var 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 (var 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor);
|
||||||
Loading…
x
Reference in New Issue
Block a user