How to refactor this code? Can't successfully build audio objects with loop

Hello Hype friends!

I have some code that creates 12 audio objects with Howler.js.
The code is working great, but each object is currently created individually.
I’d like to make this code more dry by making a loop that automatically builds the audio object 12 times.

I’ve tried a for loop in order to iterate through them automatically, but I have had no luck in getting it to work. Any idea how I can refactor this code so that I only have to write the object code once?

Here is the code - the only thing that is different between each object is the counter number (that starts at 1).

window.song1 = new Howl({src: 
     ['${resourcesFolderName}/song1.mp3'],
preload: true,
onload: function(){
	  var timer = document.getElementById("timeDisplay1");
	  var duration = document.getElementById("durationDisplay1");
      var song = window.song1;

		timer.innerHTML = timeString(0);
		duration.innerHTML = timeString(song.duration());
	
	function step(){
		///////progressBar
		var percentComplete = song.seek() / song.duration();                       
		hypeDocument.goToTimeInTimelineNamed((hypeDocument.durationForTimelineNamed("Scrubber1") * percentComplete), "Scrubber1");
		hypeDocument.goToTimeInTimelineNamed((hypeDocument.durationForTimelineNamed("AutoScroll1") * percentComplete), "AutoScroll1");
		////////////////

		////////////////timeDisplay
 		document.getElementById("timeDisplay1").innerHTML = timeString(song.seek());
 		document.getElementById("durationDisplay1").innerHTML = timeString(song.duration());
		////////////////
		requestAnimationFrame(step); 
	}	

	  animationRequest = requestAnimationFrame(step); 
},
onloaderror: function(){
  console.log("onload error");
},
onplay : function () {
  var duration = document.getElementById("durationDisplay1");
  var song = window.song1;
  duration.innerHTML = timeString(song.duration());
},
  onseek : function() {
	////////////////timeDisplay
	var timer = document.getElementById("timeDisplay1");
	var song = window.song1;
 	var duration = document.getElementById("durationDisplay1");
 		
	timer.innerHTML = timeString(song.seek());
	duration.innerHTML = timeString(song.duration());
	////////////////
  },
  onpause : function(){
	  cancelAnimationFrame(animationRequest);
  },
  onend : function(){
	cancelAnimationFrame(animationRequest);
	//hypeDocument.startTimelineNamed('play/pause1', hypeDocument.kDirectionReverse);
	hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward1');
	if (window.autoPlay) {
		window.transitionScene();
	}
},});

I’d so appreciate any input on how to get this to work.
Thanks so much!

Some Hints with nr being your current number … to add a number to a string use:

'string'+nr+'more string'

to add a number to a variable name use

window['song'+nr] = ...

This should point you in the right direction

Thanks so much!
I was indeed thinking I was concatenating things wrong.
window['song'+nr] = ... was a big help.

I tried it and for some reason it’s still not working for me.

This is what I ended up with:

var nr;
for (nr = 1; nr <= window.counter; nr++) {	
window['song'+nr] = new Howl({
src: ['${resourcesFolderName}/song'+nr+'.mp3'],
preload: true,

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

		timer.innerHTML = timeString(0);
		duration.innerHTML = timeString(song.duration());
	
	function step(){
		///////progressBar
		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(){
	cancelAnimationFrame(animationRequest);
	hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward'+nr);
	if (window.autoPlay) {
		window.transitionScene();
	}
},

});

}

However, when I run this code, only the first song loads and errors pop up in the console like this:

`Uncaught TypeError: Cannot read property 'duration' of undefined`

It works great with no errors when the objects are created individually.

Thank you again for your input on this.
Do you see anywhere where I’m going wrong?
Seems like this should be a really simple refactor, but puzzled as to why it’s not working.

It might be the problem that document.getElementById("durationDisplay"+nr); doesn’t find it’s target because it isn’t present yet. When you create a new Howl the onload is fired when the mp3 is done loading meaning it expects all the elements to be present. Just a guess. It might help to see the current Hype Document for further help.

If my guess in the last suggestion is on track this might be an approach: Run your code on each scene load and don’t use it in a loop. Rather have the nr determined by the scene index.

Something like:

var scenes = hypeDocument.sceneNames();
var currentSceneName = hypeDocument.currentSceneName();
var nr = scenes.indexOf(currentSceneName);

you could also write this in one line if you don’t want to use additional variable names:

var nr = hypeDocument.sceneNames().indexOf( hypeDocument.currentSceneName() );

I think I understand generally what you mean by applying the code on each scene, but I’m not sure if I understand how this would be implemented. Would you like to see the project?

I’d be happy to share the Hype document and if you feel you have a moment to take a look than that would be amazing! However, I’d rather not share it in a public forum yet because in this current version (basically, the finished but not yet released version), proprietary content has been added. By any chance do you have an email address where I can send you the Hype file directly? Or, is there a way to DM through the Hype forum?

Again, thanks so much

It’s a closure problem when I think about it. See …

A solution is to use a Immediately Invoked Function Expression (IIFE) like

(function(nr) {

... code assignments here

})(nr);
2 Likes

Multitrack_howler.hype.zip (1.9 MB)

this is an old examplefile (i guess i did it for you¿)

2 Likes

Wonderful!
Thanks so much MaxZieb!

This was exactly the problem.
Just dropped in the code you helped with and it now works perfectly.
Thank you so much for pointing me in the right direction. :slight_smile:

Hi Hans-Gerd,

Thanks so much for this!
I actually couldn’t open the example - might have been created on a newer (as yet unreleased :wink: ) version of Hype?

However, MaxZieb helped me figure this out wonderfully last night.
It ended up being a closure issue that was solved by using an IIFE.

Here is the working code:

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
                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() {
            cancelAnimationFrame(animationRequest);
            hypeDocument.triggerCustomBehaviorNamed('PlayPauseButtonBackward' + nr);
            if (window.autoPlay) {
                window.transitionScene();
            }
        },

    });
})(nr);
}

Great to see such a practical (and in this case necessary) use for closures.
Thanks so much for the help from both you and MaxZieb.
Have learned a ton.

All the best and thanks again!

2 Likes

Glad it helped! Closures are a bit old school these days but do the job just fine. In modern Javascript / ES5 you can also use the forEach and bind approaches. For asynchronous approaches you can use Promises but I prefer it old school if not needed otherwise. The most recent JS allows for the new keywords let and const instead of var and offers better scoping that should also solve the problem but it requires ES6.

PS: Fun fact. The Hype Runtime also runs in a old school closure and keeps it’s internals private that way. Only hooks it exposes are the API (hypeDocument and symbolInstance) as return values.

2 Likes

Thanks @MaxZieb & Raleigh for an enlightening exchange! :zap:

For those that are interested in learning more about IIFE’s I found this article very informative (beginner level - but does assume You are conversant with the basics of JavaScript).

Thanks so much for sharing this article @JimScott! I found it very informative (and just my speed). Nice to see such a complete view of where IFFE’s exist in the grand scheme of things in the JavaScript world. Definitely understand them much better now :slight_smile:

1 Like

Thanks for this and the Hype fun fact - that’s very cool that Hype runtime incorporates old school closures. I can see why! Also, I had never even heard of promises until you mentioned them. Very cool!