Unwanted audio file behavior `onend`. Stumped on how to hide, delay or override timeline behavior

audio

(Raleigh Green) #1

Hello Hype friends,
Happy holidays!

I’m at the end of a project where I’m refining behavior and getting things polished for release. One nagging issue I’m currently trying work through has got me stumped!

Here is a brief overview of my situation - what I’m trying to do and where I’m stuck:

I’m making a site that features scenes/pages with audio files that can be played (and each audio file has scrolling lyrics that are synched along with the audio file playback).

I have an autoPlay system in place such that, if autoPlay is turned on, the song/scene will automatically transition (triggered onend) to the next song and continue to play.

If autoPlay is OFF, when the song gets to the end of the track, the audio track jumps back to the beginning and resets (and the lyrics reset along with it). This behavior works and looks fine and is expected (again, when autoPlay is OFF).

However, if autoPlay is ON, the audio track jumps back to the beginning and resets right before the transition (which is a push from right to left transition). The problem with this is that the song and lyrics resetting so abruptly looks distracting and unnatural right before a push transition.

I’m trying to hide, pause, delay or do whatever I can to make it look like the song lyrics stay in the place when autoPlay is on and the scene transitions to the next song. I can’t figure out how to do this since I need to trigger any ‘hiding’ timelines right before onend on but not onend. If I trigger it onend, it would be too late and the song would have already resetted itself.

One fix I have tried (that almost worked), is if autoPlay is on, onend I made the song jump to the end right after it resets. On a desktop computer this happens so fast, it looks like it just stays there.

Problem is, on mobile it’s not fast enough and a flash back the 00:00 mark is momentarily perceivable before it flips back to the end. Plus, if the user uses the audio scrubber to quickly scrub to the end of the audio track, a visible flash is again triggered as it reaches the end.

This is clearly not a workable solution.

Another thing I’ve tried to do is to keyframe the lyric opacity changes directly into the autoPlay timeline, so that in the very beginning of scene load the lyrics are invisible, then they fade into view on song start, stay visible on playback, and then the lyrics fade out again a few seconds before the end of the track. This solution looks great (and works great with the scrubber!), but I really want the lyrics to fade into view on scene load and to fade into view when the song resets to 00:00. If I hard code the opacity changes into the autoPlay timeline, I can’t find any way to override the opacity and force the lyrics to fade into view without the song playing (played around with relative timelines for this, but no luck).

In a best case scenario, here is what the behavior should be:

  • when a song/scene loads, the lyrics would first be invisible. After a second or two the lyrics would fade into view and stay in this default state until further action is taken.
  • when an audio track is playing, the lyrics would stay visible until 1 or 2 seconds before the end of the song. Once it reaches the point where it is 98% or 99% through the song, the lyrics would disappear.
  • if autoPlay was ON when the end of the song is reached, the lyrics would stay invisible and the transition to the next scene would be triggered. If autoPlay was OFF, when the end of the song is reached, the lyrics and the audio would reset back to 00:00 and then after a second or two the lyrics would fade back into default state.

Is there anyone that can think of a way to put some sort of listener in place that fades the lyrics to invisible when it’s 2 seconds from the end of the song and also fades the lyrics back into view whenever the audio is reset back to 00:00?
Or perhaps (I’d also be fine with this simpler behavior:) is there just a way to just pause the lyrics when onend is reached so that the lyrics don’t automatically reset before the transition has had a chance to finish?

Ether of these would solve my issue but can’t figure out how to make it work.

If you are willing to help, please let me know and I’ll DM you a link to download the Hype project. The project contains some proprietary content that I’d rather not make easily available to the general public until it’s ready for release, but would be happy to share it with anyone for purposes of troubleshooting.

Thanks so much!


(Mark Hunte) #2

My initial thoughts are by using a persistant symbol for the lyrics but can you post a small example project. with your setup. ( note I did not read all of the above “yet” ) That would be easier for us to work through along with your post above.


