Webaudio panner crackling sound when position is changed - javascript

I'm working on a 3D web app where I want to use positional 3D audio.
I started noticing that a crackling noise appears on the output as the sound source changes its position.
Initially I thought it could be a programming issue or a library issue (I was using howler.js).
I made a very basic example based on plain JS and Webaudio API which is shown here
let params={
"xPosition":0,
"zPosition":-1
};
let gui=new dat.GUI( { autoPlace: true, width: 500 });
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;
let panner;
let listener;
let source;
let osc;
function initWebAudio(){
audioCtx = new AudioContext();
panner = audioCtx.createPanner();
listener = audioCtx.listener;
osc = audioCtx.createOscillator();
osc.frequency.value = 70;
osc.connect(panner);
osc.start(0);
panner.connect(audioCtx.destination);
panner.panningModel = 'HRTF';
panner.distanceModel = 'linear';
panner.maxDistance = 60;
panner.refDistance = 1;
panner.rolloffFactor = 1;
panner.coneInnerAngle = 360;
panner.coneOuterAngle = 360;
panner.coneOuterGain = 0;
panner.positionX.setValueAtTime(0,audioCtx.currentTime);
panner.positionY.setValueAtTime(1,audioCtx.currentTime);
panner.positionZ.setValueAtTime(1,audioCtx.currentTime);
if(panner.orientationX) {
panner.orientationX.value = 1;
panner.orientationY.value = 0;
panner.orientationZ.value = 0;
} else {
panner.setOrientation(1,0,0);
}
if(listener.forwardX) {
listener.forwardX.value = 0;
listener.forwardY.value = 0;
listener.forwardZ.value = -1;
listener.upX.value = 0;
listener.upY.value = 1;
listener.upZ.value = 0;
} else {
listener.setOrientation(0,0,-1,0,1,0);
}
if(listener.positionX) {
listener.positionX.value = 0;
listener.positionY.value = 0;
listener.positionZ.value = 0;
} else {
listener.setPosition(0,0,0);
}
}
function positionPanner() {
if(panner.positionX) {
panner.positionX.setValueAtTime(params.xPosition, audioCtx.currentTime);
} else {
panner.setPosition(params.xPosition,0,params.zPosition);
}
}
function tick(){
positionPanner();
}
function onClickStart(){
initWebAudio();
gui.add(osc.frequency,"value",50,220).name("frequency");
setInterval(tick,50);
}
function buildMenu(){
gui.add(params,"xPosition",-3,3).step(0.001);
gui.add(window,"onClickStart").name("start");
}
buildMenu();
https://jsfiddle.net/fedeM75/t9vpm8so/23/
Press start, then as you change the xPosition slider a crackling sound appears.
It is specially noticeable using headphones.
I searched on google and some people say that it has to do with the rate of change of the position. But I tried with different value ranges and it still happens with small changes.
By the way if the condition to use the panner is to have a very slow rate of change in the position it does not seem to be useful in real world cases.
I use a timer to update the position but the same happens using requestAnimationFrame()
Does anyone has a clue on why this happens and how to solve it?

I had this crackling audio with a simple GainNode. It could be a bug indeed since a simple implementation in the ScriptProcessor does work. Or it's the fact that I/we just don't get how the timing with audioCtx.currentTime should be handled?
For me the solution was to ditch the gain node and use a ScriptProcessor (deprecated: you could/should use an AudioWorkletNode) with my own gain applied to the audio signal directly:
let myGain = 1.0; // change this as you like without crackling noises
let whiteNoise = audioCtx.createScriptProcessor(4096, 0, 1);
whiteNoise.onaudioprocess = function(e) {
let output = e.outputBuffer.getChannelData(0);
for (let i = 0; i < output.length; i++) {
output[i] = (Math.random() * 2 - 1) * myGain;
}
}
This won't fix your problem directly since you have this problem with the 3D panner, but perhaps you can find sourcecode/examples for such a panner implementation and port it to an AudioWorkletNode? Hope this helps.

Related

THREE.js - Updating BufferGeometry position twice, causes my FPS to suffer

