Occasional audio playback delay in Javascript. How to solve? - javascript

I had a few sound effects that would play as a part of my programs. This would use the simple audio.play() method in Javascript.
The problem was? Sometimes, the sound effects would be delayed BADLY relative to the user action. Like 1-3 seconds of delay time.
The file size of each audio file is very small (5KB - 50KB). So I don't think that's the issue.
I tried preloading the audio files, that didn't work.
Weirdly enough, when the sound effect would be played once, then played repeatedly after that, it would always play just fine, instantly responsive. But it's when the users would NOT trigger actions that would cause a sound effect for a long time -- or when they would switch tabs, do other stuff, then switch back -- that the sound effect would then be delayed 1-3 seconds before playing.
Here's the pertinent code:
function AudioSettings() {
SoundEffectCashRegister = new Audio("cash-register-cha-ching-sound-effect.mp3");
SoundEffectSadTrombone = new Audio("sad-trombone-sound-effect.mp3");
SoundEffectBubblePop = new Audio("quick-bubble-pop-sound-effect.mp3");
SoundEffectCashRegister.preload = 'auto';
SoundEffectSadTrombone.preload = 'auto';
SoundEffectBubblePop.preload = 'auto';
VolumeSetting = localStorage.getItem("UserSavedVolume");
document.getElementById("volume").value = (VolumeSetting * 100); // sets volume to user-selected value, on popup load
// console.log(document.getElementById("volume").value);
SoundEffectCashRegister.volume = VolumeSetting;
SoundEffectSadTrombone.volume = VolumeSetting / 1.5;
SoundEffectBubblePop.volume = VolumeSetting / 1.5;
// console.log(SoundEffectCashRegister.volume + " " + SoundEffectSadTrombone.volume + " " + SoundEffectBubblePop.volume);
volume.addEventListener("input", (e) => { // updates volume in real time, on slider adjustment -- then saves user preference to storage
SoundEffectCashRegister.volume = e.currentTarget.value / 100;
SoundEffectSadTrombone.volume = (e.currentTarget.value / 1.5) / 100;
SoundEffectBubblePop.volume = (e.currentTarget.value / 1.5) / 100;
// console.log(SoundEffectCashRegister.volume + " " + SoundEffectSadTrombone.volume + " " + SoundEffectBubblePop.volume);
localStorage.setItem("UserSavedVolume", SoundEffectCashRegister.volume);
});
}
Then each time the audio file would need to be played, I would just use the SoundEffectCashRegister.play() method, etc.
Thanks

Related

Glitches when using webAudio API with javascript for browser

