Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ScriptProcessorNode with AudioWorkletNode in p5.SoundFile and p5.Amplitude #373

Merged
merged 3 commits into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 190 additions & 181 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.

28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 30 additions & 73 deletions src/amplitude.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

/**
* Amplitude measures volume between 0.0 and 1.0.
Expand Down Expand Up @@ -50,38 +51,42 @@ define(function (require) {

// set audio context
this.audiocontext = p5sound.audiocontext;
this.processor = this.audiocontext.createScriptProcessor(this.bufferSize, 2, 1);
this._workletNode = new AudioWorkletNode(this.audiocontext, processorNames.amplitudeProcessor, {
outputChannelCount: [1],
parameterData: { smoothing: smoothing || 0 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a potential improvement, we could check the range here, as in the smooth method. But we could also just let the parameterDescriptors handle enforcing (and choosing whether to notify the user) about the range.

processorOptions: { normalize: false }
});

this._workletNode.port.onmessage = function(event) {
if (event.data.name === 'amplitude') {
this.volume = event.data.volume;
this.volNorm = event.data.volNorm;
this.stereoVol = event.data.stereoVol;
this.stereoVolNorm = event.data.stereoVolNorm;
}
}.bind(this);

// for connections
this.input = this.processor;
this.input = this._workletNode;

this.output = this.audiocontext.createGain();
// smoothing defaults to 0
this.smoothing = smoothing || 0;


// the variables to return
this.volume = 0;
this.average = 0;

this.volNorm = 0;
this.stereoVol = [0, 0];
this.stereoAvg = [0, 0];
this.stereoVolNorm = [0, 0];

this.volMax = 0.001;
this.normalize = false;

this.processor.onaudioprocess = this._audioProcess.bind(this);


this.processor.connect(this.output);
this._workletNode.connect(this.output);
this.output.gain.value = 0;

// this may only be necessary because of a Chrome bug
this.output.connect(this.audiocontext.destination);

// connect to p5sound master output by default, unless set by input()
p5sound.meter.connect(this.processor);
p5sound.meter.connect(this._workletNode);

// add this p5.SoundFile to the soundArray
p5sound.soundArray.push(this);
Expand Down Expand Up @@ -128,29 +133,29 @@ define(function (require) {
p5sound.meter.disconnect();

if (smoothing) {
this.smoothing = smoothing;
this._workletNode.parameters.get('smoothing').value = smoothing;
}

// connect to the master out of p5s instance if no snd is provided
if (source == null) {
console.log('Amplitude input source is not ready! Connecting to master output instead');
p5sound.meter.connect(this.processor);
p5sound.meter.connect(this._workletNode);
}

// if it is a p5.Signal
else if (source instanceof p5.Signal) {
source.output.connect(this.processor);
source.output.connect(this._workletNode);
}
// connect to the sound if it is available
else if (source) {
source.connect(this.processor);
this.processor.disconnect();
this.processor.connect(this.output);
source.connect(this._workletNode);
this._workletNode.disconnect();
this._workletNode.connect(this.output);
}

// otherwise, connect to the master out of p5s instance (default)
else {
p5sound.meter.connect(this.processor);
p5sound.meter.connect(this._workletNode);
}
};

Expand All @@ -172,56 +177,6 @@ define(function (require) {
}
};

// TO DO make this stereo / dependent on # of audio channels
p5.Amplitude.prototype._audioProcess = function(event) {

for (var channel = 0; channel < event.inputBuffer.numberOfChannels; channel++) {
var inputBuffer = event.inputBuffer.getChannelData(channel);
var bufLength = inputBuffer.length;

var total = 0;
var sum = 0;
var x;

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

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

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

// add volume from all channels together
var self = this;
var volSum = this.stereoVol.reduce(function(previousValue, currentValue, index) {
self.stereoVolNorm[index - 1] = Math.max(Math.min(self.stereoVol[index - 1]/self.volMax, 1), 0);
self.stereoVolNorm[index] = Math.max(Math.min(self.stereoVol[index]/self.volMax, 1), 0);

return previousValue + currentValue;
});

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

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


};

/**
* Returns a single Amplitude reading at the moment it is called.
* For continuous readings, run in the draw loop.
Expand Down Expand Up @@ -288,6 +243,7 @@ define(function (require) {
else {
this.normalize = !this.normalize;
}
this._workletNode.port.postMessage({ name: 'toggleNormalize', normalize: this.normalize });
};

/**
Expand All @@ -300,7 +256,7 @@ define(function (require) {
*/
p5.Amplitude.prototype.smooth = function(s) {
if (s >= 0 && s < 1) {
this.smoothing = s;
this._workletNode.parameters.get('smoothing').value = s;
} else {
console.log('Error: smoothing must be between 0 and 1');
}
Expand All @@ -320,7 +276,8 @@ define(function (require) {
delete this.output;
}

delete this.processor;
this._workletNode.disconnect();
delete this._workletNode;
};

});
93 changes: 93 additions & 0 deletions src/audioWorklet/amplitudeProcessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// import processor name via preval.require so that it's available as a value at compile time
const processorNames = preval.require('./processorNames');

class AmplitudeProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{
name: 'smoothing',
defaultValue: 0,
minValue: 0,
maxValue: 1,
automationRate: 'k-rate'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inspecting this in Chrome 75.0.3770.100, somehow it has become 'a-rate'. Confusing, but looks like you're following the spec https://webaudio.github.io/web-audio-api/#ref-for-enumdef-automationrate%E2%91%A1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oshoham seems like this is related to a bug in the polyfill:

In Firefox and Safari (using the ScriptProcessorNode fallback) the smoothing param is being passed to the process method as a 4096 frame Float32 buffer, rather than as a number. This is causing NaN values when we attempt to smooth.

I think the best way around this for now is to make Smoothing one of the processorOptions that updates on a message, like toggleNormalize does. But what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I see the bug, it looks like the polyfill is assuming that all worklet parameters are a-rate: https://github.com/GoogleChromeLabs/audioworklet-polyfill/blob/d6f1a14dacc597876b1a5f9cd8719729a131b585/src/index.js#L96

I think your suggested approach makes sense - another possible solution is to treat the smoothing parameter as a-rate (i.e. a Float32 buffer of values) and update the processor code accordingly. This seems more in line with the intended use case of AudioParams, although probably less efficient for non-AudioWorklet browsers. What's your opinion?

}
];
}

constructor(options) {
super();

const processorOptions = options.processorOptions || {};
this.normalize = processorOptions.normalize || false;

this.stereoVol = [0, 0];
this.stereoVolNorm = [0, 0];

this.volMax = 0.001;

this.port.onmessage = (event) => {
const data = event.data;
if (data.name === 'toggleNormalize') {
this.normalize = data.normalize;
}
};
}

// TO DO make this stereo / dependent on # of audio channels
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const smoothing = parameters.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;
}
}

// ... 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);
}

// 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;

// 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
});

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

return true;
}
}

registerProcessor(processorNames.amplitudeProcessor, AmplitudeProcessor);
Loading