I have an array of TubeBufferGeometrys that im making animate to look as if they're growing out in length. When the animation runs the first time, it works really smoothly at 60fps. But once i set the geometrys position attributes back to zero, the FPS drop to 30.
Ive isolated my animation to run and then rerun once it finished with only the below changing. Heres the basics of my code:
Animation control view
stop() {
this.play = false;
// Start it again
setTimeout(() => {
let i = this.tubeCount;
while (i--) {
this.tubes[i].lastSegmentSet = 0;
this.tubes[i].updateToPercent(0);
}
this.elapsedTime = 0;
this.startTime = Date.now();
this.play = true;
}, 2000)
}
update() {
requestAnimationFrame(this.animate);
// ..render stuff + composer that ive disabled without effect
if (this.play) {
let percent = (Date.now() - this.startTime) / ANIMATE_DURATION;
if (percent >= 1) {
this.stop();
}
let i = this.lineCount;
while (i--) {
this.tubes[i].updateToPercent(percent);
}
}
}
Tube class (The main animation code)
constructor() {
//..other stuff
this.lastSegmentSet = 0;
}
// I first build the paths, then store the position data to use later to animate to. Then i set all the position data to zero
storeVerticies() {
this.positions = this.tube.geometry.attributes.position.array.slice(0);
const length = this.tube.geometry.attributes.position.array.length;
this.tube.geometry.attributes.position.array = new Float32Array(length);
}
setSegment(segment) {
this.setSegmentTo(segment, segment);
}
setSegmentTo(segment, target) {
let position = this.tube.geometry.attributes.position.array;
let startPoint = segment * JOINT_DATA_LENGTH; //JOINT_DATA_LENGTH is the number of values in the buffer geometry to update a segment
let targetPoint = target * JOINT_DATA_LENGTH;
let n = JOINT_DATA_LENGTH;
while (n--) {
position[startPoint + n] = this.positions[targetPoint + n];
}
}
updateToPercent(percent) {
let endSegment = Math.floor(percent * this.segmentCount);
while (this.lastSegmentSet <= endSegment) {
this.setSegment(this.lastSegmentSet++);
}
let n = this.lastSegmentSet;
while (n <= this.segmentCount + 1) {
this.setSegmentTo(n++, this.lastSegmentSet);
}
this.tube.geometry.attributes.position.needsUpdate = true;
}
Will put bounty when possible

How can I get an AudioBufferSourceNode's current time?

When working with audio elements (<audio>) or contexts(AudioContext), you can check their currentTime property to know exactly the play time of your buffer.
All of this is fine an dandy until I created multiple sources (or AudioBufferSourceNode) in a single AudioContext.
The sources could be played at different times, therefore I would need to know their corresponding currentTime's, to illustrate:
Some base code for you to work off:
buffer1 = [0,1,0]; //not real buffers
buffer2 = [1,0,1];
ctx = new AudioContext();
source1 = ctx.createBufferSourceNode();
source1.buffer = buffer1;
source1.connect(ctx.destination);
source1.start(0);
source2 = ctx.createBufferSourceNode();
source2.buffer = buffer2;
source2.connect(ctx.destination);
setTimeout(1000/*some time later*/){
source2.start(0);
}
setTimeout(1500/*some more time later*/){
getCurrentTime();
}
function getCurrentTime(){
/* magic */
/* more magic */
console.log("the sources currentTime values are obviously 1500 (source1) and 500 (source2).");
}
What I usually do is create a wrapper for the audio source node that keeps track of the playback state. I've tried to minimise the code below to show the basics.
The core idea is to keep track of the time the sound is started and the time the sound is 'paused' and use those values to get the current time and to resume playback from the paused position.
I put a working example on codepen
function createSound(buffer, context) {
var sourceNode = null,
startedAt = 0,
pausedAt = 0,
playing = false;
var play = function() {
var offset = pausedAt;
sourceNode = context.createBufferSource();
sourceNode.connect(context.destination);
sourceNode.buffer = buffer;
sourceNode.start(0, offset);
startedAt = context.currentTime - offset;
pausedAt = 0;
playing = true;
};
var pause = function() {
var elapsed = context.currentTime - startedAt;
stop();
pausedAt = elapsed;
};
var stop = function() {
if (sourceNode) {
sourceNode.disconnect();
sourceNode.stop(0);
sourceNode = null;
}
pausedAt = 0;
startedAt = 0;
playing = false;
};
var getPlaying = function() {
return playing;
};
var getCurrentTime = function() {
if(pausedAt) {
return pausedAt;
}
if(startedAt) {
return context.currentTime - startedAt;
}
return 0;
};
var getDuration = function() {
return buffer.duration;
};
return {
getCurrentTime: getCurrentTime,
getDuration: getDuration,
getPlaying: getPlaying,
play: play,
pause: pause,
stop: stop
};
}
Old thread (and very useful! thanks!) , but maybe worth mentioning that in the example above instead of
function update() {
window.requestAnimationFrame(update);
info.innerHTML = sound.getCurrentTime().toFixed(1) + '/' + sound.getDuration().toFixed(1);
}
update();
it may be more precise and less resource intensive to use a createScriptProcessor
like explained in this post
const audioBuffer = await audioContext.decodeAudioData(response.data);
const chan = audioBuffer.numberOfChannels;
const scriptNode = audioContext.createScriptProcessor(4096, chan, chan);
scriptNode.connect(audioContext.destination);
scriptNode.onaudioprocess = (e) => {
// ---> audio loop <----
};
[Update]
Note: As of the August 29 2014 Web Audio API spec publication, this feature has been marked as deprecated, and was replaced by AudioWorklet (see AudioWorkletNode). https://developers.google.com/web/updates/2017/12/audio-worklet