I am having issues with using the webAudio API with javascript.
The problem is that I am hearing glitches on the sounds being played in my browser even though I have used a gainNode to gradually increase/decrease the sound when it starts/stops.
The audio file is simply 60 seconds of 400hz tone to demonstrate the issue. In the demo I play a snippet from time point 2.0 seconds for 1 second duration, within this duration I ramp up for 100ms and at 800ms I begin to ramp down for 199ms. This is an attempt to avoid a non zero crossing glitch. I use gainNode.gain.setTargetAtTime() but also tried exponentialRampToValueAtTime() as well. In this example I repeat at time point 52 seconds.
At the beginning of the code I impliment an audioContext.resume() to trigger the audio facility of the browser.
<!DOCTYPE html>
<html>
<head>
<title>My experiment</title>
<audio id="audio" src="pure_400Hz_tone.ogg" preload="auto"></audio>
</head>
<body>
<div id="jspsych_target"></div>
<button onclick="dummyPress()">Press to Activate Audio</button>
<button onclick="playTheTones()">sound the tone</button>
</body>
<script>
console.log("setting up audiocontext at ver 28 ");
const audioContext = new AudioContext();
const element = document.querySelector("audio");
const source = audioContext.createMediaElementSource(element);
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
source.connect(gainNode);
gainNode.connect(audioContext.destination);
function dummyPress(){
audioContext.resume();
playTheTones();
};
function playTheTones(){
// ******* The First Tone ***********
// **********************************
source.currentTime = 2;
gainNode.gain.setTargetAtTime(1.0, audioContext.currentTime, 0.1);
var g = setTimeout(function(){
gainNode.gain.setTargetAtTime(0.0001, audioContext.currentTime, 0.199);
console.log("start Down # " + source.currentTime);
},800);
source.mediaElement.play();
console.log("PLAYING 2 now # " + source.currentTime);
var k = setTimeout(function(){
source.mediaElement.pause();
console.log("STOPPED # " + source.currentTime);
},1100);
// ******* The Second Tone ***********
// **********************************
setTimeout(function(){
source.currentTime = 52;
gainNode.gain.setTargetAtTime(1.0, audioContext.currentTime, 0.1);
var h = setTimeout(function(){
gainNode.gain.setTargetAtTime(0.0001, audioContext.currentTime, 0.199);
console.log("start Down # " + source.currentTime);
},800);
source.mediaElement.play();
console.log("PLAYING 52 now # " + source.currentTime);
var j = setTimeout(function(){
source.mediaElement.pause();
console.log("STOPPED # " + source.currentTime);
},1100);
},1500);
};
</script>
</html>
Unfortunately I think I have confused myself in trying to resolve the glitch issues and may not be using best practice using the API and this might be causing my problem.
Would someone look at the code and point out if I am using the API correctly and confirm that I am correct in thinking I should be able to use the API and present tones in this way without glitching.
Thanks
I found the problem
gainNode.gain.setTargetAtTime(0.0001, audioContext.currentTime, 0.199);
the third parameter is a 'time constant' not a 'time duration' so it was mammothly large at 0.199 and the gain did not diminish rapidly enough so causing the glitch. Setting to 0.01 cures the issue !

How to show multiple images subsequently with a specific time delay in javascript

How I can make it work accurately so that each picture is exactly shown 50ms after the last one? (And so that the calculated time between seeing the picture and clicking is accurate?)
Background
I want to have a slideshow of images. The images are stored in the images directory and their names are sequential. The slideshow should start after the user clicks on the play button. And the user will click on the stop button a little after the 100th picture. And the code will show the user how many milliseconds after seeing the 100th picture they have clicked on the stop button. The problem is that when I run the code it doesn't work so accurate. It has some lag on some pictures. I was wondering
Here is my code:
<!DOCTYPE html>
<html>
<head>
<script>
var player;
var timestamp;
function preloadImage()
{
var i = 1
while(true){
var img = new Image();
img.src="images/" + i;
i = i + 1;
if (img.height == 0) { break; }
}
}
function next(){
var fullPath=document.getElementById("image").src;
var filename = fullPath.split("/").pop();
var n=parseInt(filename, 10) + 1;
document.getElementById("image").src = "images/" + n;
if (document.getElementById("image").height == 0) { clearInterval(player); }
if (n == 100) { timestamp = new Date().getTime(); }
}
function start(){
clearInterval(player);
document.getElementById("image").src = "images/1";
player = setInterval(function(){ next(); }, 50)
}
function stop(){
clearInterval(player);
alert("You clicked after " + (new Date().getTime() - timestamp) + "ms.")
}
preloadImage()
</script>
</head>
<body>
<img src="images/0" id="image"><br/>
<button type="button" onclick='start()'>start</button>
<button type="button" onclick='stop()'>stop</button>
</body>
</html>
As I stated in my comment, I believe your preloadImage() is not working as you expect. Try running the stack snippet below as a demonstration, and possibly make sure your cache is cleared:
function badPreloadImage () {
var img = new Image();
img.onload = function () {
// check it asynchronously
console.log('good', this.height);
};
img.src = 'https://www.w3schools.com/w3css/img_lights.jpg';
// don't check height synchronously
console.log('bad', img.height);
}
badPreloadImage();
To preload all your images properly, you must do so asynchronously:
function preloadImage (done, i) {
if (!i) { i = 1; }
var img = new Image();
img.onloadend = function () {
if (this.height == 0) { return done(); }
preloadImage(done, i + 1);
};
img.src = "images/" + i;
}
// usage
preloadImage(function () { console.log('images loaded'); });
Other concerns
You should consider using performance.now() instead of new Date().getTime(), as it uses a more precise time resolution (currently 20us, as pointed out in this comment).
You might also consider storing references to each image as an array of Image objects, rather than loading each frame via specifying the src property of an HTMLImageElement, so that the browser can just load the data from memory, rather than loading from cache on the hard drive, or even making a new HTTP request when caching is not enabled.
Addressing each of these issues will allow you to measure timing more precisely by using the proper API and eliminating lag spikes on the DOM thread due to inefficient animation.

