web audio analyser's getFloatTimeDomainData buffer offset wrt buffers at other times and wrt buffer of 'complete file' - javascript

(question rewritten integrating bits of information from answers, plus making it more concise.)
I use analyser=audioContext.createAnalyser() in order to process audio data, and I'm trying to understand the details better.
I choose an fftSize, say 2048, then I create an array buffer of 2048 floats with Float32Array, and then, in an animation loop
(called 60 times per second on most machines, via window.requestAnimationFrame), I do
analyser.getFloatTimeDomainData(buffer);
which will fill my buffer with 2048 floating point sample data points.
When the handler is called the next time, 1/60 second has passed. To calculate how much that is in units of samples,
we have to divide it by the duration of 1 sample, and get (1/60)/(1/44100) = 735.
So the next handler call takes place (on average) 735 samples later.
So there is overlap between subsequent buffers, like this:
We know from the spec (search for 'render quantum') that everything happens in "chunck sizes" which are multiples of 128.
So (in terms of audio processing), one would expect that the next handler call will usually be either 5*128 = 640 samples later,
or else 6*128 = 768 samples later - those being the multiples of 128 closest to 735 samples = (1/60) second.
Calling this amount "Δ-samples", how do I find out what it is (during each handler call), 640 or 768 or something else?
Reliably, like this:
Consider the 'old buffer' (from previous handler call). If you delete "Δ-samples" many samples at the beginning, copy the remainder, and then append "Δ-samples" many new samples, that should be the current buffer. And indeed, I tried that,
and that is the case. It turns out "Δ-samples" often is 384, 512, 896. It is trivial but time consuming to determine
"Δ-samples" in a loop.
I would like to compute "Δ-samples" without performing that loop.
One would think the following would work:
(audioContext.currentTime() - (result of audioContext.currentTime() during last time handler ran))/(duration of 1 sample)
I tried that (see code below where I also "stich together" the various buffers, trying to reconstruct the original buffer),
and - surprise - it works about 99.9% of the time in Chrome, and about 95% of the time in Firefox.
I also tried audioContent.getOutputTimestamp().contextTime, which does not work in Chrome, and works 9?% in Firefox.
Is there any way to find "Δ-samples" (without looking at the buffers), which works reliably?
Second question, the "reconstructed" buffer (all the buffers from callbacks stitched together), and the original sound buffer
are not exactly the same, there is some (small, but noticable, more than usual "rounding error") difference, and that is bigger in Firefox.
Where does that come from? - You know, as I understand the spec, those should be the same.
var soundFile = 'https://mathheadinclouds.github.io/audio/sounds/la.mp3';
var audioContext = null;
var isPlaying = false;
var sourceNode = null;
var analyser = null;
var theBuffer = null;
var reconstructedBuffer = null;
var soundRequest = null;
var loopCounter = -1;
var FFT_SIZE = 2048;
var rafID = null;
var buffers = [];
var timesSamples = [];
var timeSampleDiffs = [];
var leadingWaste = 0;
window.addEventListener('load', function() {
soundRequest = new XMLHttpRequest();
soundRequest.open("GET", soundFile, true);
soundRequest.responseType = "arraybuffer";
//soundRequest.onload = function(evt) {}
soundRequest.send();
var btn = document.createElement('button');
btn.textContent = 'go';
btn.addEventListener('click', function(evt) {
goButtonClick(this, evt)
});
document.body.appendChild(btn);
});
function goButtonClick(elt, evt) {
initAudioContext(togglePlayback);
elt.parentElement.removeChild(elt);
}
function initAudioContext(callback) {
audioContext = new AudioContext();
audioContext.decodeAudioData(soundRequest.response, function(buffer) {
theBuffer = buffer;
callback();
});
}
function createAnalyser() {
analyser = audioContext.createAnalyser();
analyser.fftSize = FFT_SIZE;
}
function startWithSourceNode() {
sourceNode.connect(analyser);
analyser.connect(audioContext.destination);
sourceNode.start(0);
isPlaying = true;
sourceNode.addEventListener('ended', function(evt) {
sourceNode = null;
analyser = null;
isPlaying = false;
loopCounter = -1;
window.cancelAnimationFrame(rafID);
console.log('buffer length', theBuffer.length);
console.log('reconstructedBuffer length', reconstructedBuffer.length);
console.log('audio callback called counter', buffers.length);
console.log('root mean square error', Math.sqrt(checkResult() / theBuffer.length));
console.log('lengths of time between requestAnimationFrame callbacks, measured in audio samples:');
console.log(timeSampleDiffs);
console.log(
timeSampleDiffs.filter(function(val) {
return val === 384
}).length,
timeSampleDiffs.filter(function(val) {
return val === 512
}).length,
timeSampleDiffs.filter(function(val) {
return val === 640
}).length,
timeSampleDiffs.filter(function(val) {
return val === 768
}).length,
timeSampleDiffs.filter(function(val) {
return val === 896
}).length,
'*',
timeSampleDiffs.filter(function(val) {
return val > 896
}).length,
timeSampleDiffs.filter(function(val) {
return val < 384
}).length
);
console.log(
timeSampleDiffs.filter(function(val) {
return val === 384
}).length +
timeSampleDiffs.filter(function(val) {
return val === 512
}).length +
timeSampleDiffs.filter(function(val) {
return val === 640
}).length +
timeSampleDiffs.filter(function(val) {
return val === 768
}).length +
timeSampleDiffs.filter(function(val) {
return val === 896
}).length
)
});
myAudioCallback();
}
function togglePlayback() {
sourceNode = audioContext.createBufferSource();
sourceNode.buffer = theBuffer;
createAnalyser();
startWithSourceNode();
}
function myAudioCallback(time) {
++loopCounter;
if (!buffers[loopCounter]) {
buffers[loopCounter] = new Float32Array(FFT_SIZE);
}
var buf = buffers[loopCounter];
analyser.getFloatTimeDomainData(buf);
var now = audioContext.currentTime;
var nowSamp = Math.round(audioContext.sampleRate * now);
timesSamples[loopCounter] = nowSamp;
var j, sampDiff;
if (loopCounter === 0) {
console.log('start sample: ', nowSamp);
reconstructedBuffer = new Float32Array(theBuffer.length + FFT_SIZE + nowSamp);
leadingWaste = nowSamp;
for (j = 0; j < FFT_SIZE; j++) {
reconstructedBuffer[nowSamp + j] = buf[j];
}
} else {
sampDiff = nowSamp - timesSamples[loopCounter - 1];
timeSampleDiffs.push(sampDiff);
var expectedEqual = FFT_SIZE - sampDiff;
for (j = 0; j < expectedEqual; j++) {
if (reconstructedBuffer[nowSamp + j] !== buf[j]) {
console.error('unexpected error', loopCounter, j);
// debugger;
}
}
for (j = expectedEqual; j < FFT_SIZE; j++) {
reconstructedBuffer[nowSamp + j] = buf[j];
}
//console.log(loopCounter, nowSamp, sampDiff);
}
rafID = window.requestAnimationFrame(myAudioCallback);
}
function checkResult() {
var ch0 = theBuffer.getChannelData(0);
var ch1 = theBuffer.getChannelData(1);
var sum = 0;
var idxDelta = leadingWaste + FFT_SIZE;
for (var i = 0; i < theBuffer.length; i++) {
var samp0 = ch0[i];
var samp1 = ch1[i];
var samp = (samp0 + samp1) / 2;
var check = reconstructedBuffer[i + idxDelta];
var diff = samp - check;
var sqDiff = diff * diff;
sum += sqDiff;
}
return sum;
}
In above snippet, I do the following. I load with XMLHttpRequest a 1 second mp3 audio file from my github.io page (I sing 'la' for 1 second). After it has loaded, a button is shown, saying 'go', and after pressing that, the audio is played back by putting it into a bufferSource node and then doing .start on that. the bufferSource is the fed to our analyser, et cetera
related question
I also have the snippet code on my github.io page - makes reading the console easier.

