I've been working on a canvas game and been running into some problems involving a memory leak. I thought the problem was to do with rendering and removing entities, but I ran the code without rendering any and it looks like the audio scheduling object is causing a leak by itself. The problem causes the audio to start crackling and cut out after a while. The game still renders but the audio stops - I also noticed the character's shooting becomes much slower (shoot timing is scheduled along with the notes in the scheduler function).
The code I used to deal with the audio is from the A Tale of Two Clocks' tutorial. When I ran the code for the metronome on chrome and did a timeline recording the heap allocation went up.
Why is this code causing a memory leak? It looks fine to me. Timer ID is set to Null?
Image 1: Heap timeline of metronome by itself
Image 2: Timeline of my app running just the metronome function (no game entities)
Image 3: My app running normally
This is the code
function init(){
var container = document.createElement( 'div' );
// CREATE CANVAS ... ...
audioContext = new AudioContext();
requestAnimFrame(draw);
timerWorker = new Worker("js/metronomeworker.js");
timerWorker.onmessage = function(e) {
if (e.data == "tick") {
// console.log("tick!");
scheduler();
}
else {
console.log("message: " + e.data);
}
};
timerWorker.postMessage({"interval":lookahead});
}
function nextNote() {
// Advance current note and time by a 16th note...
var secondsPerBeat = 60.0 / tempo; // Notice this picks up the CURRENT
// tempo value to calculate beat length.
nextNoteTime += 0.25 * secondsPerBeat; // Add beat length to last beat time
current16thNote++; // Advance the beat number, wrap to zero
if (current16thNote == 16) {
current16thNote = 0;
}
}
function scheduleNote( beatNumber, time ) {
// push the note on the queue, even if we're not playing.
notesInQueue.push( { note: beatNumber, time: time } );
if ( (noteResolution==1) && (beatNumber%2))
return; // we're not playing non-8th 16th notes
if ( (noteResolution==2) && (beatNumber%4))
return; // we're not playing non-quarter 8th notes
// create an oscillator // create sample
var osc = audioContext.createOscillator();
osc.connect( audioContext.destination );
if (beatNumber % 16 === 0) // beat 0 == low pitch
osc.frequency.value = 880.0;
else if (beatNumber % 4 === 0 ) // quarter notes = medium pitch
osc.frequency.value = 440.0;
else // other 16th notes = high pitch
osc.frequency.value = 220.0;
osc.start( time ); //sound.play(time)
osc.stop( time + noteLength ); // " "
}
function scheduler() {
// while there are notes that will need to play before the next interval,
// schedule them and advance the pointer.
while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
scheduleNote( current16thNote, nextNoteTime );
nextNote();
}
}
function play() {
isPlaying = !isPlaying;
if (isPlaying) { // start playing
current16thNote = 0;
nextNoteTime = audioContext.currentTime;
timerWorker.postMessage("start");
return "stop";
} else {
timerWorker.postMessage("stop");
return "play";
}
}
Metronome.js:
var timerID=null;
var interval=100;
self.onmessage=function(e){
if (e.data=="start") {
console.log("starting");
timerID=setInterval(function(){postMessage("tick");},interval)
}
else if (e.data.interval) {
console.log("setting interval");
interval=e.data.interval;
console.log("interval="+interval);
if (timerID) {
clearInterval(timerID);
timerID=setInterval(function(){postMessage("tick");},interval)
}
}
else if (e.data=="stop") {
console.log("stopping");
clearInterval(timerID);
timerID=null;
}
};
How I schedule sounds (and shooting) inside scheduleNote() :
if (beatNumber % 4 === 0) {
playSound(samplebb[0], gainNode1);
}
if (planet1play === true) {
if (barNumber % 4 === 0)
if (current16thNote % 1 === 0) {
playSound(samplebb[26], planet1gain);
}
}
if (shootx) {
// Weapon 1
if (gun === 0) {
if (beatNumber === 2 || beatNumber === 6 || beatNumber === 10 || beatNumber === 14) {
shoot(bulletX, bulletY);
playSound(samplebb[3], gainNode2);
}
}
Update
The Audio is still having problems even if I run the game without rendering or updating anything Here It is worse on slower machines.
No idea why this is happening, some kind of audio buffer issue? Anyone have any ideas?
There is a risk of running several setInterval
s here if start
command is called in succession. If they are they will pile up and could explain why the memory is increasing.
I would suggest the following changes. Without you could simply check if timerID existed in the start method, but centralizing the methods will help keep track. clearInterval()
can be called with a null
argument with no consequences other that it will ignore it.
So in essence:
var timerID = null;
var interval = 100;
function tickBack() { // share callback for timer
postMessage("tick")
}
function startTimer() { // centralize start method
stopTimer(); // can be called with no consequence even if id=null
timerID = setInterval(tickBack, interval)
}
function stopTimer() { // centralize stop method
clearInterval(timerID);
timerID = null;
}
onmessage = function(e){
if (e.data === "start") {
startTimer();
}
else if (e.data === "stop") {
stopTimer()
}
else if (e.data.interval) {
interval = e.data.interval;
if (timerID) startTimer();
}
};