(Raleigh Green) #3

Hi @MarkHunte,

Thanks so much for your reply.
And many apologies to you and other forum folks for the massive amount of verbiage in the issue description. I’ve created an example project to download that will hopefully make it much easier and quicker to see the issue and troubleshoot.

In order to avoid sharing any sensitive info from the client, I’ve created a version of the project that has all of the animations and artwork removed. I’ve also replaced all of the audio with blank MP3’s (audio exported with volume on zero). This way all of the functionality of the site is retained. Here is a link to download the example Hype project.

Here is how to reproduce the glitch I’m working with:
It’s actually very simple to reproduce the issue, so please don’t be thrown by numerous steps to get there:

  1. Download the Hype file and open and preview the project in a browser.
  2. Once the site loads, click the white circular logo in the middle of the black screen.
  3. Click anywhere between or around the red “Nowhere Nation” and white “Omicron” text on the upper left of the menu screen.
  4. A black screen with “Nowhere Nation” appears with 2 arrows. Click the “>” arrow to move to the first song scene.
  5. In the song scene, press the autoPlay button in the lower left corner to start the blank audio track playing.
  6. Grab the audio scrubber button and manually drag the scrubber button to the end of the track.
  7. Once the audio scrubber is dragged to the end of the track, you will notice a flash of text in the lyric area - this is the glitch!

This is the track jumping back to the beginning and then quickly being forced to jump to the end of the track, which creates a flash.

The code that creates that flash-on-scrub problem, was something I put in in an effort to fix (or rather hide) another separate but directly related issue.

In order to see how the issue above fits into the bigger picture, It would be very helpful to point out the original problem.

In order to see this original issue (please stick with me here, it’s easy to reproduce, just verbose to explain), here’s what to do:

In the Hype file, open up the setUp() function and comment out the line of code (song.seek(song.duration());) on line 150, then re-preview the site in a browser.

This will allow you to witness the original problem that the code on line 150 is trying to solve.

To reproduce this (original) issue, follow steps 1 - 5 above to preview on a browser and navigate to the first song scene.

Then:

  1. Press autoPlay to get the blank song playing.
  2. Scrub the audio play head to almost the end of the song and let it play so that you can witness the transition to the next scene.
  3. Here is what to look for: when autoPlay is on and the audio track reaches the end, the audio track resets to the beginning and the lyrics snap back to the beginning along with it. This creates a similar unwanted flash where the lyrics reset right before the transition sequence.

For the life of me, I can’t figure out how to keep this flash from happening (since it is triggered onend, and any code that could potentially mask the issue would also be triggered onend, and therefore ineffective)!

I had the idea of resetting the audio track to the end of the song onend, which works on desktop because the reset happens so fast, it’s not noticeable. However, that then created the other problem (described above) of the flash occurring at the end of the track when the audio file is scrubbed to the end.

So, I’m wondering if you have any suggestions on how to solve the original problem - how to keep the audio file (and in particular, the lyrics) from resetting too soon when autoPlay is on?

Thank you again for any input you might have on this.
And also thank you for wading through the verbiage.
I’m hoping this will be a situation where it’s much easier to solve the issue than describe it, lol!

Any ideas at all would be greatly appreciated!
All the best :slight_smile:


(Mark Hunte) #4

ok having a quick look.

But initially it looks to me that there is another seek to 0 or reset to beginning going on somewhere that is the real issues.

And for the life of me cannot find where you created most of you custom behaviours like PlayHeaven etc.


(Raleigh Green) #5

Thanks so much for taking a look!
I agree that there has to be a reset to 0 happening somewhere, but I don’t know where it is happening - it was always the default behavior.
Very curious to see what you find. :thinking:
Thank you again for the assistance.


(Mark Hunte) #6

Can you point me to these…

Sure they may be in a symbol but wood for trees…


(Raleigh Green) #7