I think the AnalyserNode is not what you want in this situation. You want to grab the data and keep it synchronized with raf. Use a ScriptProcessorNode or AudioWorkletNode to grab the data. Then you'll get all the data as it comes. No problems with overlap, or missing data or anything.
Note also that the clocks for raf and audio may be different and hence things may drift over time. You'll have to compensate for that yourself if you need to.

Unfortunately there is no way to find out the exact point in time at which the data returned by an AnalyserNode was captured. But you might be on the right track with your current approach.
All the values returned by the AnalyserNode are based on the "current-time-domain-data". This is basically the internal buffer of the AnalyserNode at a certain point in time. Since the Web Audio API has a fixed render quantum of 128 samples I would expect this buffer to evolve in steps of 128 samples as well. But currentTime usually evolves in steps of 128 samples already.
Furthermore the AnalyserNode has a smoothingTimeConstant property. It is responsible for "blurring" the returned values. The default value is 0.8. For your use case you probably want to set this to 0.
EDIT: As Raymond Toy pointed out in the comments the smoothingtimeconstant only has an effect on the frequency data. Since the question is about getFloatTimeDomainData() it will have no effect on the returned values.
I hope this helps but I think it would be easier to get all the samples of your audio signal by using an AudioWorklet. It would definitely be more reliable.