Screensaver loads image in radom location after inactivity, restarts if user does anything

I have the following two pieces of code (awful but I have no idea what I'm doing):
var stage = new createjs.Stage("canvas");
createjs.Ticker.on("tick", tick);
// Simple loading for demo purposes.
var image = document.createElement("img");
image.src = "http://dossierindustries.co/wp-content/uploads/2017/07/DossierIndustries_Cactus-e1499205396119.png";
var _obstacle = new createjs.Bitmap(image);
setInterval(clone, 1000);
function clone() {
var bmp = _obstacle.clone();
bmp.x= Math.floor((Math.random() * 1920) + 1);
bmp.y = Math.floor((Math.random() * 1080) + 1);
stage.addChild(bmp);
}
function tick(event) {
stage.update(event);
}
<script>
$j=jQuery.noConflict();
jQuery(document).ready(function($){
var interval = 1;
setInterval(function(){
if(interval == 3){
$('canvas').show();
interval = 1;
}
interval = interval+1;
console.log(interval);
},1000);
$(document).bind('mousemove keypress', function() {
$('canvas').hide();
interval = 1;
});
});
<script src="https://code.createjs.com/easeljs-0.8.2.min.js"></script>
<canvas id="canvas" width="1920" height="1080"></canvas>
Basically what I'm hoping to achieve is that when a user is inactive for x amount of time the full page (no matter on size) slowly fills with the repeated image. When anything happens they all clear and it begins again after the set amount of inactivity.
The code above relies on an external resource which I'd like to avoid and needs to work on Wordpress.
Site is viewable at dossierindustries.co
Rather than interpret your code, I made a quick demo showing how I might approach this.
The big difference is that drawing new images over time is going to add up (they have to get rendered every frame), so this approach uses a cached container with one child, and each tick it just adds more to the cache (similar to the "updateCache" demo in GitHub.
Here is the fiddle.
http://jsfiddle.net/dcs5zebm/
Key pieces:
// Move the contents each tick, and update the cache
shape.x = Math.random() * stage.canvas.width;
shape.y = Math.random() * stage.canvas.height;
container.updateCache("source-over");
// Only do it when idle
function tick(event) {
if (idle) { addImage(); }
stage.update(event);
}
// Use a timeout to determine when idle. Clear it when the mouse moves.
var idle = false;
document.body.addEventListener("mousemove", resetIdle);
function resetIdle() {
clearTimeout(this.timeout);
container.visible = false;
idle = false;
this.timeout = setTimeout(goIdle, TIMEOUT);
}
resetIdle();
function goIdle() {
idle = true;
container.cache(0, 0, stage.canvas.width, stage.canvas.height);
container.visible = true;
}
Caching the container means this runs the same speed forever (no overhead), but you still have control over the rest of the stage (instead of just turning off auto-clear). If you have more complicated requirements, you can get fancier -- but this basically does what you want I think.

pixijs swapping out an image without it jumping

I am working on a html 5 javascript game. It has a heavily textured background. I am looking at having one 3d background item and swapping it out on the fly. So in this instance we see a room with a closed door - then when a js event is fired - the image is swapped out to show an open door.
I am trying to create the function and although I can swap the image - I am unable to stop it from jumping.
so a new image path comes in - I null and remove the old backdrop and replace it with the new. I have read about adding it to the texture cache - not sure how to do that? Its my first time using pixijs
GroundPlane.prototype.resetBackdrop = function (imagePath) {
if(this.backdrop) {
this.backdrop.alpha = 0;
this.removeChild(this.backdrop);
this.backdrop = null;
this.backdrop = PIXI.Sprite.fromImage(imagePath);
this.backdrop.anchor.x = .5;
this.backdrop.anchor.y = .5;/*
this.backdrop.scale.x = 1.2;
this.backdrop.scale.y = 1.2;*/
this.addChildAt(this.backdrop, 0);
this.backdrop.alpha = 1;
}
};
The reason for the "jump" is that the image being swapped in takes some time to load before it can be displayed on the screen.
To prevent this, you can load the image into the TextureCache ahead of time, so when you swap images, there won't be any delay.
//set the initial backdrop image
this.backdrop = PIXI.Sprite.fromImage("Image1.png");
this.backdrop.anchor.x = 0.5;
this.backdrop.anchor.y = 0.5;
this.backdrop.scale.x = 1.2;
this.backdrop.scale.y = 1.2;
//this will store the second image into the texture cache
PIXI.Texture.fromImage("Image2.png");
//if you need to keep track of when the image has finished loading,
//use a new PIXI.ImageLoader() instead.
GroundPlane.prototype.resetBackdrop = function (imagePath)
{
//Update the image Texture
this.backdrop.setTexture(PIXI.Texture.fromFrame(imagePath));
};

Capture group of HTML5 video screenshots

I am trying to generate a group of thumbnails in the browser out of a HTML5 video using canvas with this code:
var fps = video_model.getFps(); //frames per second, comes from another script
var start = shot.getStart(); //start time of capture, comes from another script
var end = shot.getEnd(); //end time of capture, comes from another script
for(var i = start; i <= end; i += 50){ //capture every 50 frames
video.get(0).currentTime = i / fps;
var capture = $(document.createElement("canvas"))
.attr({
id: video.get(0).currentTime + "sec",
width: video.get(0).videoWidth,
height: video.get(0).videoHeight
})
var ctx = capture.get(0).getContext("2d");
ctx.drawImage(video.get(0), 0, 0, video.get(0).videoWidth, video.get(0).videoHeight);
$("body").append(capture, " ");
}
The the amount of captures is correct, but the problem is that in Chrome all the canvases appear black and in Firefox they always show the same image.
Maybe the problem is that the loop is too fast to let the canvases be painted, but I read that .drawImage() is asynchronous, therefore, in theory, it should let the canvases be painted before jumping to the next line.
Any ideas on how to solve this issue?
Thanks.
After hours of fighting with this I finally came up with a solution based on the "seeked" event. For this to work, the video must be completely loaded:
The code goes like this:
var fps = video_model.getFps(); //screenshot data, comes from another script
var start = shot.getStart();
var end = shot.getEnd();
video.get(0).currentTime = start/fps; //make the video jump to the start
video.on("seeked", function(){ //when the time is seeked, capture screenshot
setTimeout( //the trick is in giving the canvas a little time to be created and painted, 500ms should be enough
function(){
if( video.get(0).currentTime <= end/fps ){
var capture = $(document.createElement("canvas")) //create canvas element on the fly
.attr({
id: video.get(0).currentTime + "sec",
width: video.get(0).videoWidth,
height: video.get(0).videoHeight
})
.appendTo("body");
var ctx = capture.get(0).getContext("2d"); //paint canvas
ctx.drawImage(video.get(0), 0, 0, video.get(0).videoWidth, video.get(0).videoHeight);
if(video.get(0).currentTime + 50/fps > end/fps){
video.off("seeked"); //if last screenshot was captured, unbind
}else{
video.get(0).currentTime += 50/fps; //capture every 50 frames
}
}
}
, 500); //timeout of 500ms
});
This has worked for me in Chrome and Firefox, I've read that the seeked event can be buggy in some version of particular browsers.
Hope this can be useful to anybody. If anyone comes up with a cleaner, better solution, it would be nice to see it.

Categories