Ehy everyone,
I'm developing a musical web-application using the Web Audio API, and I'm facing a problem that I cannot understand.
The app basically works like this: the user navigates through a full-screen image, on which there are some specific points associated with specific sounds. The app plays the sounds that are closer to the user, and fade them out when the user gets away.
The problem occurs (not every time) when the user is in a position where there are more than two sounds (3 or 4, but sometimes also 2 is enough) playing, and then goes in a "silent" position.
In that case the fades are somehow dropped and the sounds are abruptly interrupted. I can see from the logs that it enters the fading function, but it doesn't execute the scheduled gain curves.
To be more clear, here's an example of the steps my algorithm does:
when the user is in the position A, let's say 3 sounds are playing;
the user moves to position B and an event is fired when the action is finished;
the algorithm calculates how many sounds are associated to position B: zero;
performs a comparison with sounds in position A and calculates that sound 1, 2 an 3 are gone and they need to be stopped with a fade out;
for each sound invokes fadeOut function, which schedules fade values with WebAudioAPI methods, and schedules stop action with setTimeout();
The result is that the sound remains with maximum volume till the stop function is invoked.
Here's the fadeOut() function:
StreamSound.prototype.fadeOut = function (fadeTime) {
const self = this;
if (self.isPlaying()) {
if (!fadeTime) {
console.warn(`Missing argument fadeTime. Set to 1 second`);
fadeTime = 1;
}
var currGain = self.masterGain.gain.value;
var curTime = self.context.currentTime;
self.masterGain.gain.cancelScheduledValues(curTime);
self.masterGain.gain.setValueAtTime(currGain, curTime);
self.masterGain.gain.linearRampToValueAtTime(0.0001, curTime + fadeTime);
self.fading = true;
setTimeout(function () {
// If has not secondary streams
if (self.secondaryStream === undefined) {
console.log(`No secondary streams -> stop`);
self.pause();
} else if (!self.secondaryStream.isPlaying()) {
console.log(`Not playing secondary streams -> stop`);
self.pause();
} else {
self.playing = false;
}
self.fading = false;
console.log(`Fade out stream finished [ ID: ${self.index} ]`);
}, fadeTime * 1000 + 25);
console.log(`Fading out stream [ ID: ${self.index} ]`);
} else {
console.warn(`Cannot fade out. Sound not playing [ ID: ${self.index} ]`);
}
};
I suppose that probably that's due to scheduling issues, or can it be something related with JS thread management?
I know is not easy to help me just with these informations, but maybe someone had a similar problem.
Thanks in any case,
Francesco
EDIT: I opened another post with a more general description and also a link to the source code and an online beta version ->
WebAudioAPI musical application: clicks and crackles and abrupt stops
Related
I support several churches that don't have musicians, by providing a little website with a bunch of pure Javascript so they can select music for their services from a collection of about 1100 mp3 and m4a music files. Previously, they created playlists in iTunes or Media Player, but after a track completed, the player would immediately start the next track unless they quickly clicked 'Stop'. So my website allows them to select all their music ahead of time (up to 10 tracks), with a separate "Play" button for each. Hit "Play" and it plays that one track and stops. (Duh.)
I'm encountering delays in loading the files into my "audio" tags - and I need the file to load when they select it so I can display the track duration, which is frequently important to the selection of the music for the service. A delay doesn't occur very often, but often enough to be annoying. Also, the load will occasionally time out completely, even after several attempts. I've experimented played with various techniques, like using setTimeout with different values to allow several seconds before checking if it's loaded, or, loading 5 or 10 times with shorter timeout values until it's loaded. I created a test page that indicates that the timeouts vary greatly - from 2% to 5% of the time, to upwards of 25% occasionally (during tests of 1,000 to 10,000 random loads).
My first technique was relying on events (I tried both 'canplay' and 'canplaythrough' events with minimal difference):
const testAudio = document.getElementById('test-audio');
let timeStart = Date.now();
function loadMusic(p_file) {
testAudio.src = p_file;
testAudio.addEventListener('canplaythrough', musicLoaded);
timeStart = Date.now();
testAudio.load();
}
function musicLoaded() {
console.log('music loaded in ' + (Date.now()-timeStart) + 'ms');
testAudio.removeEventListener('canplaythrough', musicLoaded);
/* should I add/remove the listener each time I change the source file ? */
}
My second approach (from a post here: https://stackoverflow.com/questions/10235919/the-canplay-canplaythrough-events-for-an-html5-video-are-not-called-on-firefox) is to check the 'readyState' of the audio element after a specified timeout, rather than relying on an event. This question specifically addressed Firefox, so I should mention that in my tests Firefox has horrible load times for both the "events" and the "readyState" techniques. Chrome and Edge vary in the range of 2% to 6% load failure due to timeout and Firefox has 27% to 39% load timeouts.
let myTimeout = '';
function loadMusic(p_file) {
myTimeout = setTimeout(fileTimeout, 1000); /* I've tried various values here */
testAudio.src = p_file;
timeStart = Date.now();
testAudio.load();
}
function fileTimeout() {
if (testAudio.readyState > 3) {
console.log('music loaded in ' + (Date.now()-timeStart) + 'ms');
} else {
/* here, I've tried calling loadMusic again 5 to 10 times, which sometimes works */
/* or, just reporting that the load failed... */
console.log('music FAILED to load!');
}
}
I have a shared server hosting plan, and I suspect the delay might be due to traffic on my server. Unfortunately, my hosting service turns a deaf ear to anything that might be application or content related (not surprising). And this isn't worth upgrading to a dedicated server just to eliminate that variable. But I suspect that might be a major factor here.
I need a technique that will always work - even if it takes 30 seconds or more. As long as I can display an intermittent "Still loading..." type message I (and my users) would be satisfied. The "track X won't load" messages happen often enough to be annoying. Early on, I had a few files with bad characters in the file name that needed to be fixed before they would load. So the users think that problem persists. But I know I've fixed all them now.
Any and all suggestions are welcome - but I'd love to keep everything in plain Javascript.
Using an audio constructor:
function loadMusic(p_file) {
myTimeout = setTimeout(fileTimeout, 1000);
let audioConst = new Audio();
audioConst.src = p_file;
timeStart = Date.now();
}
function fileTimeout() {
if (audioConst.readyState > 3) {
console.log('music loaded in ' + (Date.now()-timeStart) + 'ms');
} else {
console.log('music FAILED to load!');
}
myTimeout = '';
}
I'm working on a Javascript Music App that includes a Sequencer. For those who are not familiar, MIDI sequencers work pretty much like this: There is something called PPQ: pulses per quarter note. Each pulse is called "Tick". It depicts how may "subdivisions" there are per quarter note, like resolution. So Sequencers "play" the Events that are in the tracks one Tick at a time: Play Tick1, wait Tick Duration, Play tick2, Tick Duration, and so on.
Now, let's say we have a BPM (Beats per Min) of 120 with PPQ=96 (standard). That means that each Quarter Note Duration is 500ms, and each Tick Duration is 5.20833ms.
What Timer Alternatives we have in Javascript?
1) We have the old setTimeOut. It has several problems: the min. wait time is 4ms. (https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Minimum_delay_and_timeout_nesting)
It is also subject to JITTER/time Variations. It is not precise and it is demanding, as call backs are stacked in the even loop.
2) There is an alternative to setTimeOut/setInterval which involves using requestAnimationFrame(). It is VERY precise and CPU efficient. However, the minimum time it can be set is around 16.7ms (the duration of a Frame in a typical 60FPS monitor)
Is there any other Alternative? To to precisely schedule an event every 2-5ms?
Note: the function done in side the loop, playEventsAtTick() is NOT demanding at all, so it would never take more time to execute than Tick Duration.
Thanks!
Danny Bullo
To maintain any sanity in doing this kind of thing, you're going to want to do the audio processing on a devoted thread. Better yet, use the Web Audio API and let people who have been thinking about these problems for a long time do the hard work of sample-accuracy.
Also check out Web MIDI (chrome only).
Thanks nvioli. I'm aware of Web Audio API. However, I don't think that can help here.
I'm not triggering AUDIO directly: I have MIDI events (or let's say just "EVENTS") stored in the TRACKS. And those events happen at any TICK. So the Sequencer needs to loop every Tick Duration to scan what to play at that particular tick.
Regards,
Danny Bullo
In a separate thread, such as a web worker, you can create an endless loop. In this loop, all you need to do is calculate the time between beats. After the time is valid, you can then send a message to the main process, to do some visuals, play a sound or what ever you would like to do.
Here is a Working example
class MyWorker {
constructor() {
// Keeps the loop running
this.run = true
// Beats per minute
this.bpm = 120
// Time last beat was called
this.lastLoopTime = this.milliseconds
}
get milliseconds() {
return new Date().getTime()
}
start() {
while (this.run) {
// Get the current time
let now = this.milliseconds
// Get the elapsed time between now and the last beat
let updateLength = now - this.lastLoopTime
// If not enough time has passed restart from the beginning of the loop
if (updateLength < (1000 * 60) / this.bpm) continue;
// Enough time has passed update the last time
this.lastLoopTime = now
// Do any processing that you would like here
// Send a message back to the main thread
postMessage({ msg: 'beat', time: now })
}
}
}
new MyWorker().start()
Next we can create the index page, which will run the worker, and flash a square everytime a message comes back from the worker.
<!DOCTYPE html>
<html lang="en">
<head>
<script>
// Start the worker
var myWorker = new Worker('worker.js')
// Listen for messages from the worker
myWorker.onmessage = function (e) {
var msg = e.data
switch (msg.msg) {
// If the message is a `beat` message, flash the square
case 'beat':
let div = document.querySelector('div')
div.classList.add('red')
setTimeout(() => div.classList.remove('red'), 100)
break;
}
}
</script>
<style>
div { width: 100px; height: 100px; border: solid 1px; }
.red { background: red; }
</style>
</head>
<body>
<div></div>
</body>
</html>
Get Off My Lawn: The approach you suggested does not completely work. Let's say I add a method to the web worker to STOP the Sequencer:
stop() {
this.run = false;
}
The problem is that the method myWorker.onmessage = function (e) {...} never get's triggered. I suspect it is because the Web Worker Thread is "TOO BUSY" with the endless loop. any way to solve that?
Also, while playing, it works.....but the CPU goes up considerably..... The only possible Solution would be a Sleep() method, but Real SLEEP that does not exist in Javascript...
Thanks
I have a small piece of code that continuously clicks a button called "See Older Messages" every 500 ms, in order to load infinitely-scrolled content from a webpage. Reasons for doing this are personal, but needless to say, I'm trying to automate something which would take me weeks of non-stop scrolling to do otherwise.
The problem is that the 500 ms delay gradually begins to drop as the script runs over time. After so many hours, it can take 5 seconds or more. I'm assuming this problem is caused by Facebook throttling my requests after so long, so to prevent this, I want to make the script run for an amount of time - say 2 minutes - followed by a delay of maybe 20 secs before it runs again for 2 mins, and so on. How would I go about doing this? I've racked my brains, but my limited knowledge of JavaScript hasn't come up with anything meaningful.
Below is the current code in its entirety.
setInterval(function () {
document.getElementById('see_older').getElementsByClassName('content')[0].click();
}, 500);
Thanks a lot in advance.
Keep track of when the script running started
While it's been less than 2 mins, keep clicking every 500ms.
After running for ~2 mins, stop and queue next run in 20s.
Go to step 2.
-
var lastChange;
function doClick() {
if (new Date() - lastChange < 120000 /* 2 mins */) {
document.getElementById('see_older').getElementsByClassName('content')[0].click();
setTimeout(doClick, 500);
} else setTimeout(runScript, 20000 /* 20s */);
}
(function runScript() {
lastChange = new Date();
doClick();
})();
-
I recommend using setTimeout over setInterval since, if the browser takes a while to execute, loses focus and stops executing JS, gets paged out, etc., then you will still get the time spacing between events that you want. See https://stackoverflow.com/a/731625/1059070.
Toggle whether or not your function does anything by setting another timer.
/* When true do load else don't. */
window.doLoad = true
setInterval(function () {
if window.doLoad {
document.getElementById('see_older').getElementsByClassName('content')[0].click();
}
}, 500);
/* This will toggle doLoad every two minutes. */
setInterval(function () {
if (window.onLoad == true) {
window.doLoad = false;
} else { window.doLoad = true; }
}, 120000); // two minutes of milliseconds
In your case though you might be better off using the Facebook Graph API.
Graph API documentation from Facebook
Here's an existing question with the API using Python to do basically the same thing you want to do.
JS question, also similar
I have several flash movies on a site. They all loop through an animation which lasts for approximately 15 seconds (but this is not set in stone). They all begin to play at the same time and are in sync.
However, when I resize the browser window, or scroll the page, if a flash player instance is not in the viewport, it stops playing. When it returns to the viewport, it resumes and subsequently is out of sync with the other flash player instances.
I am under the impression this is a flash player optimisation. Is there anyway of disabling this behaviour, possibly through JS / AS3? It appears to happen in Firefox and Chrome on Windows.
UPDATE
Just to clarify. I have methodologies in place using local connection and ExternalInterface to sync the ads. What I am looking for is a method to disable the "optimisation" of FlashPlayer which results in the frame rate being drastically reduced.
You can't disable this feature. It's put in place to lower the memory and CPU use when the flash application isn't visible.
What is available for you though is something called the Throttle API. This is a dedicated API created to allow creators of Flash applications the ability to be notified exactly when their application is going to be slowed down/throttled.
Here's an example.
addEventListener(ThrottleEvent.THROTTLE, onThrottleEventHandler);
function onThrottleEventHandler(e:ThrottleEvent):void
{
if(e.state == ThrottleType.THROTTLE)
{
/**
* the player is about to be slowed down to a frame rate
* set in e.targetFrameRate
*/
}
else if(e.state == ThrottleType.PAUSE)
{
/**
* the player is going to be paused completely, AKA 0 fps
*/
}
else if(e.state == ThrottleType.RESUME)
{
/**
* the player is now back to normal as it has resumed
* from the paused or throttled state
*/
}
}
Now you can figure out a way that works best for you but my suggestion is to store the current time that has passed whenever being throttled or paused via:
currentTime = getTimer();
Then calculate how much time has passed once your application has resumed using:
passedTime = getTimer() - currentTime;
Then do what you like with this information.
Hopefully this has helped, should offer you a greater degree of control now that you're familiar with the Throttle API. For more information on it, check it out in the documentation here: ThrottleEvent
I belive this kind of behavior its normal, its kind of a bug of flash that has never been fixed.
I belive inha may got u a solution, but not on an enter_frame event. that just to brutal.
what I would do is:
create a timer event.. each X seconds.. so it will call a checkFunction,
in my checkFunction(). I would check if all my movieClips are syncronized.
and if I found 1 that is not.. ill put a simple gotoAndPlay(xFrame);
var aMovieClips:Array; //get all ur movieclips into this array.
var timeToCheck = 1000; //time to check for a unsincronized movieclip
var time:Timer=new Timer(timeToCheck,0);//do inifinite call each 1000 seconds
time.start();
time.addEventListener(TimerEvent.TIMER, checkFunction);
public function checkFunction(e:TimerEvent):void
{
var aCurrentFrames:Array;
for (var n:int = 0;n<aMovieClips.length();n++)
aCurrentFrames.push(aMovieClips[n].currentFrame);
//so now u have all current frames of all movie clips.. so u can add a
//gotoAndPlay(x); to any unsyncronized movieclip
}
If it is very important that each SWF progresses simultaneously, I would control it by JavaScript.
Assuming each SWF file is a simple MovieClip, something like this is how I would go about it:
Set the Document Class of each FLA file to this:
package {
import flash.display.MovieClip;
import flash.external.ExternalInterface;
public class ExternalInterfaceControlledMC extends MovieClip {
public function ExternalInterfaceControlledMC() {
this.stop();
if (ExternalInterface.available) {
try {
ExternalInterface.addCallback("setFrame", jsSetFrame);
} catch (error:Error) {
trace("An Error occurred:", error.message);
}
} else {
trace("ExternalInterface is not available");
}
}
private function jsSetFrame(value:String):void {
var frameNumber = int(value) % this.totalFrames;
this.gotoAndStop(frameNumber);
}
}
}
In the JavaScript, you would add a reference of each instance of the SWFs into an array, then use a timer to tell each SWF to progress to a frame number.
var swfElements; //Array
var currFrame = 1;
function onPageLoad() {
//Init timer
setInterval("progressFrame", 1000 / 30); //30 fps
}
function progressFrame() {
for (var i = 0; i < swfElements.length; i++) {
swfElements[i].setFrame(currFrame);
}
currFrame++;
}
Please beware that nothing about this code is tested and is only meant to be used to illustrate my train of thought.
Very strange bug I can't seems to figure out.
I am trying to get an HTML5 video to play from a certain position when a user hits play. I am trying to have it seek right when the video starts to play.
On my play event I do this.currentTime = X
On the browser it works fine. But on the IPad, when I play the video, the video doesn't seek to the right position (it starts from zero).
Even more oddly, if I do the this.currentTime = X call in a setTimeout of let's say 1 second, it works on the IPad (sometimes).
On iOS, videos load at play time (see item #2), not at page load time. My guess is that the video is not loaded when you run this.currentTime = X, so it has no effect. This also explains why delaying the operation can sometimes fix the problem: sometimes it has loaded after a second, sometimes not.
I don't have an iOS device to test, but I'd suggest binding a loadeddata listener to the video so that your currentTime manipulation only happens after the video begins loading:
// within the play event handler...
if(!isIOSDevice) {
this.currentTime = X;
} else {
function trackTo(evt) {
evt.target.currentTime = X;
evt.target.removeEventListener("loadeddata", trackTo)
});
this.addEventListener("loadeddata", trackTo);
}
You'll need to set isIOSDevice elsewhere in your code, based on whether the current visit comes from an iOS device.
While this question is quite old the issue remains open. I discovered a solution that works on multiple tested iPads (1+2+3) for iOS 5 and 6 (iOS3+4 not tested):
Basically you first have to wait for the initial playing event, then add a one-time binder for canplaythrough and then for progress - only then can you actually change the the currentTime value. Any tries before that will fail!
The video has to start playing at first, which makes a black layer on top of the video element kinda handy. Unfortunately, sounds within the video canNOT be deactivated via JavaScript --> not a perfect UX
// https://github.com/JoernBerkefeld/iOSvideoSeekOnLoad / MIT License
// requires jQuery 1.8+
// seekToInitially (float) : video-time in seconds
function loadingSeek(seekToInitially, callback) {
if("undefined"==typeof callback) {
callback = function() {};
}
var video = $("video"),
video0 = video[0],
isiOS = navigator.userAgent.match(/(iPad|iPhone|iPod)/) !== null,
test;
if(isiOS) { // get the iOS Version
test =navigator.userAgent.match("OS ([0-9]{1})_([0-9]{1})");
// you could add a loading spinner and a black layer onPlay HERE to hide the video as it starts at 0:00 before seeking
// don't add it before or ppl will not click on the play button, thinking the player still needs to load
}
video.one("playing",function() {
if(seekToInitially > 0) {
//log("seekToInitially: "+seekToInitially);
if(isiOS) {
// iOS devices fire an error if currentTime is set before the video started playing
// this will only set the time on the first timeupdate after canplaythrough... everything else fails
video.one("canplaythrough",function() {
video.one("progress",function() {
video0.currentTime = seekToInitially;
video.one("seeked",function() {
// hide the loading spinner and the black layer HERE if you added one before
// optionally execute a callback function once seeking is done
callback();
});
});
});
} else {
// seek directly after play was executed for all other devices
video0.currentTime = seekToInitially;
// optionally execute a callback function once seeking is done
callback();
}
} else {
// seek not necessary
// optionally execute a callback function once seeking is done
callback();
}
});
}
the whole thing can be downloaded from my GitHub repo
apsillers is right. Once the video starts playing, the Quicktime player will come up and the video will not be seekable until the first 'progress' event is triggered. If you try to seek before then, you'll get an invalid state error. Here's my code:
cueVideo = function (video, pos) {
try {
video.currentTime = pos;
// Mobile Safari's quicktime player will error if this doesn't work.
} catch(error) {
if (error.code === 11) { // Invalid State Error
// once 'progress' happens, the video will be seekable.
$(video).one('progress', cueVideo.bind(this, video, pos));
}
}
}
Appreciate the attempts for answers below. Unfortunately, had to resort to just checking inside timeupdate if the currenttime was > 0 and < 1, if it was then went to that part of the video and removed the listener to timeupdate.
try to limit your X to 1 decimal
X.toFixed(1);
As you mentioned it works sometimes after a time of 1 second. Have you tried to set the position after the playing event fires? or maybe even the canplaythrough event
Take a look at the source of this page to see a whole list of events that can be used (in the javascript file)