I'm not really following your math, so I can't tell exactly what you had wrong, but you seem to look at this in a too complicated manner.
The fftSize doesn't really matter here, what you want to calculate is how many samples have been passed since the last frame.
To calculate this, you just need to
Measure the time elapsed from last frame.
Divide this time by the time of a single frame.
The time of a single frame, is simply 1 / context.sampleRate.
So really all you need is currentTime - previousTime * ( 1 / sampleRate) and you'll find the index in the last frame where the data starts being repeated in the new one.
And only then, if you want the index in the new frame you'd subtract this index from the fftSize.
Now for why you sometimes have gaps, it's because AudioContext.prototype.currentTime returns the timestamp of the beginning of the next block to be passed to the graph.
The one we want here is AudioContext.prototype.getOuputTimestamp().contextTime which represents the timestamp of now, on the same same base as currentTime (i.e the creation of the context).
(function loop(){requestAnimationFrame(loop);})();
(async()=>{
const ctx = new AudioContext();
const buf = await fetch("https://upload.wikimedia.org/wikipedia/en/d/d3/Beach_Boys_-_Good_Vibrations.ogg").then(r=>r.arrayBuffer());
const aud_buf = await ctx.decodeAudioData(buf);
const source = ctx.createBufferSource();
source.buffer = aud_buf;
source.loop = true;
const analyser = ctx.createAnalyser();
const fftSize = analyser.fftSize = 2048;
source.loop = true;
source.connect( analyser );
source.start(0);
// for debugging we use two different buffers
const arr1 = new Float32Array( fftSize );
const arr2 = new Float32Array( fftSize );
const single_sample_dur = (1 / ctx.sampleRate);
console.log( 'single sample duration (ms)', single_sample_dur * 1000);
onclick = e => {
if( ctx.state === "suspended" ) {
ctx.resume();
return console.log( 'starting context, please try again' );
}
console.log( '-------------' );
requestAnimationFrame( () => {
// first frame
const time1 = ctx.getOutputTimestamp().contextTime;
analyser.getFloatTimeDomainData( arr1 );
requestAnimationFrame( () => {
// second frame
const time2 = ctx.getOutputTimestamp().contextTime;
analyser.getFloatTimeDomainData( arr2 );
const elapsed_time = time2 - time1;
console.log( 'elapsed time between two frame (ms)', elapsed_time * 1000 );
const calculated_index = fftSize - Math.round( elapsed_time / single_sample_dur );
console.log( 'calculated index of new data', calculated_index );
// for debugging we can just search for the first index where the data repeats
const real_time = fftSize - arr1.indexOf( arr2[ 0 ] );
console.log( 'real index', real_time > fftSize ? 0 : real_time );
if( calculated_index !== real_time > fftSize ? 0 : real_time ) {
console.error( 'different' );
}
});
});
};
document.body.classList.add('ready');
})().catch( console.error );
body:not(.ready) pre { display: none; }
<pre>click to record two new frames</pre>

Related

Web Audio API : efficiently play a PCM stream

