javascript multiple sounds fired on event - javascript

I'm working on a simple script, trying to put 2 or more sounds in html using js and fire them on certain events (from a game) with callbacks.
I managed to get it working with 1 sound, but it fails when i try to add the second one or more, any ideas?
var audio = new Audio("audio.wav");
audio.addEventListener('ended', function()
{
this.currentTime = 0;
this.play();
}, false);
and callbacks
{
Start: function()
{
audio.play();
},
Stop: function()
{
audio.pause();
},
};

Here's an implementation adapted from a tutorial I found.
// A sound pool to use for the sound effects.
// http://blog.sklambert.com/html5-canvas-game-html5-audio-and-finishing-touches/#adding-html-audio
function SoundPool(filename, volume, maxSize) {
var pool = [];
this.pool = pool;
var currSound = 0;
var that = this;
// Populates the pool array with the given sound.
for (var i = 0; i < maxSize; i++) {
var sound = new Audio(filename);
sound.volume = volume;
pool[i] = sound;
}
this.setVolume = function setVolume(volume) {
for (var i = 0; i < that.pool.length; i++) {
that.pool[i].volume = volume;
}
};
this.mute = function mute(state) {
for (var i = 0; i < that.pool.length; i++) {
// State: toggle, true or false.
if (typeof state == "undefined")
that.pool[i].muted = !that.pool[i].muted;
else
that.pool[i].muted = state;
}
};
// Plays a sound.
this.play = function () {
if (pool[currSound].currentTime == 0 || pool[currSound].ended) {
pool[currSound].play();
}
currSound = (currSound + 1) % maxSize;
};
}
var laserSound = new SoundPool("sound/effects/laser.wav", 0.5, 300);
laserSound.play();

Related

pointerEvents = "none" not working as expected