Web Audio Api writing oscilator with createBuffer()

I am trying to make a sine wave with createBuffer(). Like following code.
var TWOPI = Math.PI * 2;
var Sinewave = function(freq, amp, phase){
var self = this;
this.src;
this.init = function(){
var osc = new self.Osc(phase);
var buffer = ac.createBuffer(1, osc.samplerate, osc.samplerate);
var buffering = buffer.getChannelData(0);
for(i = 0; i < buffering.length; i++){
buffering[i] = amp * this.sinetick(osc, freq);
}
this.src = ac.createBufferSource();
this.src.buffer = buffer;
this.src.loop = true;
this.src.connect(contect.destination);
}
this.Osc = function(_phase){
this.samplerate = context.sampleRate;
this.twopiovsr = TWOPI/this.samplerate;
this.curfreq = 0;
this.curphase = _phase;
this.incr = 0.0;
}
this.sinetick = function(osc, freq){
var val = Math.sin(osc.curphase);
if(osc.curfreq != freq){
osc.curfreq = freq;
osc.incr = osc.twopiovsr * freq;
}
osc.curphase += osc.incr;
if(osc.curphase >= TWOPI){
osc.curphase -= TWOPI;
}
if(osc.curphase < 0.0){
osc.curphase += TWOPI;
}
return val;
}
this.play = function(duration){
this.src.start(0);
this.src.stop(duration);
}
}
This only contains function for sine wave but I've succeeded to implement other shapes as well.
Reason for this in the first place was to set phase offset, if there's a better option please give me a notice.
So, When I look at the output signal through oscilloscope they look like how they should be. However when I try to modulate frequency of a regular createOscillator node with the signal created by code above, output is not pretty. When regular createOscillators nodes fm themselves, waves on the oscilloscope changes horizontally, only frequencies, but with the signal generated with the code above gives changes in both horizontal and vertical giving clips
what could be the problem? Thanks
And the problem was
var buffer = ac.createBuffer(1, osc.samplerate, osc.samplerate);
which should be
var buffer = ac.createBuffer(1, osc.samplerate/2, osc.samplerate);
Thanks guys

Creating an javascript graph with marker that is synchronize with a movie

I'm trying to create an online web tool for eeg signal analysis. The tool suppose to display a graph of an eeg signal synchronize with a movie that was display to a subject.
I've already implemented it successfully on csharp but I can't find a way to do it easily with any of the know javascript chart that I saw.
A link of a good tool that do something similar can be found here:
http://www.mesta-automation.com/real-time-line-charts-with-wpf-and-dynamic-data-display/
I've tried using dygraph, and google chart. I know that it should be relatively easy to create an background thread on the server that examine the movie state every ~50ms. What I was not able to do is to create a marker of the movie position on the chart itself dynamically. I was able to draw on the dygraph but was not able to change the marker location.
just for clarification, I need to draw a vertical line as a marker.
I'm in great suffering. Please help :)
Thanks to Danvk I figure out how to do it.
Below is a jsfiddler links that demonstrate such a solution.
http://jsfiddle.net/ng9vy8mb/10/embedded/result/
below is the javascript code that do the task. It changes the location of the marker in synchronizing with the video.
There are still several improvement that can be done.
Currently, if the user had zoomed in the graph and then click on it, the zoom will be reset.
there is no support for you tube movies
I hope that soon I can post a more complete solution that will also enable user to upload the graph data and video from their computer
;
var dc;
var g;
var v;
var my_graph;
var my_area;
var current_time = 0;
//when the document is done loading, intialie the video events listeners
$(document).ready(function () {
v = document.getElementsByTagName('video')[0];
v.onseeking = function () {
current_time = v.currentTime * 1000;
draw_marker();
};
v.oncanplay = function () {
CreateGraph();
};
v.addEventListener('timeupdate', function (event) {
var t = document.getElementById('time');
t.innerHTML = v.currentTime;
g.updateOptions({
isZoomedIgnoreProgrammaticZoom: true
});
current_time = v.currentTime * 1000;
}, false);
});
function change_movie_position(e, x, points) {
v.currentTime = x / 1000;
}
function draw_marker() {
dc.fillStyle = "rgba(255, 0, 0, 0.5)";
var left = my_graph.toDomCoords(current_time, 0)[0] - 2;
var right = my_graph.toDomCoords(current_time + 2, 0)[0] + 2;
dc.fillRect(left, my_area.y, right - left, my_area.h);
};
//data creation
function CreateGraph() {
number_of_samples = v.duration * 1000;
// A basic sinusoidal data series.
var data = [];
for (var i = 0; i < number_of_samples; i++) {
var base = 10 * Math.sin(i / 90.0);
data.push([i, base, base + Math.sin(i / 2.0)]);
}
// Shift one portion out of line.
var highlight_start = 450;
var highlight_end = 500;
for (var i = highlight_start; i <= highlight_end; i++) {
data[i][2] += 5.0;
}
g = new Dygraph(
document.getElementById("div_g"),
data, {
labels: ['X', 'Est.', 'Actual'],
animatedZooms: true,
underlayCallback: function (canvas, area, g) {
dc = canvas;
my_area = area;
my_graph = g;
bottom_left = g.toDomCoords(0, 0);
top_right = g.toDomCoords(highlight_end, +20);
draw_marker();
}
});
g.updateOptions({
clickCallback: change_movie_position
}, true);
}

