Skip to content

Commit c02bc88

Browse files
authored
Merge pull request #376 from oshoham/ring-buffer
Add ring buffers to AudioWorklet processors to support variable buffer sizes
2 parents 4d3a383 + 2014fbc commit c02bc88

12 files changed

+343
-108
lines changed

lib/p5.sound.js

Lines changed: 68 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/p5.sound.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/p5.sound.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/p5.sound.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/amplitude.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict';
22

33
define(function (require) {
4-
var p5sound = require('master');
5-
var processorNames = require('./audioWorklet/processorNames');
4+
const p5sound = require('master');
5+
const { safeBufferSize } = require('helpers');
6+
const processorNames = require('./audioWorklet/processorNames');
67

78
/**
89
* Amplitude measures volume between 0.0 and 1.0.
@@ -47,15 +48,19 @@ define(function (require) {
4748
p5.Amplitude = function(smoothing) {
4849

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

5253
// set audio context
5354
this.audiocontext = p5sound.audiocontext;
5455
this._workletNode = new AudioWorkletNode(this.audiocontext, processorNames.amplitudeProcessor, {
5556
outputChannelCount: [1],
57+
58+
parameterData: { smoothing: smoothing || 0 },
5659
processorOptions: {
5760
normalize: false,
58-
smoothing: smoothing || 0
61+
smoothing: smoothing || 0,
62+
numInputChannels: 2,
63+
bufferSize: this.bufferSize
5964
}
6065
});
6166

src/audioWorklet/amplitudeProcessor.js

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
// import processor name via preval.require so that it's available as a value at compile time
1+
// import dependencies via preval.require so that they're available as values at compile time
22
const processorNames = preval.require('./processorNames');
3+
const RingBuffer = preval.require('./ringBuffer').default;
34

45
class AmplitudeProcessor extends AudioWorkletProcessor {
56
constructor(options) {
67
super();
78

89
const processorOptions = options.processorOptions || {};
9-
this.smoothing = processorOptions.smoothing || 0;
10+
this.numOutputChannels = options.outputChannelCount || 1;
11+
this.numInputChannels = processorOptions.numInputChannels || 2;
1012
this.normalize = processorOptions.normalize || false;
13+
this.smoothing = processorOptions.smoothing || 0;
14+
15+
this.bufferSize = processorOptions.bufferSize || 2048;
16+
this.inputRingBuffer = new RingBuffer(this.bufferSize, this.numInputChannels);
17+
this.outputRingBuffer = new RingBuffer(this.bufferSize, this.numOutputChannels);
18+
this.inputRingBufferArraySequence = new Array(this.numInputChannels).fill(null).map(() => new Float32Array(this.bufferSize));
1119

1220
this.stereoVol = [0, 0];
1321
this.stereoVolNorm = [0, 0];
@@ -30,53 +38,61 @@ class AmplitudeProcessor extends AudioWorkletProcessor {
3038
const output = outputs[0];
3139
const smoothing = this.smoothing;
3240

33-
for (let channel = 0; channel < input.length; ++channel) {
34-
const inputBuffer = input[channel];
35-
const bufLength = inputBuffer.length;
36-
37-
let sum = 0;
38-
for (var i = 0; i < bufLength; i++) {
39-
const x = inputBuffer[i];
40-
if (this.normalize) {
41-
sum += Math.max(Math.min(x / this.volMax, 1), -1) * Math.max(Math.min(x / this.volMax, 1), -1);
42-
} else {
43-
sum += x * x;
41+
this.inputRingBuffer.push(input);
42+
43+
if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {
44+
this.inputRingBuffer.pull(this.inputRingBufferArraySequence);
45+
46+
for (let channel = 0; channel < this.numInputChannels; ++channel) {
47+
const inputBuffer = this.inputRingBufferArraySequence[channel];
48+
const bufLength = inputBuffer.length;
49+
50+
let sum = 0;
51+
for (var i = 0; i < bufLength; i++) {
52+
const x = inputBuffer[i];
53+
if (this.normalize) {
54+
sum += Math.max(Math.min(x / this.volMax, 1), -1) * Math.max(Math.min(x / this.volMax, 1), -1);
55+
} else {
56+
sum += x * x;
57+
}
4458
}
45-
}
4659

47-
// ... then take the square root of the sum.
48-
const rms = Math.sqrt(sum / bufLength);
60+
// ... then take the square root of the sum.
61+
const rms = Math.sqrt(sum / bufLength);
4962

50-
this.stereoVol[channel] = Math.max(rms, this.stereoVol[channel] * smoothing);
51-
this.volMax = Math.max(this.stereoVol[channel], this.volMax);
52-
}
63+
this.stereoVol[channel] = Math.max(rms, this.stereoVol[channel] * smoothing);
64+
this.volMax = Math.max(this.stereoVol[channel], this.volMax);
65+
}
5366

54-
// calculate stero normalized volume and add volume from all channels together
55-
let volSum = 0;
56-
for (let index = 0; index < this.stereoVol.length; index++) {
57-
this.stereoVolNorm[index] = Math.max(Math.min(this.stereoVol[index] / this.volMax, 1), 0);
58-
volSum += this.stereoVol[index];
59-
}
67+
// calculate stero normalized volume and add volume from all channels together
68+
let volSum = 0;
69+
for (let index = 0; index < this.stereoVol.length; index++) {
70+
this.stereoVolNorm[index] = Math.max(Math.min(this.stereoVol[index] / this.volMax, 1), 0);
71+
volSum += this.stereoVol[index];
72+
}
6073

61-
// volume is average of channels
62-
const volume = volSum / this.stereoVol.length;
74+
// volume is average of channels
75+
const volume = volSum / this.stereoVol.length;
6376

64-
// normalized value
65-
const volNorm = Math.max(Math.min(volume / this.volMax, 1), 0);
77+
// normalized value
78+
const volNorm = Math.max(Math.min(volume / this.volMax, 1), 0);
6679

67-
this.port.postMessage({
68-
name: 'amplitude',
69-
volume: volume,
70-
volNorm: volNorm,
71-
stereoVol: this.stereoVol,
72-
stereoVolNorm: this.stereoVolNorm
73-
});
80+
this.port.postMessage({
81+
name: 'amplitude',
82+
volume: volume,
83+
volNorm: volNorm,
84+
stereoVol: this.stereoVol,
85+
stereoVolNorm: this.stereoVolNorm
86+
});
7487

75-
// pass input through to output
76-
for (let channel = 0; channel < output.length; ++channel) {
77-
output[channel].set(input[channel]);
88+
// pass input through to output
89+
this.outputRingBuffer.push(this.inputRingBufferArraySequence);
7890
}
7991

92+
// pull 128 frames out of the ring buffer
93+
// if the ring buffer does not have enough frames, the output will be silent
94+
this.outputRingBuffer.pull(output);
95+
8096
return true;
8197
}
8298
}

src/audioWorklet/recorderProcessor.js

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
// import processor name via preval.require so that it's available as a value at compile time
1+
// import dependencies via preval.require so that they're available as values at compile time
22
const processorNames = preval.require('./processorNames');
3+
const RingBuffer = preval.require('./ringBuffer').default;
34

45
class RecorderProcessor extends AudioWorkletProcessor {
56
constructor(options) {
67
super();
78

89
const processorOptions = options.processorOptions || {};
10+
this.numOutputChannels = options.outputChannelCount || 2;
911
this.numInputChannels = processorOptions.numInputChannels || 2;
12+
this.bufferSize = processorOptions.bufferSize || 1024;
1013
this.recording = false;
1114

1215
this.clear();
@@ -21,7 +24,7 @@ class RecorderProcessor extends AudioWorkletProcessor {
2124
};
2225
}
2326

24-
process(inputs, outputs) {
27+
process(inputs) {
2528
if (!this.recording) {
2629
return true;
2730
} else if (this.sampleLimit && this.recordedSamples >= this.sampleLimit) {
@@ -30,22 +33,26 @@ class RecorderProcessor extends AudioWorkletProcessor {
3033
}
3134

3235
const input = inputs[0];
33-
const output = outputs[0];
34-
35-
for (let channel = 0; channel < output.length; ++channel) {
36-
const inputChannel = input[channel];
37-
if (channel === 0) {
38-
this.leftBuffers.push(inputChannel);
39-
if (this.numInputChannels === 1) {
40-
this.rightBuffers.push(inputChannel);
36+
37+
this.inputRingBuffer.push(input);
38+
39+
if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {
40+
this.inputRingBuffer.pull(this.inputRingBufferArraySequence);
41+
42+
for (let channel = 0; channel < this.numOutputChannels; ++channel) {
43+
const inputChannelCopy = this.inputRingBufferArraySequence[channel].slice();
44+
if (channel === 0) {
45+
this.leftBuffers.push(inputChannelCopy);
46+
if (this.numInputChannels === 1) {
47+
this.rightBuffers.push(inputChannelCopy);
48+
}
49+
} else if (channel === 1 && this.numInputChannels > 1) {
50+
this.rightBuffers.push(inputChannelCopy);
4151
}
42-
} else if (channel === 1 && this.numInputChannels > 1) {
43-
this.rightBuffers.push(inputChannel);
4452
}
45-
}
46-
47-
this.recordedSamples += output[0].length;
4853

54+
this.recordedSamples += this.bufferSize;
55+
}
4956
return true;
5057
}
5158

@@ -87,6 +94,8 @@ class RecorderProcessor extends AudioWorkletProcessor {
8794
clear() {
8895
this.leftBuffers = [];
8996
this.rightBuffers = [];
97+
this.inputRingBuffer = new RingBuffer(this.bufferSize, this.numInputChannels);
98+
this.inputRingBufferArraySequence = new Array(this.numInputChannels).fill(null).map(() => new Float32Array(this.bufferSize));
9099
this.recordedSamples = 0;
91100
this.sampleLimit = null;
92101
}

src/audioWorklet/ringBuffer.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Copyright 2018 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*
16+
* A JS FIFO implementation for the AudioWorklet. 3 assumptions for the
17+
* simpler operation:
18+
* 1. the push and the pull operation are done by 128 frames. (Web Audio
19+
* API's render quantum size in the speficiation)
20+
* 2. the channel count of input/output cannot be changed dynamically.
21+
* The AudioWorkletNode should be configured with the `.channelCount = k`
22+
* (where k is the channel count you want) and
23+
* `.channelCountMode = explicit`.
24+
* 3. This is for the single-thread operation. (obviously)
25+
*
26+
* @class
27+
*/
28+
class RingBuffer {
29+
/**
30+
* @constructor
31+
* @param {number} length Buffer length in frames.
32+
* @param {number} channelCount Buffer channel count.
33+
*/
34+
constructor(length, channelCount) {
35+
this._readIndex = 0;
36+
this._writeIndex = 0;
37+
this._framesAvailable = 0;
38+
39+
this._channelCount = channelCount;
40+
this._length = length;
41+
this._channelData = [];
42+
for (let i = 0; i < this._channelCount; ++i) {
43+
this._channelData[i] = new Float32Array(length);
44+
}
45+
}
46+
47+
/**
48+
* Getter for Available frames in buffer.
49+
*
50+
* @return {number} Available frames in buffer.
51+
*/
52+
get framesAvailable() {
53+
return this._framesAvailable;
54+
}
55+
56+
/**
57+
* Push a sequence of Float32Arrays to buffer.
58+
*
59+
* @param {array} arraySequence A sequence of Float32Arrays.
60+
*/
61+
push(arraySequence) {
62+
// The channel count of arraySequence and the length of each channel must
63+
// match with this buffer obejct.
64+
65+
// Transfer data from the |arraySequence| storage to the internal buffer.
66+
let sourceLength = arraySequence[0].length;
67+
for (let i = 0; i < sourceLength; ++i) {
68+
let writeIndex = (this._writeIndex + i) % this._length;
69+
for (let channel = 0; channel < this._channelCount; ++channel) {
70+
this._channelData[channel][writeIndex] = arraySequence[channel][i];
71+
}
72+
}
73+
74+
this._writeIndex += sourceLength;
75+
if (this._writeIndex >= this._length) {
76+
this._writeIndex = 0;
77+
}
78+
79+
// For excessive frames, the buffer will be overwritten.
80+
this._framesAvailable += sourceLength;
81+
if (this._framesAvailable > this._length) {
82+
this._framesAvailable = this._length;
83+
}
84+
}
85+
86+
/**
87+
* Pull data out of buffer and fill a given sequence of Float32Arrays.
88+
*
89+
* @param {array} arraySequence An array of Float32Arrays.
90+
*/
91+
pull(arraySequence) {
92+
// The channel count of arraySequence and the length of each channel must
93+
// match with this buffer obejct.
94+
95+
// If the FIFO is completely empty, do nothing.
96+
if (this._framesAvailable === 0) {
97+
return;
98+
}
99+
100+
let destinationLength = arraySequence[0].length;
101+
102+
// Transfer data from the internal buffer to the |arraySequence| storage.
103+
for (let i = 0; i < destinationLength; ++i) {
104+
let readIndex = (this._readIndex + i) % this._length;
105+
for (let channel = 0; channel < this._channelCount; ++channel) {
106+
arraySequence[channel][i] = this._channelData[channel][readIndex];
107+
}
108+
}
109+
110+
this._readIndex += destinationLength;
111+
if (this._readIndex >= this._length) {
112+
this._readIndex = 0;
113+
}
114+
115+
this._framesAvailable -= destinationLength;
116+
if (this._framesAvailable < 0) {
117+
this._framesAvailable = 0;
118+
}
119+
}
120+
}
121+
122+
// export an object for compatibility with preval.require()
123+
module.exports = {
124+
default: RingBuffer
125+
};

src/audioWorklet/soundFileProcessor.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1-
// import processor name via preval.require so that it's available as a value at compile time
1+
// import dependencies via preval.require so that they're available as values at compile time
22
const processorNames = preval.require('./processorNames');
3+
const RingBuffer = preval.require('./ringBuffer').default;
34

45
class SoundFileProcessor extends AudioWorkletProcessor {
6+
constructor(options) {
7+
super();
8+
9+
const processorOptions = options.processorOptions || {};
10+
this.bufferSize = processorOptions.bufferSize || 256;
11+
this.inputRingBuffer = new RingBuffer(this.bufferSize, 1);
12+
this.inputRingBufferArraySequence = [new Float32Array(this.bufferSize)];
13+
}
14+
515
process(inputs) {
616
const input = inputs[0];
7-
const inputChannel = input[0];
8-
const position = inputChannel[inputChannel.length - 1] || 0;
17+
// we only care about the first input channel, because that contains the position data
18+
this.inputRingBuffer.push([input[0]]);
19+
20+
if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {
21+
this.inputRingBuffer.pull(this.inputRingBufferArraySequence);
22+
const inputChannel = this.inputRingBufferArraySequence[0];
23+
const position = inputChannel[inputChannel.length - 1] || 0;
924

10-
this.port.postMessage({ name: 'position', position: position });
25+
this.port.postMessage({ name: 'position', position: position });
26+
}
1127

1228
return true;
1329
}

0 commit comments

Comments
 (0)