Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

My advanced version of playsound macro. Testers and comments are welcome

yunyun
edited February 2016 in Workshop
The javascript code is here.
Based on http://www.glorioustrainwrecks.com/node/5061 (written by Leon Arnott), but has more possibilities and some significant differences. Some macros added, some changed, and now it is possible to add an optional second parameter (where the first is the sound file name) which regulates the behavior of several macros.
Written and tested for Sugarcane format. Probably should work with the others, but I don't know ;)
So, this is the full list of macros (square brackets means that the parameter is optional, it's not a part of syntax):

<<playsound "file">>
Plays "file" once. Nothing changed here.

<<loopsound "file" [times]>>
Plays "file" "times" times - or infinitely, if the parameter if omitted. IMPORTANT: now it is played only in the current passage; when you leave it, the sound stops. Be advised, however, that the event handler gets the control only after the file is played to the end, so if the file is long, you may hear it long enough after quitting the passage. This can be fixed either by stopping it manually in every passage where the user can get from the initial one (definitely not a nice solution!), or by using prerender function which inserts <<stopallsound>> macro (see at the bottom of the file). However, for some passages you may want to keep the music from the previous ones. So, mark the passages where you want to mute any previous sounds immediately with "stopsound" tag, and for them it will be done automatically.

<<arealoopsound "file" [parameter]>>
A new one. You probably has areas in your game which consist of several passages but represent the same place of the game world. So you want to play one music (or other sound effects) in all of these passages. Mark them all with the same tag and use this macro. The "file" will be played infinitely (and without restarting when you change the passage) while you are moving across the passages which have the same tag as the first tag of the passage in which the macro was called, but will the stopped when you move to a passage without such tag (see the notice about playing after leaving above).
The parameter defines what exactly happens when you exit - and what will happen when you return to a passage with the same tag. If "parameter" is omitted, the sound stops completely, and you will need to restart it manually. If "parameter" is any positive number, the sound is only muted and will play again automatically. This may be very convenient, but remember that playing too many sound files simultaneously, even if they are muted, may become a burden to your browser and CPU.

<<pausesound "file">>, <<stopsound "file">> and <<unloopsound "file">>
Not changed (at least from the user's point of view). All do pretty the same, only "pause" and "unloop" don't reset "file" to the beginning.

<<fadeoutsound/fadeinsound "file" [seconds]>>
Now you can specify how long fading should take (2 seconds by default). "seconds" is a number (no "s" at the end). If file is being played already, it continues, otherwise it starts. Notice that fadeoutsound will eventually stop the file, overriding any previous loop settings for it.

<<setvolumesound "file" [level]>>
A new one. Allows, you guessed it, to set the sound volume for the specific file (no difference if it is currently played of not). If "level" is within 0..1 (borders included), it is treated as a fraction; if 1<level<=100, it is treated as percentage (no % sign is needed or allowed). So <<setvolumesound "file" 0.23>> and <<setvolumesound "file" 23>> do the same. If omitted or has other values, it means 1 (100% volume).

<<stopallsound>>
Not changed.

Additionally, if you want to play a sound in Twine as a function call (may be useful, e.g., if you need a sound when the user clicks a certain link), you may add this code to your Start passage:
<<silently>>
<<set $playsound=
function(track)
{track=track.slice(0, track.lastIndexOf(".")); macros["playsound"].soundtracks[track].play()}
>>
<<endsilently>>\
It will allow you to do things like this:
Switch 1 is up. [[Turn down|controls][$playsound("switchdown.wav");$switches[0]=0]]

Comments

  • I suppose users of the Twine 1 vanilla story formats might like this.

    As a suggestion, in your InArray function, don't use the for…in loop to iterate arrays. Use a standard for loop, with the 3-part syntax, to iterate arrays. For example:
    function InArray(array, value) {
    	for (var i = 0, len = array.length; i < len; ++i) {
    		if (array[i] == value) {
    			return true;
    		}
    	}
    
    	return false; 
    }
    

    Reasoning:
    Arrays in JavaScript are just objects (i.e. Array object) with some extra syntactical sugar added. What the for…in loop does is iterate over all of an object's enumerable properties (in arbitrary order). The basic enumerable properties on an array object are its indices, which is why using the for…in loop works at all. However, the for…in loop does not limit itself to the object's own properties and will iterate all enumerable properties from the property chain as well. You really do not want, or need, to be traversing an array's property chain just to iterate over its indices.

    Now, you could guard against enumerable properties on the property chain by using the hasOwnProperty() method to ensure that an iterated property is actually one of the array's own, however, that's extra work your loop, generally, doesn't need to be doing.
  • yunyun
    edited February 2016
    I knew that JS arrays are not regular arrays and, e.g, array[0] and array[2] may exist while array[1] may absent, that's why for...in is used. But if it scans unnecessary properties, you are probably right. Why are you using an extra variable "len" instead of for (var i=0; i<array.length; ++i), however?
    Meanwhile, I changed the code and added domacro method to allow all those macros to be called as a function in Twine this way:
    <<set $soundmacro=
    function(macro,paramstr)
    {macros[macro].domacro(macro, paramstr);}
    >>
    <<set $chimes=
    function(n)
    {state.history[0].variables["soundmacro"]("loopsound",'"1bell.wav" '+n);}
    >>
    <<set $chimes(12)>>
    
  • yun wrote: »
    Why are you using an extra variable "len" instead of for (var i=0; i<array.length; ++i), however?
    Local variables are generally faster to access than object properties and the value of array.length needs to be determined for each looping.

  • BTW, how to call this InArray() function from Twine? (Sugarcane again).
    I am trying the following and get a ReferenceError: InArray is not defined)
    <<set $chimes=
    function(n)
    {macros["playsound"].soundtracks["1bell"].volume=InArray(state.history[0].passage.tags,"salon")?1:0.5;
     $soundmacro("loopsound",'"1bell.wav" '+n);}
    >>
    
    (Everything else works.)
  • As a bit of a preface to my reply. Ideally, you should be using one of the standard library Array methods to do this, rather than hand-rolling your own search function. However, since the macro set and story formats you're dealing with support IE8 (sans polyfills), you're kind of hosed in that regard.

    yun wrote: »
    I knew that JS arrays are not regular arrays and, e.g, array[0] and array[2] may exist while array[1] may absent, that's why for...in is used. But if it scans unnecessary properties, you are probably right.
    The concept you're thinking of is known as a sparse array.

    You are unlikely to see a sparse array come out of the vanilla story formats (or most anything, really), so unless you know for a fact that you'll be dealing with them it's, generally, best to assume that any array you interact with will not be sparse.

    In later versions of the ECMAScript (JavaScript) standards, there are several more ways to deal with arrays, many of which handle sparse arrays better than for…in allows.

    yun wrote: »
    Why are you using an extra variable "len" instead of for (var i=0; i<array.length; ++i), however?
    So you aren't having to lookup the length property within the array stored within the array variable every time the loop conditional is checked. Caching the value is just a bit of a micro optimization.

    Normally, you shouldn't worry too much about accessing object properties—just go ahead do it. However, in situations where you're going to be reusing the same property value over and over again (e.g. within a loop), then, as long as the property isn't going to mutate on you, you may want to consider caching it to speed up access.

    Since InArray() iterates arrays, which are not being mutated, to search for a particular value, then caching the length of the array isn't a bad idea.
  • I thought there should be a standard method for checking if a value is in array, but brief googling showed that people use functions like my InArray. And of course I want as much cross-browser compatibility as possible.
    I thought accessing a property (not a method calculating something) and a single variable doesn't differ much. But even if it does, how many tags may a passage have? Usually no more than a couple. Saving several nanoseconds while looping them it not worth ;)

    Again, how to call InArray() defined in my script passage from Twine code? It says "InArray is not defined", see an example above.
  • yun wrote: »
    I thought there should be a standard method for checking if a value is in array...
    Did you look at the indexOf() method, or does than not do what you want?
    if (array.indexOf(value) != -1) {
    ... found the value in the array ...
    }
    
    yun wrote: »
    Again, how to call InArray() defined in my script passage from Twine code? It says "InArray is not defined", see an example above.
    Your issue is one of Scope, your InArray() function is declared in one local scope (a script tagged passage) and you are trying to use it in a different local scope (within an anonymous function) which is why you get the error message.

    You would have to make your InArray() function global for it to be seen everywhere, one common method used to do this is to define the function on the global window object although you could use the above indexOf() function instead which is already available on any array.

    The following is a copy of your InArray function defined on the window object.
    if (! window.InArray) {
    	window.InArray = function(array, value) {
    		for (var i=0; i<array.length; ++i) {
    			if(array[i]==value) return true;
    		}
    		return false;
    	};
    }
    
  • edited February 2016
    yun wrote: »
    I thought there should be a standard method for checking if a value is in array, but brief googling showed that people use functions like my InArray. And of course I want as much cross-browser compatibility as possible.
    Doing a brief search did you no favors here. greyelf has already linked <Array>.indexOf() and a future version (ES7) will add <Array>.includes().

    As I noted above, the Twine 1 vanilla story formats don't include any polyfills, however, so you probably shouldn't use <Array>.indexOf() and are better off sticking with your custom InArray() function.

    greyelf wrote: »
    Did you look at the indexOf() method, or does than not do what you want?
    if (array.indexOf(value) != -1) {
    ... found the value in the array ...
    }
    
    It does exactly what yun needs. However, <Array>.indexOf() doesn't exist in some of the older browsers supported by the Twine 1 vanilla story formats and the original version of these macros. If yun is trying to maintain compatibility, then using <Array>.indexOf() is a non-starter, unless they also include a polyfill for it.

    Speaking personally, I would go ahead and add a polyfill, so I could use the standard library method. However, I'm getting the impression that yun wouldn't be in favor of that.
  • However, <Array>.indexOf() doesn't exist in some of the older browsers ....
    I did not include that information in my previous comment because it is available in the linked page, which also includes a polyfill to support those older browsers.
  • Someone wrote that indexOf() was only for strings and I believed without checking :( Now I see it works even with my rather old browsers. IE8, however, does not support Audio, so I will not bother taking care about it at all. Normal people don't use it anyway ;)
    Thanks to both of you!
Sign In or Register to comment.