Multiple different Collada scenes with Three.js animation won't work

I have a multiple models in Collada format (knight.dae & archer.dae).
My problem is that i can't get them all to animate(lets say Idle with is 2-3frames).
When i load the scene i either get only one animated model and one stil model(no animation,no nothing,it's like he's being modeled in 3ds max).
I know my problem is with the skin and morphs but i searched alot and didnt find an answer and due to my lack of experience my attempts have failed so far.
Help pls!
//animation length of the model is 150(and it hosts 4 different animations)
var startFrame = 0, endFrame = 150, totalFrames = endFrame - startFrame, lastFrame;
var urls = [];
var characters = [];
urls.push('3D/archer/archer.dae');
urls.push('3D/archer/archer.dae');
//here's the loader
loader = new THREE.ColladaLoader();
loader.options.convertUpAxis = true;
for (var i=0;i<urls.length;i++) {
loader.load(urls[i],function colladaReady( collada ){
player = collada.scene;
player.scale.x = player.scale.y = player.scale.z =10;
player.position.y=115;
player.position.z=i*200;
player.updateMatrix()
skin = collada.skins [ 0 ];
//skinArray.push(skin);;
var mesh=new THREE.Mesh(new THREE.CubeGeometry(10,20,10,1,1,1));
player.add(mesh);
characters.push(mesh);
scene.add( player );
});
}
//i added the cube because i use raycaster and it doesnt detect collada obj
// Here is where i try my animation.
function animate() {
requestAnimationFrame( animate );
render();
}
function render() {
update();
renderer.render(scene,camera);
}
function update() {
var delta = clock.getDelta();
delta = delta / 2;
if ( t > 1 ) t = 0;
if ( skin )
{
skin.morphTargetInfluences[lastFrame] = 0;
var currentFrame = startFrame + Math.floor(t*totalFrames);
skin.morphTargetInfluences[currentFrame] = 1;
t += delta;
lastFrame = currentFrame;
}
}
Try something like this.... at the beginning:
var skins = [];
At your collada callback, something you already seem to have thought about:
skins.push(collada.skins[0]);
At your renderer, instead of the current if (skin) clause:
t += delta;
lastFrame = currentFrame;
var currentFrame = startFrame + Math.floor(t*totalFrames);
for (var i = 0; i < skins.length; i++) {
var skin = skins[i];
if (skin) {
skin.morphTargetInfluences[lastFrame] = 0;
skin.morphTargetInfluences[currentFrame] = 1;
}
}
Point being, you need to loop all the skins in the update() function. I didn't check the frame handling code very carefully as that was not the question.. If your skins have different amount of frames you need to take those into account in your code (maybe make the lastFrame, currentFrame etc variables to arrays matching the skins array).
if ( skinArray[0] && skinArray[1] )
{
skinArray[0].morphTargetInfluences[lastFrame] = 0;
skinArray[1].morphTargetInfluences[lastFrame] = 0;
var currentFrame = startFrame + Math.floor(t*totalFrames);
skinArray[0].morphTargetInfluences[currentFrame] = 1;
skinArray[1].morphTargetInfluences[currentFrame] = 1;
t += delta;
lastFrame = currentFrame;
}
I came up with this code,it does the job but i just don't like it,mainly because it feels it's hardcoded.So if any of you guys can come up with a more elegant solution i'd be more then happy.

Categories

Resources