I'm creating a memory card game and it works until I try to click on the cards too fast. When I open two cards, I am calling compareCards function which adds document.body.style.pointerEvents = "none"; but obviously I can click on the third card if I am fast enough. How can I fix it? Here is my full JS code, note that class .flip adds pointer-events: none; among other things while .match adds short animation. I guess it probably has something to do with setTimeouts but I need them in order to show animatioins.
const playBtn = document.querySelector(".intro button");
const restartBtn = document.querySelectorAll(".restartBtn");
const introScreen = document.querySelector(".intro");
const game = document.querySelector(".game");
const gameContainer = document.querySelector("#gameContainer");
const timer = document.querySelector(".timer span");
const moves = document.querySelector(".moves span");
let time,
minutes = 0,
seconds = 0;
let numberOfMoves = 0;
moves.innerHTML = numberOfMoves;
let openCards = [];
let matchedCards = [];
function startGame() {
let shuffledDeck = shuffle(deckCards);
for (let i = 0; i < shuffledDeck.length; i++) {
const card = document.createElement("div");
card.classList.add("card");
const image = document.createElement("img");
image.setAttribute("src", "img/" + shuffledDeck[i]);
card.appendChild(image);
gameContainer.appendChild(card);
}
runTimer();
}
const deckCards = [
... images to add to game ...];
gameContainer.addEventListener("click", function (e) {
if (e.target.className === "card") {
flipCard();
}
function flipCard() {
e.target.classList.add("flip");
addCard();
}
function addCard() {
if (openCards.length == 0 || openCards.length == 1) {
openCards.push(e.target.firstElementChild);
}
compareCards();
}
});
function compareCards() {
if (openCards.length == 2) {
document.body.style.pointerEvents = "none";
}
if (openCards[0].src == openCards[1].src && openCards.length == 2) {
cardsMatched();
} else if (openCards[0].src !== openCards[1].src && openCards.length == 2) {
cardsNotMatched();
}
}
function countMoves() {
numberOfMoves++;
moves.innerHTML = numberOfMoves;
}
function cardsMatched() {
setTimeout(function () {
openCards[0].parentElement.classList.add("match");
openCards[1].parentElement.classList.add("match");
matchedCards.push(...openCards);
document.body.style.pointerEvents = "auto";
gameWon();
openCards = [];
}, 500);
countMoves();
}
function cardsNotMatched() {
setTimeout(function () {
openCards[0].parentElement.classList.remove("flip");
openCards[1].parentElement.classList.remove("flip");
document.body.style.pointerEvents = "auto";
openCards = [];
}, 500);
countMoves();
}
function gameWon() {
if (matchedCards.length == 16) {
stopTimer();
showModal();
}
}
const modal = document.querySelector(".modal");
function showModal() {
const closeModal = document.querySelector(".closeBtn");
modal.style.display = "block";
closeModal.addEventListener("click", () => {
modal.style.display = "none";
});
window.onclick = function (event) {
if (event.target == modal) {
modal.style.display = "none";
}
};
}
function resetEverything() {
modal.style.display = "none";
stopTimer();
timer.innerHTML = `00:00`;
numberOfMoves = 0;
moves.innerHTML = numberOfMoves;
matchedCards = [];
openCards = [];
startGame();
}
function shuffle(array) {
let currentIndex = array.length,
randomIndex;
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
function runTimer() {
time = setInterval(() => {
seconds++;
if (seconds == 60) {
minutes++;
seconds = 0;
}
timer.innerHTML = `${minutes < 10 ? `0${minutes}` : minutes}:${
seconds < 10 ? `0${seconds}` : seconds
}`;
}, 1000);
}
function stopTimer() {
seconds = 0;
minutes = 0;
clearInterval(time);
}
playBtn.addEventListener("click", () => {
introScreen.classList.remove("fadeIn");
introScreen.classList.add("fadeOut");
game.classList.add("fadeIn");
startGame();
});
What you're running into is a common problem where it takes time for things like CSS to propagate through the website, especially when you have timeouts. What you should do is VERY RARELY rely on CSS classes for your logic and let CSS be what it's meant to be: a purely visual medium.
The solution is simple. Instead of relying on pointer events, set up a flag that toggles whether the user is allowed to click on something or not, and then reference that flag for all your logic. This allows you to decouple what you see from what's happening underneath the hood. Something like this (only the relevant bits):
let canAction = true; // add a flag
function startGame() {
// ... start game logic
canAction = true; // set the flag to allow actions
}
gameContainer.addEventListener("click", function (e) {
if (canAction) {
// ... card click logic
}
});
function compareCards() {
if (openCards.length == 2) {
canAction = false; // stop user from taking further action
}
// ... rest of compare card logic
}
function cardsMatched() {
canAction = true; // re allow the user to click on cards - put this inside the setTimeout or outside depending on what you need
setTimeout(function () {
// ...
}, 500);
countMoves();
}
function cardsNotMatched() {
canAction = true; // re allow the user to click on cards - put this inside the setTimeout or outside depending on what you need
setTimeout(function () {
// ...
}, 500);
countMoves();
}
Of course you are free to keep the pointer-events CSS stuff as well, but don't rely on it for your logic. You'll just be inviting a whole lot of messy situations like this.

audio.play() works in Chrome but not Safari

I know there are questions similar to this on this site that have been answered, but I haven't found any that fix this issue which is why I'm making a post.
Here is what I have tried:
Listening for events "canplay", "loadedmetadata", "canplaythrough"
Doing audio.load() on initialization
Setting audio.preload to "auto"
Testing on Chrome (it works perfectly in this browser!)
Appending the audio element to document.body on initialization
When I try to play() without the eventListener (which also doesn't work), the console says: Unhandled Promise Rejection: NotSupportedError: The operation is not supported.
Am I missing something?
The code:
var AUDIO = {
create: function(name, file, type) {
AUDIO.library[name] = {};
AUDIO.library[name].type = type;
AUDIO.library[name].audio = new Audio();
AUDIO.library[name].audio.preload = "auto";
if (type=="music") {
AUDIO.library[name].audio.loop = true;
}
AUDIO.library[name].audio.src = file;
AUDIO.library[name].audio.load();
},
play: function(name, fadeout_playing) {
if (fadeout_playing) {
for (let i=0; i<AUDIO.playing.length; i++) {
let n = AUDIO.playing[i];
AUDIO.fadeout(n);
}
}
let a = AUDIO.library[name].audio;
a.currentTime = 0;
a.addEventListener("canplaythrough", function() {
console.log("you are listening to: "+ name);
AUDIO.playing.push(name);
a.play();
}, false);
},
fadeout: function(name) {
var sound = AUDIO.library[name].audio;
var fade = setInterval(function() {
if (sound.volume >= 0.1) {
sound.volume -= 0.1;
sound.volume = sound.volume.toFixed(1);
}
if (sound.volume <= 0) {
sound.volume = 0;
let index = AUDIO.playing.indexOf(name);
AUDIO.playing.splice(index, 1);
clearInterval(fade);
}
}, 200);
},
library: {},
playing: [],
};
AUDIO.create("act1", "music/chillelectric.mp3", "music");
//this runs when i click a button
function setup() {
...
AUDIO.play("act1");
}
Thanks in advance.

How do you play identical audio twice in Javascript?

I'm building a DTMF dial emulator. After a certain function runs, the function dial() is executed. Currently, I hear the ringback tone once but I cannot get it to ring twice.
I originally tried to get the same file to play twice by resetting the audio currentTime to 0 and playing it again after a delay of 4 seconds (the duration between ringback tones in North America).
That didn't work so I thought perhaps JS didn't want me playing the same audio twice. So I recreated it again as a second variable and created a new function for that next in the sequence for dial().
That didn't work, but if I added an alert box before ringingTone2 played, it played as soon as I dismissed the alert.
Obviously, I can't have an alert dialog when this is done. How can I have the sound play twice, but with a 4 second gap in between? I've had no success with setTimeout(ring(), 4000) either.
Here is some of the code pertaining to this issue:
var ringingTone1 = new Audio('DTMF-ringbackTone.mp3');
var ringingTone2 = new Audio('DTMF-ringbackTone.mp3');
function dial() {
ring();
function ring() {
ringingTone1.play();
setTimeout(ringingTone2.play(),4000);
}
Right now, JS basically plays the ringback tone once and moves onto to what is after this in the script.
The following causes it to stop working:
function dial() {
ring();
function ring() {
var played = 0;
var maxPlay = 2;
var ringingTone = document.getElementById('music');
var playBtn = document.getElementById('playbtn');
ringingTone.onplay = ring() {
//played counter
played++;
};
ringingTone.addEventListener("ended", ring() {
//reset to start point
ringingTone.currentTime = 0;
if (played < maxPlay) {
ringingTone.play();
} else {
played = 0;
}
});
playBtn.addEventListener("click", ring() {
ringingTone.play();
});
}
This is my complete script:
var availableNumbers = ["0", "911"];
function numberSuggestion() {
var randomNumber = Math.random() * availableNumbers.length -1;
var suggestedNumber = availableNumbers[randomNumber];
document.getElementById("suggestion").innerHTML = "How about dialing " + suggestedNumber + "? Don't like this number? Click the button above again!";
}
var dialTone;
function offHook() {
document.getElementById("WE2500").style.display = "none";
document.getElementById("dialPad").style.display = "block";
dialTone = new Audio('dialTone.m4a');
dialTone.play();
}
var number = "";
var timeout;
function numberDial() {
if (dialTone) {
dialTone.pause();
dialTone.currentTime = 0;
}
clearTimeout(timeout);
timeout = setTimeout(dial, 2000);
}
function dial1() {
numberDial();
number = number + "1";
var tone1 = new Audio('DTMF-1.wav');
tone1.play();
}
function dial2() {
numberDial();
number = number + "2";
var tone2 = new Audio('DTMF-2.wav');
tone2.play();
}
function dial3() {
numberDial();
number = number + "3";
var tone3 = new Audio('DTMF-3.wav');
tone3.play();
}
function dial4() {
numberDial();
number = number + "4";
var tone4 = new Audio('DTMF-5.wav');
tone4.play();
}
function dial5() {
numberDial();
number = number + "5";
var tone5 = new Audio('DTMF-5.wav');
tone5.play();
}
function dial6() {
numberDial();
number = number + "6";
var tone6 = new Audio('DTMF-6.wav');
tone6.play();
}
function dial7() {
numberDial();
number = number + "7";
var tone7 = new Audio('DTMF-7.wav');
tone7.play();
}
function dial8() {
numberDial();
number = number + "8";
var tone8 = new Audio('DTMF-8.wav');
tone8.play();
}
function dial9() {
numberDial();
number = number + "9";
var tone9 = new Audio('DTMF-9.wav');
tone9.play();
}
function dial0() {
numberDial();
number = number + "0";
var tone0 = new Audio('DTMF-0.wav');
tone0.play();
}
function dialStar() {
numberDial();
number = number + "*";
var toneStar = new Audio('DTMF-star.wav');
toneStar.play();
}
function dialPound() {
numberDial();
number = number + "#";
var tonePound = new Audio('DTMF-pound.wav');
tonePound.play();
}
//var ringingTone1 = new Audio('DTMF-ringbackTone.mp3');
//var ringingTone2 = new Audio('DTMF-ringbackTone.mp3');
function dial() {
ring();
function ring() {
var played = 0;
var maxPlay = 2;
var ringingTone = document.getElementById('music');
var playBtn = document.getElementById('playbtn');
ringingTone.onplay = ring() {
//played counter
played++;
};
ringingTone.addEventListener("ended", ring() {
//reset to start point
ringingTone.currentTime = 0;
if (played < maxPlay) {
ringingTone.play();
} else {
played = 0;
}
});
playBtn.addEventListener("click", ring() {
ringingTone.play();
});
}
switch(number) {
case "0":
break;
case "911":
var pickup911 = new Audio('911-xxx-fleet.mp3');
pickup911.play();
break;
default:
}
}
The idea is simple, one counter for played time, one constant for max play.
Use the onplay event to increase the counter everytime a audio played.
Use the ended event to replay the audio if max play time not reached yet. Otherwise, set played count to 0.
var played = 0;
var maxPlay = 2;
var ringingTone = document.getElementById('music');
var playBtn = document.getElementById('playbtn');
ringingTone.onplay = function() {
//played counter
played++;
};
ringingTone.addEventListener("ended", function() {
//reset to start point
ringingTone.currentTime = 0;
if (played < maxPlay) {
ringingTone.play();
} else {
played = 0;
}
});
playBtn.addEventListener("click", function() {
ringingTone.play();
});
<audio id="music" src="http://www.noiseaddicts.com/samples_1w72b820/3732.mp3"></audio>
<button id="playbtn">Play me</button>
Calling a function in setTimeout (and I think it applies to all callbacks) using () will cause the code inside to be evaluated immediately (which in your case meant play both audios at the same time).
Hence my comment above to use .play without (), which looks like doesn't work. However, this does work:
function dial() {
function ring() {
ringingTone.play();
}
ring();
setTimeout(ring, 4000);
}
dial();
http://jsfiddle.net/tL3monyp/
(audio src unscrupulously copied from Daniel's answer ;-)
I've also added 2 timeouts to illustrate calling the function with and without ().

Update progressbar in each loop

I have a progress bar that I update in a loop of many iterations.
https://jsfiddle.net/k29qy0do/32/
(open the console before you click the start button)
var progressbar = {};
$(function () {
progressbar = {
/** initial progress */
progress: 0,
/** maximum width of progressbar */
progress_max: 0,
/** The inner element of the progressbar (filled box). */
$progress_bar: $('#progressbar'),
/** Set the progressbar */
set: function (num) {
if (this.progress_max && num) {
this.progress = num / this.progress_max * 100;
console.log('percent: ' + this.progress + '% - ' + num + '/' + this.progress_max);
this.$progress_bar.width(String(this.progress) + '%');
}
},
fn_wrap: function (num) {
setTimeout(function() {
this.set(num);
}, 0);
}
};
});
$('#start_button').on('click', function () {
var iterations = 1000000000;
progressbar.progress_max = iterations;
var loop = function () {
for (var i = 1; i <= iterations; i++) {
if (iterations % i === 100) {
progressbar.set(i); //only updates the progressbar in the last iteration
//progressbar.fn_wrap(i); //even worse, since no output to the console is produced
}
}
}
//setTimeout(loop, 0);
loop();
});
The console is updated iteratively as expected.
However, the progressbar is not updating.
The problem is that the browser window seems to 'hang' until the loop finishes.
Only the console is updated, not the progressbar.
I have tried to add the setTimeout, as suggested below, in several places.
But that just makes things worse, because I then do not even get the console to output the progress while executing the loop.
Okay, I found a solution in the answer to this question:
Javascript: How to update a progress bar in a 'for' loop
var i = 0;
(function loop() {
i++;
if (iterations % i === 100) {
progressbar.set(i); //updates the progressbar, even in loop
}
if (i < iterations) {
setTimeout(loop, 0);
}
})();
My solution:
https://jsfiddle.net/ccvs4rer/3/
Lets break this down to steps
Step 1: Clean up HTML
Assuming the purpose of your question is to understand how to work the progress bar and not the styles or the labels (loading, please be patient, etc.). Lets just have the progress bar and the start button.
<div id='progressbar-outer' style="">
<div id='progressbar' style=""></div>
</div>
<button id="start_button">Start</button>
Step 2: The Styles
Lets make the progress bar visible to the user
#progressbar-outer {
height:2em;
border:5px solid #000;
width:15em;
}
#progressbar {
width:0%;
background-color:#F00;
height:100%;
}
Step 3: Using setTimeout where it belongs
In your code, you have used setTimeout to set the value of your progress bar. However, the for loop is still active.
for (var i = 1; i <= iterations; i++) {
if (iterations % i === 100) {
progressbar.set(i); //only updates the progressbar in the last iteration
//progressbar.fn_wrap(i); //even worse, since no output to the console is produced
//setTimeout(function() {
// progressbar.set(i);
//}, 0);
}
}
The use of setTimeout does not affect the rest of the code. Hence, the UI was held hostage till the loop ended. Try the following code.
$('#start_button').on('click', function () {
var iterations = 100;
progressbar.progress_max = iterations;
var loop = function (value) {
progressbar.set(value);
if (value < iterations) setTimeout(function () {
loop(value + 1)
}, 30);
else $('#progressbar').css('background-color', '#0F0');
}
loop(1);
});
Preview
Try this fiddle: https://jsfiddle.net/Ljc3b6rn/4/
What you really want is an Asynchronous loop to allow the browser to update the DOM in between iterations.
JSFiddle: http://jsfiddle.net/u5b6gr1w/
function delayedLoop(collection, delay, callback, context) {
context = context || null;
var i = 0,
nextInteration = function() {
if (i === collection.length) {
return;
}
callback.call(context, collection[i], i);
i++;
setTimeout(nextInteration, delay);
};
nextInteration();
}
Some HTML:
<div class="progress-bar"><div style="width: 0"></div></div>
A splash of CSS:
.progress-bar {
border: 1px solid black;
background-color: #f0f0f0;
}
.progress-bar div {
background-color: red;
height: 1.25em;
}
And some JavaScript to wire things together:
var progressBar = document.querySelector(".progress-bar div"),
items = [1,2,3,4,5,6,7,8,9,10];
delayedLoop(items, 500, function(item, index) {
var width = (item / items.length * 100) + "%";
progressBar.style.width = width;
progressBar.innerHTML = width;
});
My guess would be that all your progress updates are running in the same call stack. While JavaScript code is running, the DOM cannot update. Maybe this question will help you come up with a work-around.
What do you wnat to do? Why do you need it? You should only use a progressbar when you have to wait for something to finish. But we don't know what you do on your page.
If you want to display the progress of an ajax upload:
$.ajax({
...
xhr: function() {
var xhr = $.ajaxSettings.xhr();
$(xhr.upload).bind("progress", function(event) {
var e = event.originalEvent;
var percent = 0;
if (e.lengthComputable)
percent = Math.ceil(e.loaded/e.total*100);
$("#progress").width(percent+"%");
});
return xhr;
}
...
});
For images, you need an ajax call:
$.ajax({
method: "GET",
url: "http://example.com/path/image.jpg",
xhr: function() {/* see the code above*/ }
...
});
For getting the content of an uploaded file:
var reader = new FileReader();
reader.readAsText(uploadedFile);
$(reader).bind("progress", function(e) {
var percent = 0;
if (e.lengthComputable)
percent = Math.ceil(e.loaded/e.total*100);
$("#progress").css("width", percent+"%");
});
For large around of process, like math or appending a lot of divs that will take 10+ secons:
Main.js:
var worker = new Worker("Worker.js");
$(worker).bind("message", function(data) {
$("#progress").width((data*100)+"%");
});
Worker.js:
var total = 43483,
finished = 0,
doStuff = function() {
++finished;
return 1+1;
};
setInterval(function()
{
self.postMessage(finished/total);
}, 100);
for (var i = 0; i < total; ++i)
setTimeout(doStuff, i*10);
Because it's nice, and you want to tell the user there's a progress when there isn't, just animate the div:
$("#progress").animate({width: "100%"}, 3000);
You can use promises to wait until the width is set before continuing the loop.
Updating the progress bar for 1000000000 iterations will be slow if you go 1 by 1, so you might find it useful to decrease the update frequency.
Instead of a for loop, I used a recursive function that loops when the promise has been fulfilled.
set: function (num) {
var deferred = $.Deferred();
if (this.progress_max && num) {
this.progress = num / this.progress_max * 100;
var self = this;
self.$progress_bar.animate({"width": String(this.progress) + '%'}, "fast", function() {
deferred.resolve();
});
return deferred;
}
}
$('#start_button').on('click', function () {
var iterations = 1000000000;
var i = 0;
progressbar.progress_max = iterations;
var loop = function(){
i+=100000000;
if(i <= iterations){
progressbar.set(i).then(function(){
loop();
}); ;
}
};
loop();
});
https://jsfiddle.net/k29qy0do/34/
You have to use window.requestAnimationFrame, otherwise the browser will block until your loop is finished. The callback passed to requestAnimationFrame will get a timestamp as a parameter which you might be able to use for calculations of the progress.
This are my 2 takes on the question:
Using a web worker. The webworker blob code comes from here
Web worker code:
<script type="text/ww">
function loop(e) {
var data = JSON.parse(e.data);
var i = parseInt(data.i, 10);
var iterations = parseInt(data.iterations, 10);
while (iterations % ++i !== 100 && i <= iterations);
if(i <= iterations) {
self.postMessage(JSON.stringify({ i: i, iterations: iterations }));
}
}
self.onmessage = function(e) {
loop(e);
};
</script>
The code:
var ww = document.querySelector('script[type="text/ww"]'),
code = ww.textContent,
blob = new Blob([code], {type: 'text/javascript'}),
blobUrl = URL.createObjectURL(blob),
worker = new Worker(blobUrl);
worker.onmessage = function(e) {
var data = JSON.parse(e.data);
var i = parseInt(data.i, 10);
var iterations = parseInt(data.iterations, 10);
progressbar.set(i);
worker.postMessage(JSON.stringify({ i: i, iterations: iterations }));
}
$('#start_button').on('click', function () {
var iterations = 1000000000;
progressbar.progress_max = iterations;
worker.postMessage(JSON.stringify({ i: 0, iterations: iterations }));
});
The other idea hangs the UI thread, but changes the width visually, as I use requestAnimationFrame to break the counting, change width of the progressbar, and then continue the count.
function loopFrame(i, iterations) {
requestAnimationFrame(function() {
if (iterations % i === 100) {
progressbar.set(i);
}
if(i < iterations) {
loopFrame(i + 1, iterations);
}
});
}
$('#start_button').on('click', function () {
var iterations = 1000000000;
console.log(iterations);
progressbar.progress_max = iterations;
loopFrame(0, iterations);
});
Maybe this will be usefull.
var service = new Object();
//function with interrupt for show progress of operations
service.progressWhile = new Object();
service.progressWhile.dTime = 50; //step ms between callback display function
service.progressWhile.i = 0; //index
service.progressWhile.timer = 0; //start time for cycle
//#parametr arr - array for actions
//#parametr actionCallback - The function for processing array's elements
//#parametr progressCallback - function to display the array index
function progressWhile(arr, actionCallback, progressCallback) {
try {
var d = new Date();
service.progressWhile.timer = d.getTime();
log(service.progressWhile.i);
if (service.progressWhile.i >= arr.length) {
service.progressWhile.i = 0;
return;
}
while (service.progressWhile.i < arr.length) {
actionCallback(arr[service.progressWhile.i++]);
d = new Date();
if (d.getTime() - service.progressWhile.timer > service.progressWhile.dTime) {
break;
}
}
if (progressCallback != undefined)
progressCallback(service.progressWhile.i);
} catch (er) {
log(er);
return;
}
setTimeout(function () {
progressWhile(arr, actionCallback, progressCallback);
}, 0);
}
Here's updated fiddle
I used animate to make it a progress bar like look and feel.
Hope this will help you.
var progressbar = {};
$(function() {
progressbar = {
/** initial progress */
progress : 0,
/** maximum width of progressbar */
progress_max : 0,
/** The inner element of the progressbar (filled box). */
$progress_bar : $('#progressbar'),
/** Method to set the progressbar.*/
set : function(num) {
if (this.progress_max && num) {
this.progress = num / this.progress_max * 100;
console.log('percent: ' + this.progress + '% - ' + num + '/' + this.progress_max);
$('#progressbar').animate({
width : String(this.progress) + '%',
}, 500, function() {
// Animation complete.
});
}
},
fn_wrap : function(num) {
setTimeout(function() {
this.set(num);
}, 0);
}
};
});
$('#start_button').on('click', function() {
$('#progressbar').css('width', '0%');
var iterations = 1000000000;
progressbar.progress_max = iterations;
var loop = function() {
for (var i = 1; i <= iterations; i++) {
if (iterations % i === 100) {
progressbar.set(i);
//only updates the progressbar in the last iteration
}
}
}
loop();
});
Fiddler
[1]: https://jsfiddle.net/k29qy0do/21/

recorderJS record/download audio buffer WEB AUDIO API

I want to record the audio output from a simple drum sequencer and export it for download as a wav file. I have a live link to my current attempt at this implementation attempt.
The sum output of the sequencer is routed to the variable finalMixNode
yet setting this as the input for the recorder.js doesn't work. I think it may be a problem with the audio context but I can't figure it out. I successfully created a oscillator and recorded its output but I can't extend this to the sequencer.
Here is the main js code in which I am trying to record the output. I'm hoping someone will see what I am missing.
//audio node variables
var context;
var convolver;
var compressor;
var masterGainNode;
var effectLevelNode;
var lowPassFilterNode;
var noteTime;
var startTime;
var lastDrawTime = -1;
var LOOP_LENGTH = 16;
var rhythmIndex = 0;
var timeoutId;
var testBuffer = null;
var currentKit = null;
var reverbImpulseResponse = null;
var tempo = 120;
var TEMPO_MAX = 200;
var TEMPO_MIN = 40;
var TEMPO_STEP = 4;
if (window.hasOwnProperty('AudioContext') && !window.hasOwnProperty('webkitAudioContext')) {
window.webkitAudioContext = AudioContext;
}
$(function() {
init();
toggleSelectedListener();
playPauseListener();
lowPassFilterListener();
reverbListener();
createLowPassFilterSliders();
initializeTempo();
changeTempoListener();
});
function createLowPassFilterSliders() {
$("#freq-slider").slider({
value: 1,
min: 0,
max: 1,
step: 0.01,
disabled: true,
slide: changeFrequency
});
$("#quality-slider").slider({
value: 0,
min: 0,
max: 1,
step: 0.01,
disabled: true,
slide: changeQuality
});
}
function lowPassFilterListener() {
$('#lpf').click(function() {
$(this).toggleClass("active");
$(this).blur();
if ($(this).hasClass("btn-default")) {
$(this).removeClass("btn-default");
$(this).addClass("btn-warning");
lowPassFilterNode.active = true;
$("#freq-slider,#quality-slider").slider( "option", "disabled", false );
}
else {
$(this).addClass("btn-default");
$(this).removeClass("btn-warning");
lowPassFilterNode.active = false;
$("#freq-slider,#quality-slider").slider( "option", "disabled", true );
}
})
}
function reverbListener() {
$("#reverb").click(function() {
$(this).toggleClass("active");
$(this).blur();
if ($(this).hasClass("btn-default")) {
$(this).removeClass("btn-default");
$(this).addClass("btn-warning");
convolver.active = true;
}
else {
$(this).addClass("btn-default");
$(this).removeClass("btn-warning");
convolver.active = false;
}
})
}
function changeFrequency(event, ui) {
var minValue = 40;
var maxValue = context.sampleRate / 2;
var numberOfOctaves = Math.log(maxValue / minValue) / Math.LN2;
var multiplier = Math.pow(2, numberOfOctaves * (ui.value - 1.0));
lowPassFilterNode.frequency.value = maxValue * multiplier;
}
function changeQuality(event, ui) {
//30 is the quality multiplier, for now.
lowPassFilterNode.Q.value = ui.value * 30;
}
function playPauseListener() {
$('#play-pause').click(function() {
var $span = $(this).children("span");
if($span.hasClass('glyphicon-play')) {
$span.removeClass('glyphicon-play');
$span.addClass('glyphicon-pause');
handlePlay();
}
else {
$span.addClass('glyphicon-play');
$span.removeClass('glyphicon-pause');
handleStop();
}
});
}
function toggleSelectedListener() {
$('.pad').click(function() {
$(this).toggleClass("selected");
});
}
function init() {
initializeAudioNodes();
loadKits();
loadImpulseResponses();
}
function initializeAudioNodes() {
context = new webkitAudioContext();
var finalMixNode;
if (context.createDynamicsCompressor) {
// Create a dynamics compressor to sweeten the overall mix.
compressor = context.createDynamicsCompressor();
compressor.connect(context.destination);
finalMixNode = compressor;
} else {
// No compressor available in this implementation.
finalMixNode = context.destination;
}
// Create master volume.
// for now, the master volume is static, but in the future there will be a slider
masterGainNode = context.createGain();
masterGainNode.gain.value = 0.7; // reduce overall volume to avoid clipping
masterGainNode.connect(finalMixNode);
//connect all sounds to masterGainNode to play them
//don't need this for now, no wet dry mix for effects
// // Create effect volume.
// effectLevelNode = context.createGain();
// effectLevelNode.gain.value = 1.0; // effect level slider controls this
// effectLevelNode.connect(masterGainNode);
// Create convolver for effect
convolver = context.createConvolver();
convolver.active = false;
// convolver.connect(effectLevelNode);
//Create Low Pass Filter
lowPassFilterNode = context.createBiquadFilter();
//this is for backwards compatibility, the type used to be an integer
lowPassFilterNode.type = (typeof lowPassFilterNode.type === 'string') ? 'lowpass' : 0; // LOWPASS
//default value is max cutoff, or passing all frequencies
lowPassFilterNode.frequency.value = context.sampleRate / 2;
lowPassFilterNode.connect(masterGainNode);
lowPassFilterNode.active = false;
}
function loadKits() {
//name must be same as path
var kit = new Kit("TR808");
kit.load();
//TODO: figure out how to test if a kit is loaded
currentKit = kit;
}
function loadImpulseResponses() {
reverbImpulseResponse = new ImpulseResponse("sounds/impulse- responses/matrix-reverb2.wav");
reverbImpulseResponse.load();
}
//TODO delete this
function loadTestBuffer() {
var request = new XMLHttpRequest();
var url = "http://www.freesound.org/data/previews/102/102130_1721044-lq.mp3";
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = function() {
context.decodeAudioData(
request.response,
function(buffer) {
testBuffer = buffer;
},
function(buffer) {
console.log("Error decoding drum samples!");
}
);
}
request.send();
}
//TODO delete this
function sequencePads() {
$('.pad.selected').each(function() {
$('.pad').removeClass("selected");
$(this).addClass("selected");
});
}
function playNote(buffer, noteTime) {
var voice = context.createBufferSource();
voice.buffer = buffer;
var currentLastNode = masterGainNode;
if (lowPassFilterNode.active) {
lowPassFilterNode.connect(currentLastNode);
currentLastNode = lowPassFilterNode;
}
if (convolver.active) {
convolver.buffer = reverbImpulseResponse.buffer;
convolver.connect(currentLastNode);
currentLastNode = convolver;
}
voice.connect(currentLastNode);
voice.start(noteTime);
}
function schedule() {
var currentTime = context.currentTime;
// The sequence starts at startTime, so normalize currentTime so that it's 0 at the start of the sequence.
currentTime -= startTime;
while (noteTime < currentTime + 0.200) {
var contextPlayTime = noteTime + startTime;
var $currentPads = $(".column_" + rhythmIndex);
$currentPads.each(function() {
if ($(this).hasClass("selected")) {
var instrumentName = $(this).parents().data("instrument");
switch (instrumentName) {
case "kick":
playNote(currentKit.kickBuffer, contextPlayTime);
break;
case "snare":
playNote(currentKit.snareBuffer, contextPlayTime);
break;
case "hihat":
playNote(currentKit.hihatBuffer, contextPlayTime);
break;
case "tomhi":
playNote(currentKit.tomhiBuffer, contextPlayTime);
break;
case "tommid":
playNote(currentKit.tommidBuffer, contextPlayTime);
break;
case "tomlow":
playNote(currentKit.tomlowBuffer, contextPlayTime);
break;
case "cl":
playNote(currentKit.clBuffer, contextPlayTime);
break;
case "cb":
playNote(currentKit.cbBuffer, contextPlayTime);
break;
case "cp":
playNote(currentKit.cpBuffer, contextPlayTime);
break;
case "cy":
playNote(currentKit.cyBuffer, contextPlayTime);
break;
case "rs":
playNote(currentKit.rsBuffer, contextPlayTime);
break;
}
//play the buffer
//store a data element in the row that tells you what instrument
}
});
if (noteTime != lastDrawTime) {
lastDrawTime = noteTime;
drawPlayhead(rhythmIndex);
}
advanceNote();
}
timeoutId = requestAnimationFrame(schedule)
}
function drawPlayhead(xindex) {
var lastIndex = (xindex + LOOP_LENGTH - 1) % LOOP_LENGTH;
//can change this to class selector to select a column
var $newRows = $('.column_' + xindex);
var $oldRows = $('.column_' + lastIndex);
$newRows.addClass("playing");
$oldRows.removeClass("playing");
}
function advanceNote() {
// Advance time by a 16th note...
// var secondsPerBeat = 60.0 / theBeat.tempo;
//TODO CHANGE TEMPO HERE, convert to float
tempo = Number($("#tempo-input").val());
var secondsPerBeat = 60.0 / tempo;
rhythmIndex++;
if (rhythmIndex == LOOP_LENGTH) {
rhythmIndex = 0;
}
//0.25 because each square is a 16th note
noteTime += 0.25 * secondsPerBeat
// if (rhythmIndex % 2) {
// noteTime += (0.25 + kMaxSwing * theBeat.swingFactor) * secondsPerBeat;
// } else {
// noteTime += (0.25 - kMaxSwing * theBeat.swingFactor) * secondsPerBeat;
// }
}
function handlePlay(event) {
rhythmIndex = 0;
noteTime = 0.0;
startTime = context.currentTime + 0.005;
schedule();
}
function handleStop(event) {
cancelAnimationFrame(timeoutId);
$(".pad").removeClass("playing");
}
function initializeTempo() {
$("#tempo-input").val(tempo);
}
function changeTempoListener() {
$("#increase-tempo").click(function() {
if (tempo < TEMPO_MAX) {
tempo += TEMPO_STEP;
$("#tempo-input").val(tempo);
}
});
$("#decrease-tempo").click(function() {
if (tempo > TEMPO_MIN) {
tempo -= TEMPO_STEP;
$("#tempo-input").val(tempo);
}
});
}
function __log(e, data) {
log.innerHTML += "\n" + e + " " + (data || '');
}
var audio_context;
var recorder;
function startUserMedia() {
var input = finalMixNode;
__log('Media stream created.');
input.start();
__log('Input connected to audio context destination.');
recorder = new Recorder(input);
__log('Recorder initialised.');
}
function startRecording(button) {
recorder && recorder.record();
button.disabled = true;
button.nextElementSibling.disabled = false;
__log('Recording...');
}
function stopRecording(button) {
recorder && recorder.stop();
button.disabled = true;
button.previousElementSibling.disabled = false;
__log('Stopped recording.');
// create WAV download link using audio data blob
createDownloadLink();
recorder.clear();
}
function createDownloadLink() {
recorder && recorder.exportWAV(function(blob) {
var url = URL.createObjectURL(blob);
var li = document.createElement('li');
var au = document.createElement('audio');
var hf = document.createElement('a');
au.controls = true;
au.src = url;
hf.href = url;
hf.download = new Date().toISOString() + '.wav';
hf.innerHTML = hf.download;
li.appendChild(au);
li.appendChild(hf);
recordingslist.appendChild(li);
});
}
window.onload = function init() {
try {
// webkit shim
window.AudioContext = window.AudioContext || window.webkitAudioContext;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
window.URL = window.URL || window.webkitURL;
// audio_context = new AudioContext;
__log('Audio context set up.');
} catch (e) {
alert('No web audio support in this browser!');
}
startUserMedia();
};
Your finalMixNode is scoped into the initializeAudioNodes() function, therefore it is undefined when you call it from startUserMedia().
Also, this variable is either a dynamicCompressor node or the AudioContext's destination.
Recorderjs needs a node with an output (which AudioDestinationNode doesn't have currently1 ) so make sure to construct your recorder with the final compressor node (or a final gainNode)
Executing this in my js console from your page does work :
var recorder = new Recorder(compressor);
1 Thanks #padenot for noticing me it's being discussed here

Categories