Ah - and regarding the custom behaviors: those all came from the animation scenes (which are symbols) that I deleted in order to share the Hype file. Those can all be disregarded - sorry for any confusion there. If the animations were still there, you would be able to find them (but they are unrelated to the current issue and all working fine).


(Mark Hunte) #8

:scream::scream::scream:

Not your fault …


(Mark Hunte) #9

So, I could not really find a better solution than what you have. I looked over the Howler docs. Did not find anything that conclusively said that the default behaviour is to reset to 0 on end but I think that is what is happening.

Initially I thought I could use the onfade event but this is not being called?. That maybe just because these are blank audios.

Any way oolong those lines I I added a -1 to your stop gap line and this seemed to work.
But my Mac is not your Mac…

try it and see , also not knowing how your sound is actually ending but assume 1s back at the end would be safe…

if (window.autoPlay) {
            		// Jump to the end of the song so that it looks like the song stays stationary during fade before transition
            		song.seek(song.duration()-1);

(Raleigh Green) #10

Hi @MarkHunte,

Thanks so much for taking a look at this!
I’m traveling today but will give this a look tonight when I get there. Fingers crossed regarding the -1 you added!

Thanks again for your input.
Happy New Years :slight_smile:


(Raleigh Green) #11

Hi @MarkHunte,

So I tested the -1 on my machine and unfortunately I’m getting the same flash when I scrub to the end when autoPlay is turned on. There is plenty of leeway at the end of the song so 1 (or more) seconds at the end of the song would be totally safe.

Seems like the basic question is: since (it seems) like the default behavior is to reset to 0 on end, how to make a change happen right before it gets to the end?

I have access to percent complete:
var percentComplete = (hypeDocument.currentTimeInTimelineNamed("Scrubber"+window.counter) / hypeDocument.durationForTimelineNamed("Scrubber"+window.counter));

Is there a way that I can put in place a listener that fires an early fade to black only if:

  • autoPlay is ON
  • the audio track reaches 98% complete?

If so, I’d think this should work with both scenarios (scrubbing and reaching onend).
With this setup I could make the scene fade to black early enough to hide the audio file resetting back to the beginning.

I’m just not sure how to configure this.

Any further ideas from you or anyone else would be so appreciated.
Very stuck!


(Raleigh Green) #12

Quick addendum to my previous post:

The song.seek(song.duration()-1); works great with just regular autoPlay.
But I still get the flash when autoPlay is ON and I scrub the audio to the end.

So, I’m realizing instead of putting in a listener (as I suggested above), if I could somehow make it so that when autoPlay is ON, the audio scrubber only scrubbs to 98% or 99% (and then stops), this would fix my issue because the track would not reach the end. Unfortunately, my attempts at making this strategy were were fruitless.

This is my scrubber code:

function Scrubb(hypeDocument, element, event) {
	var song = window["song"+window.counter];
	
	// Setup scrubber position to control timeline position
	var percentComplete = (hypeDocument.currentTimeInTimelineNamed("Scrubber"+window.counter) / hypeDocument.durationForTimelineNamed("Scrubber"+window.counter));
	
	// Mute song while scrubbing
	song.volume(0);
	
	// Make the audio scrub along with scrubber timeline with howler.js's seek() function
	song.seek(song.duration() * percentComplete);
	
	// Unmute song once scrub is done
	if(event['hypeGesturePhase'] == hypeDocument.kHypeGesturePhaseEnd) {
		song.volume(1);
	}
}

Can you think of any way that I can modify this code so that when autoPlay is ON, I can only scrubb to 98% or 99% of the audio track? Seems like there should be a way to modify percentComplete so that I can’t scrubb all the way to the end.

Thanks again - any ideas would be greatly appreciated.
All the best!


(Mark Hunte) #13

I did try that idea already with no luck. But will look again .

I am sure there are a few answers steering us in the face.


(Mark Hunte) #14

So maybe…

	    var song = window["song"+window.counter];
	
	// Setup scrubber position to control timeline position
	var percentComplete = (hypeDocument.currentTimeInTimelineNamed("Scrubber"+window.counter) / hypeDocument.durationForTimelineNamed("Scrubber"+window.counter));
	
	// Mute song while scrubbing
	song.volume(0);
	
	// Make the audio scrub along with scrubber timeline with howler.js's seek() function
	
	
		//console.log(song.duration() ,song.duration() -2, song.seek())
	
	 
	if (window.autoPlay && song.seek() >= song.duration() -1 ) {
	console.log('autoPlay', song.duration() -1)
song.seek(song.duration() -1);
} else {


song.seek(song.duration() * percentComplete);

}
 
	// Unmute song once scrub is done
	if(event['hypeGesturePhase'] == hypeDocument.kHypeGesturePhaseEnd) {
		song.volume(1);
	}

Don’t do more than 1s or this runs into problems when trying to scrub back.
Again hard to see if this is really working on my mac.

You will also need to add similar to clickToTime.


(Mark Hunte) #15

Hah,

Just had the thought that what would be useful is getting a current timeupdate of the songs position.
Howler does not have that so we would need to create one and use a requestAnimationFrame.

Then remembered that when I tried to do the similar to my last post I did it in the step() function you have in the setup() function, because this was updating with a requestAnimationFrame.

So I now think again this would be the best place to put the code ( remove all other changes to the Scrub() and also remove the song.seek(song.duration() -1) in the onend() function. )

We continuously check for where the time is and if window.autoPlay && song.seek() >= song.duration() -1
we set song.seek(song.duration() -1); and run the same code you run in the onend event function.

This I think will save us putting code all over the place in other functions like scrub, onend and clickToTime.

The one thing though I do notice is the on auto display does not keep lit although auto does continue on the next track. But I am sure you can fix that…

function step() {
                    ///////progressBar
                 
                 if (window.autoPlay && song.seek() >= song.duration() -1 ) {
					
					console.log('autoPlay ', song.duration() -1)
					
					
					song.seek(song.duration() -1);
					
					hypeDocument.startTimelineNamed('sceneFader' + nr, hypeDocument.kDirectionForward)
        			// Reset the song back to 00:00 after a short delay
            		setTimeout(function(){
            			song.seek(0);
    				},1500);
    				// Trigger the transition scene after resetting play/pause button
            		setTimeout(function(){
        				window.transitionScene();
    				},1000);
    				setTimeout(function(){
            			hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward' + nr);
    				},1900);
					cancelAnimationFrame(animationRequest); 
							} else {
	
                    var percentComplete = song.seek() / song.duration();
                    hypeDocument.goToTimeInTimelineNamed((hypeDocument.durationForTimelineNamed("Scrubber" + nr) * percentComplete), "Scrubber" + nr);
                    hypeDocument.goToTimeInTimelineNamed((hypeDocument.durationForTimelineNamed("AutoScroll" + nr) * percentComplete), "AutoScroll" + nr);
                    ////////////////

                    ////////////////timeDisplay
                    document.getElementById("timeDisplay" + nr).innerHTML = timeString(song.seek());
                    document.getElementById("durationDisplay" + nr).innerHTML = timeString(song.duration());
                    ////////////////
                    requestAnimationFrame(step); 
                    }             
                }

                animationRequest = requestAnimationFrame(step);

            },

