I am building a javascript game, and i want to create a background music based on sound file snippets. Short mp3 files to play them as one continuous track. I have tried binding an "ended" event handler on the audio file, though this causes a delay between audio fragments.
To solve this I made a hacky solution that still does not work, changing the audio 1 second before it finishes.
Ebuc.manageAudio = function(){
var listener = function (event) {
if (this.currentTime > (this.duration - 1) && Ebuc.bgnext) {
Ebuc.manageAudio();
console.log("aduio");
Ebuc.bgnext = false;
}
if(this.currentTime < 2){
Ebuc.bgnext = true;
console.log("reset");
}
console.log(event);
console.log("listener active")
};
var color = Level.current.color;
if(Ebuc.bgsong == null) {
Ebuc.bgsong = new Audio('assets/sound/' + Resources.audioSetList[color].getcurrentsong());
Ebuc.bgsong.addEventListener('timeupdate', listener, true);
}
else{
Ebuc.bgsong = new Audio('assets/sound/' + Resources.audioSetList[color].getcurrentsong());
}
Ebuc.bgsong.play();
Resources.audioSetList[color].next();
};
This sample works once, when it is time to switch fragment 2 to fragment 3 the loop stops. Console logging the event listener gives 4 times a log before stopping.
Q1: Why is this eventlistener suddenly disappearing?
Q2: Is there a non hack solution for chaining these audio fragments.
I thank you in advance.
You're going to have more than just pausing issues trying to rapidly switch between two short audio clips, you're going to probably want to crossfade between the two audio tracks quickly as well to prevent any popping, artifacts, etc.
Here's an example of crossfading using howler from howler's github issues. You could probably use this example, and keep a queue of loaded instances to transition to. I hope that helps.
//you'll probably want this crossfade duration to be shorter.
var crossfadeDuration = 5000,
volume = 0.7;
var instance1, instance2, soundDuration;
// Singleton helper to build similar instances
var createHowlerInstance = function (urls, onload) {
return new Howl({
urls: urls,
loop: false,
volume: 0,
onload: onload
});
};
// Create "slave" instance. This instance is meant
// to be played after the first one is done.
instance2 = createHowlerInstance(['file2.mp3']);
// Create "master" instance. The onload function passed to
// the singleton creator will coordinate the crossfaded loop
instance1 = createHowlerInstance(['file1.mp3'], function(){
// Get the sound duration in ms from the Howler engine
soundDuration = Math.floor(instance1._duration * 1000);
(function crossfadedLoop(enteringInstance, leavingInstance){
// Fade in entering instance
enteringInstance.pos(0).play().fade(0, volume, crossfadeDuration);
// Wait for the audio end to fade out entering instance
// white fading in leaving instance
setTimeout(function(){
enteringInstance.fade(volume, 0, crossfadeDuration);
crossfadedLoop(leavingInstance, enteringInstance);
}, soundDuration - crossfadeDuration);
})(instance1, instance2);
});
By using the idea of setting a timeOut in the answer of pantalohnes I have created the following code to solve the gap:
Ebuc.manageAudio = function(){
var color = Level.current.color;
Ebuc.bgsong = new Audio('assets/sound/' + Resources.audioSetList[color].getcurrentsong());
Ebuc.bgsong.addEventListener("loadedmetadata",function(){
setTimeout(Ebuc.manageAudio, (Ebuc.bgsong.duration * 1000) - 50);
Ebuc.bgsong.play();
console.log(Ebuc.bgsong.duration);
Resources.audioSetList[color].next();
});
};
The 50 milliseconds timeout bridges the gap between the sequenced files exactly.
Answering your question (although I see you found another solution), I think I found your bug:
The second time your enter Ebuc.manageAudio(), Ebuc.bgsong is already set and you just create a new Audio Ebuc.bgsong = new Audio(...) without attaching the listener to it, so you're not being notified for any 'timeupdate' events emitted when playing the second audio file.
You should also remove the listener from the previous playing audio.
So, if all else is ok, I think this should fix it:
Ebuc.manageAudio = function(){
var listener = function (event) {
if (this.currentTime > (this.duration - 1) && Ebuc.bgnext) {
Ebuc.manageAudio();
console.log("aduio");
Ebuc.bgnext = false;
}
if(this.currentTime < 2){
Ebuc.bgnext = true;
console.log("reset");
}
console.log(event);
console.log("listener active")
};
var color = Level.current.color;
if(Ebuc.bgsong != null) {
Ebuc.bgsong.removeEventListener('timeupdate', listener, true);
}
Ebuc.bgsong = new Audio('assets/sound/' + Resources.audioSetList[color].getcurrentsong());
Ebuc.bgsong.addEventListener('timeupdate', listener, true);
Ebuc.bgsong.play();
Resources.audioSetList[color].next();
};
More than that, I think that if you properly remove the listener from the previous playing audio, you won't need that bgnext hack at all:
var listener = function (event) {
if (this.currentTime > (this.duration - 1)) {
Ebuc.manageAudio();
console.log("aduio");
}
console.log(event);
console.log("listener active")
};
Ebuc.manageAudio = function () {
var color = Level.current.color;
if (Ebuc.bgsong != null) {
Ebuc.bgsong.removeEventListener('timeupdate', listener, true);
}
Ebuc.bgsong = new Audio('assets/sound/' + Resources.audioSetList[color].getcurrentsong());
Ebuc.bgsong.addEventListener('timeupdate', listener, true);
Ebuc.bgsong.play();
Resources.audioSetList[color].next();
};
Let me know if that worked :)
Related
JavaScript touch events contain properties for radius and force. Unfortunately it appears that events aren't generated when either property changes. Events are only triggered for things like touch start, move or end. Can anyone think of a way to get more updates on change of touch size?
Currently to get radius updates I have to wiggle my finger to trigger the touch move event, but I would prefer a software solution.
I had the same issue and then discovered this blog post: http://blog.framerjs.com/posts/prototyping-3D-touch-interactions.html
In a nutshell, we need to use touchstart event to capture the touch event, assign the event to a variable and then use setInterval to get the force value:
var el = document.getElementById('myElement');
var currTouch = null;
var currTouchInterval = null;
attachListeners = function () {
el.addEventListener('touchstart', enterForceTouch, false);
el.addEventListener('touchend', exitForceTouch, false);
}
enterForceTouch = function (evt) {
evt.preventDefault();
currTouch = evt;
currTouchInterval = setInterval(updateForceTouch, 10); // 100 times per second.
}
updateForceTouch = function() {
if (currTouch) {
console.log(currTouch.touches[0].force); // Log our current force value.
}
}
exitForceTouch = function (evt) {
evt.preventDefault();
currTouch = null;
clearInterval(currTouchInterval);
}
attachListeners();
I want to create a timer in JavaScript. I see the setTimeout(fn, 100) but unclear how to wrap this so it will clear itself at the end.
I tried doing
var buttonTimer = null;
$scope.backButton = function() {
if(buttonTimer === null){
$history.back();
}
buttonTimer = setTimeout(function(buttonTimer){
buttonTimer = null;
}, 100);
}
The whole point is to prevent the user from hitting this function too quickly.. and ignoring all subsequent clicks within that 100ms window, at the end of the window, clear the timer and resume accepting clicks
Since you are doing angular, I prepared a plnkr for demonstration:
http://plnkr.co/edit/5qrslKpmkglXTvEyYgBr?p=preview
Your code is almost Ok, the only problem is that you start a new timeout on every click. The effect is, that the callback fires multiple times and resets buttonTimer.
So the only change is:
var blocker = null;
$scope.backButton = function() {
if(blocker == null) {
blocker = setTimeout(function(){
blocker = null;
}, 1500);
// DO YOUR STUFF HERE
}
};
You can use throttle from lodash/underscore or Ramdajs.
for example
$scope.backButton=_.throttle(100,function(){/* do something here */});
i´m testing a Windows 8 app for a touch pannel.
for now i just want to handle the "tapped" events over the screen.
I´ve tried it with a "MSGesture" object, but the tap events fires the 60%-80% of times. It detects all the "pointerdown" events, but not all of them also trigger a tap event.
Now i´ve tried the same with a "GestureRecognizer" object and the problem remains the same: all the "pointerdown" events fire ok, but not all of them are recognize as tap events.
The Code:
gr = new Windows.UI.Input.GestureRecognizer();
gr.gestureSettings = Windows.UI.Input.GestureSettings.tap | Windows.UI.Input.GestureSettings.doubleTap;
gr.addEventListener("tapped", onTap);
var mainGrid = document.getElementById("mainGrid");
mainGrid.addEventListener("pointerdown", onPointerDown, false);
mainGrid.addEventListener('pointermove', processMove, false);
mainGrid.addEventListener('pointerup', processUp, false);
mainGrid.addEventListener('pointercancel', processUp, false);
function onPointerDown(e) {
count++;
counter.innerHTML = "Count: " + count;
// Get the current PointerPoint
var pp = e.getCurrentPoint(e.currentTarget);
// Feed the PointerPoint to GestureRecognizer
gr.processDownEvent(pp);
}
function processMove(e) {
// Get the current PointerPoint
var pps = e.getIntermediatePoints(e.currentTarget);
// Feed the PointerPoint to GestureRecognizer
gr.processMoveEvents(pps);
}
function processUp(e) {
// Get the current PointerPoint
var pp = e.getCurrentPoint(e.currentTarget);
// Feed the PointerPoint to GestureRecognizer
gr.processUpEvent(pp);
}
function onTap(e) {
count2++;
counter2.innerHTML = "Count2: " + count2;
}
As you can see, there are "counter" and "counter2" DOM elements, those are just used for showing a counter that increments each time each event is fired.
Could it be a Windows 8 App performance bug?
Thanks for your help!
I'm using BigVideo.js jQuery plugin to show full screen background videos. I have links that load different background videos on click. At the end of each video (3-5 sec long) I'm loading an animation. Each video has its own animation on top.
Right now I'm fading in the animations with a time out that corresponds to the video length but it's not full proof. What I really need is for #anim03 to fade in when video-03.mp4 ends. But I can't figure out exactly how the BigVideo.js .on("ended") event really works. In the code I have below (simplified):
// init plugin
var BV = new $.BigVideo({useFlashForFirefox:false});
BV.init();
function setupVideo(url) {
if (Modernizr.touch) {
BV.show(url + '.jpg');
} else {
BV.show(url + '.mp4',{
altSource: url + '.webm',
ambient: false
});
}
}
function setupAnimation(num) {
BV.getPlayer().on("ended", function () { // event from video.js API - when video ends playing
$('#anim0' + num).animate({ opacity: 1 });
});
}
$('a').on('click', function(e) {
e.preventDefault();
// we remove .ext cause we got to setup .webm and .jpg versions
var url = $(this).attr('href').replace('.mp4', '');
setupVideo(url);
var current = $(this).parent().index()+1;
setupAnimation(current);
});
The event is triggered but it seems to go through some sort of queue and fire multiple times. If I console.log num like that:
function setupAnimation(num) {
console.log(num);
BV.getPlayer().on("ended", function () {
$('#anim0' + num).animate({ opacity: 1 });
});
}
I get a single expected value. But if I do it like that:
function setupAnimation(num) {
BV.getPlayer().on("ended", function () {
console.log(num);
$('#anim0' + num).animate({ opacity: 1 });
});
}
Then I get multiple values for num and each time I click and this function is called, I get more and more… I'm guessing this .on("ended") loops through some array or something? I can't figure out that part from looking at the plugin's code.
Any help or pointer much appreciated!
When you use the player's "on" function you register a new listener function (This gives you the possibility to trigger multiple functions when something happens).
The reason your on "ended" function fires multiple times, is because each time setupAnimation executed it adds a new instance of the listener-function:
function () {
console.log(num);
$('#anim0' + num).animate({ opacity: 1 });
}
check the API documentation of Video.JS
https://github.com/videojs/video.js/blob/master/docs/api/vjs.Player.md#on-type-fn-
If I get right what you're trying to accomplish, the solution is to run 'setupAnimation' outside the on 'click' function.
There is a good example for doing long press in Javascript here: Long Press in JavaScript?
But it does not provide for knowing the duration of the press.
If I want to do different things based on the length of the press I cant use the pattern in that post.
I was trying to do something similar by saving current time in a variable on('mousedown')
and then calculating the time difference on('mouseup').
this works fine within a normal Javasript page in a "normal" browser.
However within my phonegap app something happens,
looks like the mouseup event is not being called if the finger is kept on the screen for a long duration (say 5 sec..).
Is this some native mobile browser behavior? Can I override it somehow?
I am using plain jQuery not jQuery mobile.
Any ideas anyone?
You could have a look at how the taphold and vmouseup (handleTouchEnd() line 752) events are implemented in jQuery mobile source code.
Since it is already tested and implemented I'd suggest to use jquery mobile instead of jquery and modify (since it already handles all the 'quirks' related each mobile browser), and change the code as you need.
You can check the time to identify Click or Long Press [jQuery]
function AddButtonEventListener() {
try {
var mousedowntime;
var presstime;
$("button[id$='" + buttonID + "']").mousedown(function() {
var d = new Date();
mousedowntime = d.getTime();
});
$("button[id$='" + buttonID + "']").mouseup(function() {
var d = new Date();
presstime = d.getTime() - mousedowntime;
if (presstime > 999/*You can decide the time*/) {
//Do_Action_Long_Press_Event();
}
else {
//Do_Action_Click_Event();
}
});
}
catch (err) {
alert(err.message);
}
}
Note that this solution is usefull if you do not use jQuery Mobile for some reason.
I used the article Fast Touch Event Handling and just added a piece of code
$.event.special.tap = {
distanceThreshold: 10,
timeThreshold: 350,
setup: function () {
var self = this,
$self = $(self);
// Bind touch start
$self.on('touchstart', function (startEvent) {
// Save the target element of the start event
var target = startEvent.target,
touchStart = startEvent.originalEvent.touches[0],
startX = touchStart.pageX,
startY = touchStart.pageY,
threshold = $.event.special.tap.distanceThreshold,
timeout,
expired = false;
function timerFired() {
expired = true;
}
function removeTapHandler() {
clearTimeout(timeout);
$self.off('touchmove', moveHandler).off('touchend', tapHandler).off('touchcancel', removeTapHandler);
};
function tapHandler(endEvent) {
removeTapHandler();
if (target == endEvent.target) {
if (expired) {
$.event.simulate('longtap', self, endEvent);
} else {
$.event.simulate('tap', self, endEvent);
}
}
};
// Remove tap and move handlers if the touch moves too far
function moveHandler(moveEvent) {
var touchMove = moveEvent.originalEvent.touches[0],
moveX = touchMove.pageX,
moveY = touchMove.pageY;
if (Math.abs(moveX - startX) > threshold || Math.abs(moveY - startY) > threshold) {
removeTapHandler();
}
};
// Remove the tap and move handlers if the timeout expires
timeout = setTimeout(timerFired, $.event.special.tap.timeThreshold);
// When a touch starts, bind a touch end and touch move handler
$self.on('touchmove', moveHandler).on('touchend', tapHandler).on('touchcancel', removeTapHandler);
});
}
};
So, now I have a tap and a longtap events