Here is a problem:
My JS application receives raw PCM data (via WebRTC data channel),
The sample rate is 88200 (I can easily change it to 44100 on the other end),
Data is already properly encoded in 4-byte float [-1, 1] little-endian samples,
Data arrives by chunks of 512 samples (512*4 bytes),
Data can start arriving at any moment, it can last any time, it may stop, it may resume.
The goal is to render a sound.
What I did is:
var samples = []; // each element of this array stores a chunk of 512 samples
var audioCtx = new AudioContext();
var source = audioCtx.createBufferSource();
source.buffer = audioCtx.createBuffer(1, 512, 88200);
// bufferSize is 512 because it is the size of chunks
var scriptNode = audioCtx.createScriptProcessor(512, 1, 1);
scriptNode.onaudioprocess = function(audioProcessingEvent) {
// play a chunk if there is at least one.
if (samples.length > 0) {
audioProcessingEvent.outputBuffer.copyToChannel(samples.shift(), 0, 0);
}
};
source.connect(scriptNode);
scriptNode.connect(audioCtx.destination);
source.start();
peerConnection.addEventListener("datachannel", function(e) {
e.channel.onmessage = function(m) {
var values = new Float32Array(m.data);
samples.push(values);
};
);
There are few issues:
audioProcessingEvent.outputBuffer.sampleRate is always 48000. Apparently is does not depend on the bitrate of source and I could not find a way to set it to 88200, 44100 nor any other value. Sound is rendered with a delay that constantly grows.
ScriptProcessorNode is deprecated.
It is very expensive method in terms of processor.
Thank you in advance for any suggestion!
You want an AudioBuffer.
You can copy raw PCM data into its channels directly from your TypedArray.
You can specify its sampleRate, and the AudioContext will take care of the resampling to match the audio card's settings.
However beware, 2048 bytes per chunk means that every chunk will represent only 5ms of audio data #88Khz: We pass a Float32Array, so the byteSize is 4 and 2048 / 4 / 88200 = ±0.0058s.
You will probably want to increase this, and to implement some buffering strategy.
Here is a little demo as a proof of concept storing chunks' data into a buffer Float32Array.
const min_sample_duration = 2; // sec
const sample_rate = 88200; // Hz
// how much data is needed to play for at least min_sample_duration
const min_sample_size = min_sample_duration * sample_rate;
const fetching_interval = 100; // ms
// you'll probably want this much bigger
let chunk_size = 2048; // bytes
const log = document.getElementById( 'log' );
const btn = document.getElementById( 'btn' );
btn.onclick = e => {
let stopped = false;
let is_reading = false;
const ctx = new AudioContext();
// to control output volume
const gain = ctx.createGain();
gain.gain.value = 0.01;
gain.connect( ctx.destination );
// this will get updated at every new fetch
let fetched_data = new Float32Array( 0 );
// keep it accessible so we can stop() it
let active_node;
// let's begin
periodicFetch();
// UI update
btn.textContent = "stop";
btn.onclick = e => {
stopped = true;
if( active_node ) { active_node.stop(0); }
};
oninput = handleUIEvents;
// our fake fetcher, calls itself every 50ms
function periodicFetch() {
// data from server (here just some noise)
const noise = Float32Array.from( { length: chunk_size / 4 }, _ => (Math.random() * 1) - 0.5 );
// we concatenate the data just fetched with what we have already buffered
fetched_data = concatFloat32Arrays( fetched_data, noise );
// for demo only
log.textContent = "buffering: " + fetched_data.length + '/ ' + min_sample_size;
if( !stopped ) {
// do it again
setTimeout( periodicFetch , fetching_interval );
}
// if we are not actively reading and have fetched enough
if( !is_reading && fetched_data.length > min_sample_size ) {
readingLoop(); // start reading
}
}
function readingLoop() {
if( stopped || fetched_data.length < min_sample_size ) {
is_reading = false;
return;
}
// let the world know we are actively reading
is_reading = true;
// create a new AudioBuffer
const aud_buf = ctx.createBuffer( 1, fetched_data.length, sample_rate );
// copy our fetched data to its first channel
aud_buf.copyToChannel( fetched_data, 0 );
// clear the buffered data
fetched_data = new Float32Array( 0 );
// the actual player
active_node = ctx.createBufferSource();
active_node.buffer = aud_buf;
active_node.onended = readingLoop; // in case we buffered enough while playing
active_node.connect( gain );
active_node.start( 0 );
}
function handleUIEvents( evt ) {
const type = evt.target.name;
const value = evt.target.value;
switch( type ) {
case "chunk-size":
chunk_size = +value;
break;
case "volume":
gain.gain.value = +value;
break;
}
}
};
// helpers
function concatFloat32Arrays( arr1, arr2 ) {
if( !arr1 || !arr1.length ) {
return arr2 && arr2.slice();
}
if( !arr2 || !arr2.length ) {
return arr1 && arr1.slice();
}
const out = new Float32Array( arr1.length + arr2.length );
out.set( arr1 );
out.set( arr2, arr1.length );
return out;
}
label { display: block }
<button id="btn">start</button>
<pre id="log"></pre>
<div>
<label>Output volume:<input type="range" name="volume" min="0" max="0.5" step="0.01" value="0.01"></label>
</div>
<div>
Size of each chunk fetched:
<label><input type="radio" name="chunk-size" value="2048" checked>2048 bytes (OP's current)</label>
<label><input type="radio" name="chunk-size" value="35280">35280 bytes (barely enough for 0.1s interval)</label>
<label><input type="radio" name="chunk-size" value="44100">44100 bytes (enough for 0.1s interval)</label>
</div>

JavaScript: Assigning a variable if variable changed

In JAVASCRIPT:
If I have a variable which value is constantly changing (100+ times a second). How do I 'record' a specific value at a specific point in time?
Added to this, how do I base this point in time off of another variable of which value has changed?
This needs to be strictly in JavaScript. I've looked at the onChange() method, but I'm unsure if I have to use this in conjunction with HTML for it to work. If not, could someone give me an example where this is not the case?
Cheers
I'm not 100% clear on what you're trying to do, but as Ranjith says you can use setTimeout to run arbitrary code at some (approximate) future time.
This example could likely be improved if I had a bit more detail about what you're doing.
If you're in a node environment you might consider using an event emitter to broadcast changes instead of having to have the variable in scope. (This isn't particularly hard to do in a browser either if that's where you are.)
The html/css parts of this are just for displaying the values in the example; not necessary otherwise.
const rand = document.getElementById('rand');
const snapshot = document.getElementById('snapshot');
let volatile = 0;
// update the value every ~100ms
setInterval(() => {
// assign a new random value
volatile = Math.random();
// display it so we can see what's going on
rand.innerText = volatile;
}, 100);
// do whatever you want with the snapshotted value here
const snap = () => snapshot.innerText = volatile;
// grab the value every 2 seconds
setInterval(snap, 2000);
div {
margin: 2rem;
}
<div>
<div id="rand"></div>
<div id="snapshot"></div>
</div>
Ok - well you can poll variable changes ... even though you can use setters...
Lets compare:
Polling:
let previous;
let watched = 0;
let changes = 0;
let snap = () => previous = watched !== previous && ++changes && watched || previous;
let polling = setInterval(snap, 100);
let delta = 1000 * 2
let start = Date.now();
let last = start;
let now;
let dt = 0
while(start + delta > Date.now()){
now = Date.now();
dt += now - last;
last = now;
if(dt > 100){
watched++;
dt = 0;
}
}
document.getElementsByTagName('h1')[0].innerText = (changes === 0 ? 0 : 100 * watched / changes) + "% hit"
if(watched - changes === watched){
throw Error("polling missed 100%");
}
<h1><h1>
emitting:
const dataChangeEvent = new Event("mutate");
const dataAccessEvent = new Event("access");
// set mock context - as it is needed
let ctx = document.createElement('span');
// add watchable variable
add('watched', 0);
//listen for changes
let changes = 0;
ctx.addEventListener('mutate', () => changes++);
let delta = 1000 * 2
let start = Date.now();
let last = start;
let now;
let dt = 0
while(start + delta > Date.now()){
now = Date.now();
dt += now - last;
last = now;
if(dt > 100){
ctx.watched++;
dt = 0;
}
}
document.getElementsByTagName('h1')[0].innerText = (changes === 0 ? 0 : 100 * ctx.watched / changes) + "% hit"
if(ctx.watched - changes === ctx.watched){
throw Error("trigger missed 100%");
}
function add(name, value){
let store = value
Object.defineProperty(ctx, name, {
get(){
ctx.dispatchEvent(dataAccessEvent, store)
return store;
},
set(value){
ctx.dispatchEvent(dataChangeEvent, {
newVal: value,
oldVal: store,
stamp: Date.now()
});
store = value;
}
})
}
<h1></h1>
The usage of a while loop is on purpose.

Implementing quadtree collision with javascript?

I am working on an io game similar to agar.io and slither.io (using node.js and socket.io) where there are up to 50 players and around 300 foods in 2d space on the map at a time. Players and food are both circular. Every frame, the server needs to check whether a player has collided with food and act accordingly. Players and foods are both arrays of JSON objects with varying coordinates and sizes. The brute-force method would be looping through all the foods, and for each food, looping through all players to see if they are in collision. Of course, that makes 300*50 iterations, 60 times per second (at 60fps), which is of course way too heavy for the server.
I did come across the quadtree method which is a new concept to me. Also my scarce knowledge on javascript is making me wonder how exactly I might implement it. The problems that I cannot solve are the following:
1. Since players can theoretically be of any size (even as big as the map), then how big would the sections that I divide the map in have to be?
2. Even if I do divide the map into sections, then the only way I can see it working is that for every player, I need to get the foods that share the same sections as the player. This is the big question - now matter how much I think of it, I would still need to loop through every food and check if it's in the required sections. How would I do that without looping? Because that still makes 50*300 iterations, 60 times per second, which does not sound in any way faster to me.
tldr: I need to find a way to detect collisions between a set of 50 objects and a set of 300 objects, 60 times per second. How do I do that without looping through 50*300 iterations at 60 fps?
I could not find any information online that answers my questions. I apologize in advance if I have missed something somewhere that could yield the answers I seek.
This is a small example that only checks a single layer, but I think it demonstrates how you can check for collisions without iterating over all objects.
// 2d array of list of things in said square
// NOT A QUADTREE JUST DEMONSTRATING SOMETHING
let quadlayer = [];
for (let i=0;i<4;++i) {
quadlayer[i] = [];
for (let j=0;j<4;++j) {
quadlayer[i][j] = [];
}
}
function insertObject(ur_object) {
quadlayer[ur_object.x][ur_object.y].push(ur_object);
}
function checkCollision(ur_object) {
let other_objects = quadlayer[ur_object.x][ur_object.y];
console.log('comparing against '+other_objects.length+' instead of '+100);
}
for (let i=0;i<10;++i) {
for (let j=0;j<10;++j) {
insertObject({
x:i%4,
y:j%4
})
}
}
checkCollision({x:1,y:2});
An interesting problem... Here's another take, which essentially uses the sweep line algorithm. (For a good explanation of the sweep line algorithm, see https://www.geeksforgeeks.org/given-a-set-of-line-segments-find-if-any-two-segments-intersect/ ).
The typical performance of a sweep line algorithm is O(n log n) compared to brute force of O(n^2).
In this particular implementation of the sweep line algorithm, all objects (food and people) are kept in a queue, with each object having two entries in the queue. The first entry is x - radius and the second entry is x + radius. That is, the queue tracks the lower/left and upper/right x bounds of all the objects. Furthermore, the queue is sorted by the x bounds using function updatePositionInQueue, which is essentially an insertion sort.
This allows the findCollisions routine to simply walk the queue, maintaining an active set of objects that need to be checked against each other. That is, the objects that overlap in the x dimension will be dynamically added and removed from the active set. Ie, when the queue entry represents a left x bound of an object, the object is added to the active set, and when the queue entry represents an right x bound of an object, the object is removed from the active set. So as the queue of objects is walked, each object that is about to be added to the active set only has to be checked for collisions against the small active set of objects with overlapping x bounds.
Note that as the algorithm stands, it checks for all collisions between people-and-people, people-and-food, and food-and-food...
As a pleasant bonus, the updatePositionInQueue routine permits adjustment of the sorted queue whenever a object moves. That is, if a person moves, the coordinates of their x,y position can be updated on the object, and then updatePositionInQueue( this.qRight ) and updatePositionInQueue( this.qLeft ) can be called, which will look to the previous and next objects in the sorted queue to move the updated object until its x bound is properly sorted. Given that the objects position should not be changing that much between frames, the movement of the left and right x bound entries in the queue should be minimal from frame-to-frame.
The code is as follows, which towards the bottom randomly generates object data, populates the queue, and then runs both the sweep line collision check along with a brute force check to verify the results, in addition to reporting on the performance as measured in object-to-object collision checks.
var queueHead = null;
function QueueEntry(paf, lor, x) {
this.paf = paf;
this.leftOrRight = lor;
this.x = x;
this.prev = null;
this.next = null;
}
function updatePositionInQueue( qEntry ) {
function moveEntry() {
// Remove qEntry from current position in queue.
if ( qEntry.prev === null ) queueHead = qEntry.next;
if ( qEntry.prev ) qEntry.prev.next = qEntry.next;
if ( qEntry.next ) qEntry.next.prev = qEntry.prev;
// Add qEntry to new position in queue.
if ( newLocation === null ) {
qEntry.prev = null;
qEntry.next = queueHead;
queueHead = qEntry;
} else {
qEntry.prev = newLocation;
qEntry.next = newLocation.next;
if ( newLocation.next ) newLocation.next.prev = qEntry;
newLocation.next = qEntry;
}
}
// Walk the queue, moving qEntry into the
// proper spot of the queue based on the x
// value. First check against the 'prev' queue
// entry...
let newLocation = qEntry.prev;
while (newLocation && qEntry.x < newLocation.x ) {
newLocation = newLocation.prev;
}
if (newLocation !== qEntry.prev) {
moveEntry();
}
// ...then against the 'next' queue entry.
newLocation = qEntry;
while (newLocation.next && newLocation.next.x < qEntry.x ) {
newLocation = newLocation.next;
}
if (newLocation !== qEntry) {
moveEntry();
}
}
function findCollisions() {
console.log( `\nfindCollisions():\n\n` );
var performanceCount = 0;
var consoleResult = [];
activeObjects = new Set();
var i = queueHead;
while ( i ) {
if ( i.leftOrRight === true ) {
activeObjects.delete( i.paf );
}
if ( i.leftOrRight === false ) {
let iPaf = i.paf;
for ( let o of activeObjects ) {
if ( (o.x - iPaf.x) ** 2 + (o.y - iPaf.y) ** 2 <= (o.radius + iPaf.radius) ** 2 ) {
if ( iPaf.id < o.id ) {
consoleResult.push( `Collision: ${iPaf.id} with ${o.id}` );
} else {
consoleResult.push( `Collision: ${o.id} with ${iPaf.id}` );
}
}
performanceCount++;
}
activeObjects.add( iPaf );
}
i = i.next;
}
console.log( consoleResult.sort().join( '\n' ) );
console.log( `\nfindCollisions collision check count: ${performanceCount}\n` );
}
function bruteForceCollisionCheck() {
console.log( `\nbruteForceCollisionCheck():\n\n` );
var performanceCount = 0;
var consoleResult = [];
for ( i in paf ) {
for ( j in paf ) {
if ( i < j ) {
let o1 = paf[i];
let o2 = paf[j];
if ( (o1.x - o2.x) ** 2 + (o1.y - o2.y) ** 2 <= (o1.radius + o2.radius) ** 2 ) {
if ( o1.id < o2.id ) {
consoleResult.push( `Collision: ${o1.id} with ${o2.id}` );
} else {
consoleResult.push( `Collision: ${o2.id} with ${o1.id}` );
}
}
performanceCount++;
}
}
}
console.log( consoleResult.sort().join( '\n' ) );
console.log( `\nbruteForceCollisionCheck collision check count: ${performanceCount}\n` );
}
function queuePrint() {
var i = queueHead;
while (i) {
console.log(`${i.paf.id}: x(${i.x}) ${i.paf.type} ${i.leftOrRight ? 'right' : 'left'} (x: ${i.paf.x} y: ${i.paf.y} r:${i.paf.radius})\n`);
i = i.next;
}
}
function PeopleAndFood( id, type, x, y, radius ) {
this.id = id;
this.type = type;
this.x = x;
this.y = y;
this.radius = radius;
this.qLeft = new QueueEntry( this, false, x - radius );
this.qRight = new QueueEntry( this, true, x + radius );
// Simply add the queue entries to the
// head of the queue, and then adjust
// their location in the queue.
if ( queueHead ) queueHead.prev = this.qRight;
this.qRight.next = queueHead;
queueHead = this.qRight;
updatePositionInQueue( this.qRight );
if ( queueHead ) queueHead.prev = this.qLeft;
this.qLeft.next = queueHead;
queueHead = this.qLeft;
updatePositionInQueue( this.qLeft );
}
//
// Test algorithm...
//
var paf = [];
const width = 10000;
const height = 10000;
const foodCount = 300;
const foodSizeMin = 10;
const foodSizeMax = 20;
const peopleCount = 50;
const peopleSizeMin = 50;
const peopleSizeMax = 100;
for (i = 0; i < foodCount; i++) {
paf.push( new PeopleAndFood(
i,
'food',
Math.round( width * Math.random() ),
Math.round( height * Math.random() ),
foodSizeMin + Math.round(( foodSizeMax - foodSizeMin ) * Math.random())
));
}
for (i = 0; i < peopleCount; i++) {
paf.push( new PeopleAndFood(
foodCount + i,
'people',
Math.round( width * Math.random() ),
Math.round( height * Math.random() ),
peopleSizeMin + Math.round(( peopleSizeMax - peopleSizeMin ) * Math.random())
));
}
queuePrint();
findCollisions();
bruteForceCollisionCheck();
(Note that the program prints the queue, followed by the results of findCollisions and bruteForceCollisionCheck. Only the tail end of the console appears to show when running the code snippet.)
Am sure that the algorithm can be squeezed a bit more for performance, but for the parameters in the code above, the test runs are showing a brute force check of 61075 collisions vs ~600 for the sweep line algorithm. Obviously the size of the objects will impact this ratio, as the larger the objects, the larger the set of objects with overlapping x bounds that will need to be cross checked...
An enjoyable problem to solve. Hope this helps.

Train neural-net with a sequence ( currently not converging )

Due to the recursive nature, I've been able to activate an lstm, which has only 1 input-neuron, with a sequence by inputting one item at a time.
However, when I attempt to train the network with the same technique, it never converges. The training goes on forever.
Here's what I'm doing, I'm converting a natural-language string to binary and then feeding one digit as a time. The reason I am converting into binary is because the network only takes values between 0 and 1.
I know the training works because when I train with an array of as many values as the input-neurons, in this case 1 so: [0], it converges and trains fine.
I guess I could pass each digit individually, but then it would have an individual ideal-output for each digit. And when the digit appears again with another ideal-output in another training set, it won't converge because how could for example 0 be of class 0 and 1?
Please tell me if I am wrong on this assumption.
How can I train this lstm with a sequence so that similar squences are classified similarly when activated?
Here is my whole trainer-file: https://github.com/theirf/synaptic/blob/master/src/trainer.js
Here is the code that trains the network on a worker:
workerTrain: function(set, callback, options) {
var that = this;
var error = 1;
var iterations = bucketSize = 0;
var input, output, target, currentRate;
var length = set.length;
var start = Date.now();
if (options) {
if (options.shuffle) {
function shuffle(o) { //v1.0
for (var j, x, i = o.length; i; j = Math.floor(Math.random() *
i), x = o[--i], o[i] = o[j], o[j] = x);
return o;
};
}
if(options.iterations) this.iterations = options.iterations;
if(options.error) this.error = options.error;
if(options.rate) this.rate = options.rate;
if(options.cost) this.cost = options.cost;
if(options.schedule) this.schedule = options.schedule;
if (options.customLog){
// for backward compatibility with code that used customLog
console.log('Deprecated: use schedule instead of customLog')
this.schedule = options.customLog;
}
}
// dynamic learning rate
currentRate = this.rate;
if(Array.isArray(this.rate)) {
bucketSize = Math.floor(this.iterations / this.rate.length);
}
// create a worker
var worker = this.network.worker();
// activate the network
function activateWorker(input)
{
worker.postMessage({
action: "activate",
input: input,
memoryBuffer: that.network.optimized.memory
}, [that.network.optimized.memory.buffer]);
}
// backpropagate the network
function propagateWorker(target){
if(bucketSize > 0) {
var currentBucket = Math.floor(iterations / bucketSize);
currentRate = this.rate[currentBucket];
}
worker.postMessage({
action: "propagate",
target: target,
rate: currentRate,
memoryBuffer: that.network.optimized.memory
}, [that.network.optimized.memory.buffer]);
}
// train the worker
worker.onmessage = function(e){
// give control of the memory back to the network
that.network.optimized.ownership(e.data.memoryBuffer);
if(e.data.action == "propagate"){
if(index >= length){
index = 0;
iterations++;
error /= set.length;
// log
if(options){
if(this.schedule && this.schedule.every && iterations % this.schedule.every == 0)
abort_training = this.schedule.do({
error: error,
iterations: iterations
});
else if(options.log && iterations % options.log == 0){
console.log('iterations', iterations, 'error', error);
};
if(options.shuffle) shuffle(set);
}
if(!abort_training && iterations < that.iterations && error > that.error){
activateWorker(set[index].input);
}
else{
// callback
callback({
error: error,
iterations: iterations,
time: Date.now() - start
})
}
error = 0;
}
else{
activateWorker(set[index].input);
}
}
if(e.data.action == "activate"){
error += that.cost(set[index].output, e.data.output);
propagateWorker(set[index].output);
index++;
}
}
A natural language string should not be converted to binary to normalize. Use one-hot encoding instead:
Additionally, I advise you to take a look at Neataptic instead of Synaptic. It fixed a lot of bugs in Synaptic and has more functions for you to use. It has a special option during training, called clear. This tells the network to reset the context every training iteration, so it knows it is starting from the beginning.
Why does your network only have 1 binary input? The networks inputs should make sense. Neural networks are powerful, but you are giving them a very hard task.
Instead you should have multiple inputs, one for each letter. Or even more ideally, one for each word.

merging / layering multiple ArrayBuffers into one AudioBuffer using Web Audio API

I need to layer looping .wav tracks that ultimately I will need to be able to turn on and off and keep in sync.
First I load the tracks and stopped BufferLoader from turning the loaded arraybuffer into an AudioBuffer (hence the false)
function loadTracks(data) {
for (var i = 0; i < data.length; i++) {
trackUrls.push(data[i]['url']);
};
bufferLoader = new BufferLoader(context, trackUrls, finishedLoading);
bufferLoader.load(false);
return loaderDefered.promise;
}
When you click a button on screen it calls startStop().
function startStop(index, name, isPlaying) {
if(!activeBuffer) {
activeBuffer = bufferList[index];
}else{
activeBuffer = appendBuffer(activeBuffer, bufferList[index]);
}
context.decodeAudioData(activeBuffer, function(buffer){
audioBuffer = buffer;
play();
})
function play() {
var scheduledTime = 0.015;
try {
audioSource.stop(scheduledTime);
} catch (e) {}
audioSource = context.createBufferSource();
audioSource.buffer = audioBuffer;
audioSource.loop = true;
audioSource.connect(context.destination);
var currentTime = context.currentTime + 0.010 || 0;
audioSource.start(scheduledTime - 0.005, currentTime, audioBuffer.duration - currentTime);
audioSource.playbackRate.value = 1;
}
Most of the code I found on this guys github.
In the demo you can hear he is layering AudioBuffers.
I have tried the same on my hosting.
Disregarding the argularJS stuff, the Web Audio stuff is happening on the service.js at:
/js/angular/service.js
If you open the console and click the buttons you can see the activeBuffer.byteLength (type ArrayBuffer) is incrementing, however even after being decoded by the context.decodeAudioData method it still only plays the first sound you clicked instead of a merged AudioBuffer
I'm not sure I totally understand your scenario - don't you want these to be playing simultaneously? (i.e. bass gets layered on top of the drums).
Your current code is trying to concatenate an additional audio file whenever you hit the button for that file. You can't just concatenate audio files (in their ENCODED form) and then run it through decode - the decodeAudioData method is decoding the first complete sound in the arraybuffer, then stopping (because it's done decoding the sound).
What you should do is change the logic to concatenate the buffer data from the resulting AudioBuffers (see below). Even this logic isn't QUITE what you should do - this is still caching the encoded audio files, and decoding every time you hit the button. Instead, you should cache the decoded audio buffers, and just concatenate it.
function startStop(index, name, isPlaying) {
// Note we're decoding just the new sound
context.decodeAudioData( bufferList[index], function(buffer){
// We have a decoded buffer - now we need to concatenate it
audioBuffer = buffer;
if(!audioBuffer) {
audioBuffer = buffer;
}else{
audioBuffer = concatenateAudioBuffers(audioBuffer, buffer);
}
play();
})
}
function concatenateAudioBuffers(buffer1, buffer2) {
if (!buffer1 || !buffer2) {
console.log("no buffers!");
return null;
}
if (buffer1.numberOfChannels != buffer2.numberOfChannels) {
console.log("number of channels is not the same!");
return null;
}
if (buffer1.sampleRate != buffer2.sampleRate) {
console.log("sample rates don't match!");
return null;
}
var tmp = context.createBuffer(buffer1.numberOfChannels, buffer1.length + buffer2.length, buffer1.sampleRate);
for (var i=0; i<tmp.numberOfChannels; i++) {
var data = tmp.getChannelData(i);
data.set(buffer1.getChannelData(i));
data.set(buffer2.getChannelData(i),buffer1.length);
}
return tmp;
};
SOLVED:
To get multiple loops of the same duration playing at the same time and keep in sync even when you start and stop them randomly.
First, create all your buffer sources where bufferList is an array of AudioBuffers and the first sound is a sound you are going to read from and overwrite with your other sounds.
function createAllBufferSources() {
for (var i = 0; i < bufferList.length; i++) {
var source = context.createBufferSource();
source.buffer = bufferList[i];
source.loop = true;
bufferSources.push(source);
};
console.log(bufferSources)
}
Then:
function start() {
var rewrite = bufferSources[0];
rewrite.connect(context.destination);
var processNode = context.createScriptProcessor(2048, 2, 2);
rewrite.connect(processNode)
processNode.onaudioprocess = function(e) {
//getting the left and right of the sound we want to overwrite
var left = rewrite.buffer.getChannelData(0);
var right = rewrite.buffer.getChannelData(1);
var overL = [],
overR = [],
i, a, b, l;
l = bufferList.length,
//storing all the loops channel data
for (i = 0; i < l; i++) {
overL[i] = bufferList[i].getChannelData(0);
overR[i] = bufferList[i].getChannelData(1);
}
//looping through the channel data of the sound we are going to overwrite
a = 0, b = overL.length, l = left.length;
for (i = 0; i < l; i++) {
//making sure its a blank before we start to write
left[i] -= left[i];
right[i] -= right[i];
//looping through all the sounds we want to add and assigning the bytes to the old sound, both at the same position
for (a = 0; a < b; a++) {
left[i] += overL[a][i];
right[i] += overR[a][i];
}
left[i] /= b;
right[i] /= b);
}
};
processNode.connect(context.destination);
rewrite.start(0)
}
If you remove a AudioBuffer from bufferList and add it again at any point, it will always be in sync.
EDIT:
Keep in mind that:
-processor node gets garbage collected weirdly.
-This is very taxing, might want to think about using WebWorkers somehow

Categories

Resources