p.s

There may be other issues doing it this way but this may give you an idea moving forward…

Also fyi


(Raleigh Green) #16

@MarkHunte, thank you so much for the fantastic input on all of this! And also many thanks for posing the question to goldfire! I was thinking of contacting them as well in order to get some clarity on this. One would think default reset behavior would be mentioned somewhere in the howler documentation but I also see no mention of it anywhere. Very curious to see what they say.

For what it’s worth, last night I made a super simple test version (with song.play() as the only method) and was able to confirm that howler audio files do indeed reset to 0 without any direction to do so. Kind of makes me wish I had just used the Web Audio API (also did a more-or-less identical test with it last night and, predictably, there is no auto-reset at song end), but unfortunately I don’t really have the time to backtrack and rework.

In the meantime, I’m going to give your suggestions a go tonight. Fingers crossed. :slight_smile:
Will report back with results.
Thanks again for all your awesome help and Happy New year!


(Hans-Gerd Claßen) #17

wouldn’t it be just setting the timelinepositions ‘onend’ too?

your first approach was to
song.seek(song.duration())
when onend fires. here should be the moment to set the position of the songtexttimeline accordingly …


(Mark Hunte) #18

The problem is Howl is reseting the track to 0 before the onend is fired?
So a simpler idea is not to get to the end in the first place when Auto is on.
For this to work we would have to place code in a few places or just put it some where that is continuously being fired/updated .

