Skip to content

Commit

Permalink
Merge pull request #376 from oshoham/ring-buffer
Browse files Browse the repository at this point in the history
Add ring buffers to AudioWorklet processors to support variable buffer sizes
  • Loading branch information
therewasaguy authored Aug 26, 2019
2 parents 4d3a383 + 2014fbc commit c02bc88
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 108 deletions.
99 changes: 68 additions & 31 deletions lib/p5.sound.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/p5.sound.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/p5.sound.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/p5.sound.min.js.map

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions src/amplitude.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict';

define(function (require) {
var p5sound = require('master');
var processorNames = require('./audioWorklet/processorNames');
const p5sound = require('master');
const { safeBufferSize } = require('helpers');
const processorNames = require('./audioWorklet/processorNames');

/**
* Amplitude measures volume between 0.0 and 1.0.
Expand Down Expand Up @@ -47,15 +48,19 @@ define(function (require) {
p5.Amplitude = function(smoothing) {

// Set to 2048 for now. In future iterations, this should be inherited or parsed from p5sound's default
this.bufferSize = 2048;
this.bufferSize = safeBufferSize(2048);

// set audio context
this.audiocontext = p5sound.audiocontext;
this._workletNode = new AudioWorkletNode(this.audiocontext, processorNames.amplitudeProcessor, {
outputChannelCount: [1],

parameterData: { smoothing: smoothing || 0 },
processorOptions: {
normalize: false,
smoothing: smoothing || 0
smoothing: smoothing || 0,
numInputChannels: 2,
bufferSize: this.bufferSize
}
});

Expand Down
94 changes: 55 additions & 39 deletions src/audioWorklet/amplitudeProcessor.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// import processor name via preval.require so that it's available as a value at compile time
// import dependencies via preval.require so that they're available as values at compile time
const processorNames = preval.require('./processorNames');
const RingBuffer = preval.require('./ringBuffer').default;

class AmplitudeProcessor extends AudioWorkletProcessor {
constructor(options) {
super();

const processorOptions = options.processorOptions || {};
this.smoothing = processorOptions.smoothing || 0;
this.numOutputChannels = options.outputChannelCount || 1;
this.numInputChannels = processorOptions.numInputChannels || 2;
this.normalize = processorOptions.normalize || false;
this.smoothing = processorOptions.smoothing || 0;

this.bufferSize = processorOptions.bufferSize || 2048;
this.inputRingBuffer = new RingBuffer(this.bufferSize, this.numInputChannels);
this.outputRingBuffer = new RingBuffer(this.bufferSize, this.numOutputChannels);
this.inputRingBufferArraySequence = new Array(this.numInputChannels).fill(null).map(() => new Float32Array(this.bufferSize));

this.stereoVol = [0, 0];
this.stereoVolNorm = [0, 0];
Expand All @@ -30,53 +38,61 @@ class AmplitudeProcessor extends AudioWorkletProcessor {
const output = outputs[0];
const smoothing = this.smoothing;

for (let channel = 0; channel < input.length; ++channel) {
const inputBuffer = input[channel];
const bufLength = inputBuffer.length;

let sum = 0;
for (var i = 0; i < bufLength; i++) {
const x = inputBuffer[i];
if (this.normalize) {
sum += Math.max(Math.min(x / this.volMax, 1), -1) * Math.max(Math.min(x / this.volMax, 1), -1);
} else {
sum += x * x;
this.inputRingBuffer.push(input);

if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {
this.inputRingBuffer.pull(this.inputRingBufferArraySequence);

for (let channel = 0; channel < this.numInputChannels; ++channel) {
const inputBuffer = this.inputRingBufferArraySequence[channel];
const bufLength = inputBuffer.length;

let sum = 0;
for (var i = 0; i < bufLength; i++) {
const x = inputBuffer[i];
if (this.normalize) {
sum += Math.max(Math.min(x / this.volMax, 1), -1) * Math.max(Math.min(x / this.volMax, 1), -1);
} else {
sum += x * x;
}
}
}

// ... then take the square root of the sum.
const rms = Math.sqrt(sum / bufLength);
// ... then take the square root of the sum.
const rms = Math.sqrt(sum / bufLength);

this.stereoVol[channel] = Math.max(rms, this.stereoVol[channel] * smoothing);
this.volMax = Math.max(this.stereoVol[channel], this.volMax);
}
this.stereoVol[channel] = Math.max(rms, this.stereoVol[channel] * smoothing);
this.volMax = Math.max(this.stereoVol[channel], this.volMax);
}

// calculate stero normalized volume and add volume from all channels together
let volSum = 0;
for (let index = 0; index < this.stereoVol.length; index++) {
this.stereoVolNorm[index] = Math.max(Math.min(this.stereoVol[index] / this.volMax, 1), 0);
volSum += this.stereoVol[index];
}
// calculate stero normalized volume and add volume from all channels together
let volSum = 0;
for (let index = 0; index < this.stereoVol.length; index++) {
this.stereoVolNorm[index] = Math.max(Math.min(this.stereoVol[index] / this.volMax, 1), 0);
volSum += this.stereoVol[index];
}

// volume is average of channels
const volume = volSum / this.stereoVol.length;
// volume is average of channels
const volume = volSum / this.stereoVol.length;

// normalized value
const volNorm = Math.max(Math.min(volume / this.volMax, 1), 0);
// normalized value
const volNorm = Math.max(Math.min(volume / this.volMax, 1), 0);

this.port.postMessage({
name: 'amplitude',
volume: volume,
volNorm: volNorm,
stereoVol: this.stereoVol,
stereoVolNorm: this.stereoVolNorm
});
this.port.postMessage({
name: 'amplitude',
volume: volume,
volNorm: volNorm,
stereoVol: this.stereoVol,
stereoVolNorm: this.stereoVolNorm
});

// pass input through to output
for (let channel = 0; channel < output.length; ++channel) {
output[channel].set(input[channel]);
// pass input through to output
this.outputRingBuffer.push(this.inputRingBufferArraySequence);
}

// pull 128 frames out of the ring buffer
// if the ring buffer does not have enough frames, the output will be silent
this.outputRingBuffer.pull(output);

return true;
}
}
Expand Down
39 changes: 24 additions & 15 deletions src/audioWorklet/recorderProcessor.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// import processor name via preval.require so that it's available as a value at compile time
// import dependencies via preval.require so that they're available as values at compile time
const processorNames = preval.require('./processorNames');
const RingBuffer = preval.require('./ringBuffer').default;

class RecorderProcessor extends AudioWorkletProcessor {
constructor(options) {
super();

const processorOptions = options.processorOptions || {};
this.numOutputChannels = options.outputChannelCount || 2;
this.numInputChannels = processorOptions.numInputChannels || 2;
this.bufferSize = processorOptions.bufferSize || 1024;
this.recording = false;

this.clear();
Expand All @@ -21,7 +24,7 @@ class RecorderProcessor extends AudioWorkletProcessor {
};
}

process(inputs, outputs) {
process(inputs) {
if (!this.recording) {
return true;
} else if (this.sampleLimit && this.recordedSamples >= this.sampleLimit) {
Expand All @@ -30,22 +33,26 @@ class RecorderProcessor extends AudioWorkletProcessor {
}

const input = inputs[0];
const output = outputs[0];

for (let channel = 0; channel < output.length; ++channel) {
const inputChannel = input[channel];
if (channel === 0) {
this.leftBuffers.push(inputChannel);
if (this.numInputChannels === 1) {
this.rightBuffers.push(inputChannel);

this.inputRingBuffer.push(input);

if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {
this.inputRingBuffer.pull(this.inputRingBufferArraySequence);

for (let channel = 0; channel < this.numOutputChannels; ++channel) {
const inputChannelCopy = this.inputRingBufferArraySequence[channel].slice();
if (channel === 0) {
this.leftBuffers.push(inputChannelCopy);
if (this.numInputChannels === 1) {
this.rightBuffers.push(inputChannelCopy);
}
} else if (channel === 1 && this.numInputChannels > 1) {
this.rightBuffers.push(inputChannelCopy);
}
} else if (channel === 1 && this.numInputChannels > 1) {
this.rightBuffers.push(inputChannel);
}
}

this.recordedSamples += output[0].length;

this.recordedSamples += this.bufferSize;
}
return true;
}

Expand Down Expand Up @@ -87,6 +94,8 @@ class RecorderProcessor extends AudioWorkletProcessor {
clear() {
this.leftBuffers = [];
this.rightBuffers = [];
this.inputRingBuffer = new RingBuffer(this.bufferSize, this.numInputChannels);
this.inputRingBufferArraySequence = new Array(this.numInputChannels).fill(null).map(() => new Float32Array(this.bufferSize));
this.recordedSamples = 0;
this.sampleLimit = null;
}
Expand Down
125 changes: 125 additions & 0 deletions src/audioWorklet/ringBuffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* A JS FIFO implementation for the AudioWorklet. 3 assumptions for the
* simpler operation:
* 1. the push and the pull operation are done by 128 frames. (Web Audio
* API's render quantum size in the speficiation)
* 2. the channel count of input/output cannot be changed dynamically.
* The AudioWorkletNode should be configured with the `.channelCount = k`
* (where k is the channel count you want) and
* `.channelCountMode = explicit`.
* 3. This is for the single-thread operation. (obviously)
*
* @class
*/
class RingBuffer {
/**
* @constructor
* @param {number} length Buffer length in frames.
* @param {number} channelCount Buffer channel count.
*/
constructor(length, channelCount) {
this._readIndex = 0;
this._writeIndex = 0;
this._framesAvailable = 0;

this._channelCount = channelCount;
this._length = length;
this._channelData = [];
for (let i = 0; i < this._channelCount; ++i) {
this._channelData[i] = new Float32Array(length);
}
}

/**
* Getter for Available frames in buffer.
*
* @return {number} Available frames in buffer.
*/
get framesAvailable() {
return this._framesAvailable;
}

/**
* Push a sequence of Float32Arrays to buffer.
*
* @param {array} arraySequence A sequence of Float32Arrays.
*/
push(arraySequence) {
// The channel count of arraySequence and the length of each channel must
// match with this buffer obejct.

// Transfer data from the |arraySequence| storage to the internal buffer.
let sourceLength = arraySequence[0].length;
for (let i = 0; i < sourceLength; ++i) {
let writeIndex = (this._writeIndex + i) % this._length;
for (let channel = 0; channel < this._channelCount; ++channel) {
this._channelData[channel][writeIndex] = arraySequence[channel][i];
}
}

this._writeIndex += sourceLength;
if (this._writeIndex >= this._length) {
this._writeIndex = 0;
}

// For excessive frames, the buffer will be overwritten.
this._framesAvailable += sourceLength;
if (this._framesAvailable > this._length) {
this._framesAvailable = this._length;
}
}

/**
* Pull data out of buffer and fill a given sequence of Float32Arrays.
*
* @param {array} arraySequence An array of Float32Arrays.
*/
pull(arraySequence) {
// The channel count of arraySequence and the length of each channel must
// match with this buffer obejct.

// If the FIFO is completely empty, do nothing.
if (this._framesAvailable === 0) {
return;
}

let destinationLength = arraySequence[0].length;

// Transfer data from the internal buffer to the |arraySequence| storage.
for (let i = 0; i < destinationLength; ++i) {
let readIndex = (this._readIndex + i) % this._length;
for (let channel = 0; channel < this._channelCount; ++channel) {
arraySequence[channel][i] = this._channelData[channel][readIndex];
}
}

this._readIndex += destinationLength;
if (this._readIndex >= this._length) {
this._readIndex = 0;
}

this._framesAvailable -= destinationLength;
if (this._framesAvailable < 0) {
this._framesAvailable = 0;
}
}
}

// export an object for compatibility with preval.require()
module.exports = {
default: RingBuffer
};
24 changes: 20 additions & 4 deletions src/audioWorklet/soundFileProcessor.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
// import processor name via preval.require so that it's available as a value at compile time
// import dependencies via preval.require so that they're available as values at compile time
const processorNames = preval.require('./processorNames');
const RingBuffer = preval.require('./ringBuffer').default;

class SoundFileProcessor extends AudioWorkletProcessor {
constructor(options) {
super();

const processorOptions = options.processorOptions || {};
this.bufferSize = processorOptions.bufferSize || 256;
this.inputRingBuffer = new RingBuffer(this.bufferSize, 1);
this.inputRingBufferArraySequence = [new Float32Array(this.bufferSize)];
}

process(inputs) {
const input = inputs[0];
const inputChannel = input[0];
const position = inputChannel[inputChannel.length - 1] || 0;
// we only care about the first input channel, because that contains the position data
this.inputRingBuffer.push([input[0]]);

if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {
this.inputRingBuffer.pull(this.inputRingBufferArraySequence);
const inputChannel = this.inputRingBufferArraySequence[0];
const position = inputChannel[inputChannel.length - 1] || 0;

this.port.postMessage({ name: 'position', position: position });
this.port.postMessage({ name: 'position', position: position });
}

return true;
}
Expand Down
Loading

0 comments on commit c02bc88

Please sign in to comment.