Skip to content

Commit 49ada2a

Browse files
committed
Use a ring buffer to allow for a buffer size of 1024 in p5.SoundRecorder AudioWorklet processor
1 parent 4d3a383 commit 49ada2a

File tree

3 files changed

+153
-16
lines changed

3 files changed

+153
-16
lines changed

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/soundRecorder.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ define(function (require) {
8585

8686
this._workletNode = new AudioWorkletNode(ac, processorNames.recorderProcessor, {
8787
outputChannelCount: [this._outputChannels],
88-
processorOptions: { numInputChannels: this._inputChannels }
88+
processorOptions: {
89+
numInputChannels: this._inputChannels,
90+
bufferSize: 1024
91+
}
8992
});
9093

9194
this._workletNode.port.onmessage = function(event) {

0 commit comments

Comments
 (0)