To be honest if this was my code I would tear it down and change the way fade in out worked ( hopefully ) but even for me the code is too mature for me to go that far ( at least at the mo…) so trying to work with what we have…


(Raleigh Green) #19

Hey guys,

@MarkHunte, thanks again for your input and @h_classen, thank you for your thoughts as well. You guys are awesome.

Just wanted to follow up with this.

I had a chance to plug in the:

if (window.autoPlay && song.seek() >= song.duration() -1 ) { console.log('autoPlay', song.duration() -1) song.seek(song.duration() -1); } else { song.seek(song.duration() * percentComplete); }

code tonight.

I tried it in both the Scrubb() function and the step() function and I’m sorry to say that the flash still occurs with both of them. Ugh.

Besides waiting to see if James Simpson from GoldFire/Howler.js has any options (which, unfortunately due to time constraints, is not really an option), at this point, I’m considering looking into what might be involved with reworking this entire thing with the Web Audio API. This is a very daunting and somewhat painful prospect to consider because everything else is working perfectly and it’s so close to release.

This is a tricky one. If either of you have any other thoughts, I’m willing to try just about anything at this point. In the meantime, I will also continue to think of options, and I again thank you both for your awesome input.

All the best and happy New Year. :slight_smile:
Thanks again.


(Mark Hunte) #20

Did you try the last code I posted which including doing the transition… ( and you would need to comment out the onend autoplay code.

Just did it on a different Mac.

The setup code I just used was this.

    //---Setup Variables---------------------------------
	
	window.counter = 1;
	window.autoPlay = false;
	window.numberOfTracks = 12;
	window.currentSong = 'song' + window.counter;
	
	//---Transition Scene Function---------------------------------
	
	// Switch to the next scene by default, or resets back to the first scene (scene1), if the last scene has been reached.
	window.transitionScene = function () {
		
		// Increment the counter in order to play the next song and transition to the next scene.
		window.counter++;
		// If the counter is greater than the number of tracks, reset to scene/song 1.
		if (window.counter > window.numberOfTracks) {
		    window.counter = 1;
		    // transition to the first scene with a push left to right transition
		    hypeDocument.showSceneNamed('scene' + window.counter, hypeDocument.kSceneTransitionPushLeftToRight);
		    
		// Otherwise, keep transitioning to the next scene.
		} else {
			// transition to the next scene with a push right to left transition
			hypeDocument.showSceneNamed('scene' + window.counter, hypeDocument.kSceneTransitionPushRightToLeft);
		}
		
		// Stop the new song to reset it, then play the song
		setTimeout(function(){
			window['song' + window.counter].stop();
			window['song' + window.counter].play();
		},1000);
		
		// Check to see if autoPlay is on
		if (window.autoPlay) {
			// If autoPlay is ON, after a short delay, turn on both autoPlay and play/Pause buttons in the new scene
			setTimeout(function(){
				
            	hypeDocument.triggerCustomBehaviorNamed('autoPlayToggleForward' + window.counter);
            	hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonForward' + window.counter);
            
        	},1200);
		} else {
			// If autoPlay is OFF, after a short delay, turn on only the play/Pause button in the new scene
			setTimeout(function(){

            	hypeDocument.triggerCustomBehaviorNamed('autoPlayToggleBackward' + window.counter);
            	hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonForward' + window.counter);
            
        	},1200);
		}
		
	}
	
	//---reset buttons back to defaults---------------------
	
	// This function resets the play/pause and AutoPlay buttons back to defaults. Used primarily in FFbutton() and RRbutton() functions.
	window.flipButtonsBackToDefaults = function() {
		
		hypeDocument.triggerCustomBehaviorNamed('autoPlayToggleBackward' + window.counter);
		hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward' + window.counter);
	
	}
	
	//---set the timestring---------------------------------
	
	// this function converts the audio time to min and secs
	function timeString(time){
		var mins = Math.floor(time / 60);
    	var secs = Math.floor(time % 60);

    	if (secs < 10) {
        	secs = '0' + String(secs);
      	}
      	if (mins < 10) {
      		mins = '0' + String(mins);
      	}
      	return mins + ':' + secs;
	}
	
	//---Build the audio objects---------------------------------
	
	for (var nr = 1; nr <= window.numberOfTracks; nr++) {
    (function(nr) {
        window['song' + nr] = new Howl({

            src: ['${resourcesFolderName}/song' + nr + '.mp3'],
            preload: true,

            onload: function() {
                var timer = hypeDocument.getElementById("timeDisplay" + nr);
                var duration = hypeDocument.getElementById("durationDisplay" + nr);
                var song = window['song' + nr];

                timer.innerHTML = timeString(0);
                duration.innerHTML = timeString(song.duration());

                function step() {
                    ///////progressBar
                 
                 if (window.autoPlay && song.seek() >= song.duration() -1 ) {
					
					console.log('autoPlay ', song.duration() -1)
					
					
					song.seek(song.duration() -1);
					
					hypeDocument.startTimelineNamed('sceneFader' + nr, hypeDocument.kDirectionForward)
        			// Reset the song back to 00:00 after a short delay
            		setTimeout(function(){
            			song.seek(0);
    				},1500);
    				// Trigger the transition scene after resetting play/pause button
            		setTimeout(function(){
        				window.transitionScene();
    				},1000);
    				setTimeout(function(){
            			hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward' + nr);
    				},1900);
					cancelAnimationFrame(animationRequest); 
							} else {
	
                    var percentComplete = song.seek() / song.duration();
                    hypeDocument.goToTimeInTimelineNamed((hypeDocument.durationForTimelineNamed("Scrubber" + nr) * percentComplete), "Scrubber" + nr);
                    hypeDocument.goToTimeInTimelineNamed((hypeDocument.durationForTimelineNamed("AutoScroll" + nr) * percentComplete), "AutoScroll" + nr);
                    ////////////////

                    ////////////////timeDisplay
                    document.getElementById("timeDisplay" + nr).innerHTML = timeString(song.seek());
                    document.getElementById("durationDisplay" + nr).innerHTML = timeString(song.duration());
                    ////////////////
                    requestAnimationFrame(step); 
                    }             
                }

                animationRequest = requestAnimationFrame(step);

            },
            
            onloaderror: function() {
                console.log("onload error");
            },
            
            onplay: function() {
                var duration = document.getElementById("durationDisplay" + nr);
                var song = window['song' + nr];
                duration.innerHTML = timeString(song.duration());
            },

            onseek: function() {
                ////////////////timeDisplay
                var timer = document.getElementById("timeDisplay" + nr);
                var song = window['song' + nr];
                var duration = document.getElementById("durationDisplay" + nr);
				
                timer.innerHTML = timeString(song.seek());
                duration.innerHTML = timeString(song.duration());
                ////////////////
            },

            onpause: function() {
                cancelAnimationFrame(animationRequest);   
            },

            onend: function() {
            	var song = window['song' + nr];
            	cancelAnimationFrame(animationRequest);
             
            hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward' + nr);
            	 
            },
        });
    })(nr);
  }