<<button "[]">><<stopallsound>><</button>>\s\n<<button "||">><<pauseallsound>><</button>>\s\n<<button "<)">><<quieter>><</button>>\s\n<<button "<)))">><<louder>><</button>>\s\n<<button "|>">><<playsound $background_music 0.5>><</button>>\s\n<<button "Loop simple">><<loopsound $background_music 0.5>><</button>>\s\n<<button "Loop w/crossfade">><<fadeinsound $background_music 0.5 80>><</button>>
<<set $currentLoops = []>>\s\n<<set $background_music = "accordion.mp3">>\s\n<<set $footsteps = "footsteps.mp3">>\s\n[[<- loopsound, fadesound etc.|loopsound]]<div align="right">[[jumpscare]]</div>\s\n\n<h2>quieter, louder</h2>\s\n\n<<display "common controls">>\n\nThese macros adjust the overall volume of the story. The relative proportions of individual audio clips will be preserved.\n\nThese macros do NOT change the system volume. If the reader has their system volume down to two bars, these controls adjust the volume within that two-bar range.\n\nThis means the reader can have rainymoods.com playing in one tab at a certain volume, and then your story playing in another tab at a different volume.\n\n<h3>{{{<<quieter>>}}}</h3>\s\nReduces the story's overall volume by 1/10th of the reader's current system volume.\n\n<h3>{{{<<louder>>}}}</h3>\s\nIncreases the story's overally volume by 1/10th of the reader's current system volume.\n\nTogether, these options create a 10-unit range for the reader to adjust your story's volume (within the reader's system volume.)\n\n[[<- loopsound, fadesound etc.|loopsound]]<div align="right">[[jumpscare]]</div>
sub-Q Demo\nsqTwineSound \nv. 0.8.0
<<set $currentLoops = []>>\s\n<<set $background_music = "accordion.mp3">>\s\n<<set $footsteps = "footsteps.mp3">>\s\n[[<- pausesound, stopsound|pausesound]]<div align="right">[[quieter, louder ->|quieter]]</div>\s\n\n<h2>loopsound, unloopsound, fadeinsound, fadeinsounds, fadeoutsound, fadeoutsounds, playsounds, pauseallsound, stopallsound</h2>\s\n\n<<display "single loop controls">>\n<<display "multiple loop controls">>\n\nNote that you must keep track of what's playing--what you want to stop and start--on your own.\n\n\n<h3>{{{<<loopsound "accordion.mp3">>}}}</h3>\s\nThe parameters (in this order, please):\n\nParameters:\n\n<ul><li>REQUIRED: clipName (e.g. "backgroundMusic.mp3" or $backgroundMusic)</li><li>OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)</li><li>OPTIONAL: number of milliseconds to overlap/crossfade the loop (1000 ms by default, must be >= 10 ms if declared)</li></ul>\s\n\nStarts playing the given clip on repeat. Note that browsers will not necessarily play looping audio seamlessly.\n\n<h3>{{{<<unloopsound $footsteps>>}}}</h3>\s\nLet the given sound stop when it finishes its current loop (so the sound no longer repeats.)\n\n<h3>{{{<<fadeinsound "footsteps.mp3">>}}}</h3>\s\nIdentical to loopsound, but performs a fade in over the given overlap period.\n\n<h3>{{{<<fadeinsounds ["accordion.mp3", "footsteps.mp3"]>>}}}</h3>\s\nOR IDEALLY\n<h3>{{{<<set $spookySounds = [$moodMusic, $footSteps]>>}}}\n{{{<<fadeinsounds $spookySounds>>}}}</h3>\s\n\nFade in multiple sounds at once. Previous clip volume and overlap is remembered.\n\n<h3>{{{<<fadeoutsound $birdsong>>}}}</h3>\s\nIdentical to stopsound, but fades out the sound over 2 seconds.\n\n<h3>{{{<<fadeoutsounds ["moodMusic.mp3", "footsteps.mp3"]>>}}}</h3>\s\n OR IDEALLY\n<h3>{{{<<set $spookySounds = [$moodMusic, $footSteps]>>}}}\n{{{<<fadeoutsounds $spookySounds>>}}}</h3>\s\n\nFade out multiple sounds at once.\n\n<h3>{{{<<playsounds ["moodMusic.mp3", "footsteps.mp3"]>>}}}</h3>\s\n OR IDEALLY\n<h3>{{{<<set $spookySounds = [$moodMusic, $footSteps]>>}}}\n{{{<<playsounds $spookySounds>>}}}</h3>\s\n\nPlay multiple sounds at once (picking up where we left off)\n\nParameters:\n\n<ul><li>REQUIRED: clipName (e.g. "backgroundMusic.mp3" or $backgroundMusic)</li><li>OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)</li><li>OPTIONAL: number of milliseconds to overlap/crossfade the loop (1000 ms by default, must be >= 10 ms if declared)</li><li>OPTIONAL: true if you'd like to loop, false if no</li></ul>\s\n\n<h3>{{{<<pauseallsound>>}}}</h3>\nPauses all sounds at their current location. \n\n<h3>{{{<<stopallsound>>}}}</h3>\nStops all sounds immediately. If any stopped sound is played again, it will play from the beginning.\n\n[[<- pausesound, stopsound|pausesound]]<div align="right">[[quieter, louder ->|quieter]]</div>\s
All sample sounds acquired from <a href="http://soundbible.com/" target="_blank">soundbible.com</a>.\n\n<ul>\n<li><a href="http://soundbible.com/1954-Cat-Meow-2.html" target="_blank">Soft meow</a></li>\n<li><a href="http://soundbible.com/1509-Cat-Scream.html" target="_blank">Cat scream</a></li>\n<li><a href="http://soundbible.com/528-Accordion.html" target="_blank">Accordion Music</a></li>\n<li><a href="http://soundbible.com/2057-Footsteps-On-Cement.html" target="_blank">Footsteps</a></li>\n</ul>\n\n[[Back to start|Start]]
<<set $sound = "Yes">>\s\n<<set $currentLoops = []>>\s\n<<set $background_music = "accordion.mp3">>\s\n<<set $footsteps = "footsteps.mp3">>\s\n<h2>Welcome to the sub-Q Sound Macro Suite Demo</h2>\s\n<h3>Macros</h3>\s\n<ul><li>[[playsound]], [[updatevolume|playsound]]</li>\n<li>[[pausesound]], [[pauseallsound|pausesound]]</li>\n<li>[[loopsound]], [[unloopsound|loopsound]], [[fadeinsound|loopsound]], [[fadeinsounds|loopsound]], [[fadeoutsound|loopsound]], [[fadeoutsounds|loopsound]], [[playsounds|loopsound]], [[stopsound|loopsound]], [[stopallsound|loopsound]]</li>\n<li>[[quieter]], [[louder|quieter]]</li>\n<li>[[jumpscare]]</li></ul>\n<h3>Overview</h3>\s\nThis suite builds on Leon Arnott's incredibly rad HTML5 sound macros by adding\n<ul><li>story-wide volume control</li>\n<li>individual volume control</li>\n<li>seamless loops (with crossfade)</li>\n<li>fade duration control</li>\n<li>the ability to start multiple audio tracks at once</li></ul>\s\n\nA macro is a piece of code you can insert in a Twine passage like so:\n{{{<<playsound $thisSound>>}}}\n\nThis demo shows what the sub-Q suite of sound macros can be used to do.\n\n<h3>Quick Reference</h3>\s\n<<display "questions">>\s\n\n<h3>Before We Start</h3>\s\nIn this demo, I use "clip" and "track" to mean the playback of a specific audio file.\n\nThese macros recognize the following file formats, but be advised not every format works in every browser: ogg, mp3, wav, and webm.\n\nFor optimal performance of these macros, the <a href="http://www.motoslave.net/sugarcube/" target="_blank">latest Sugarcube story format is recommended</a>. This demo uses Sugarcube v1.0.3.\n\nIn the Sugarcube story format, you can pass your audio clip name[s] as a string (e.g. "meow.mp3") or a variable (e.g. $meow, assuming you have defined {{{<<set $meow = "meow.mp3">>}}}.) But in Sugarcane and Jonah, the audio clip names must be passed as strings (e.g. "meow.mp3"). You will see both these strings and variables used in the examples.\n\nNote that, even in Sugarcube, string variables are recommended because they help you do things like this:\n\n {{{<<set $heartbeat = "heartbeat.mp3">>}}}\n {{{<<set $spookyMusic = "spookyMusic.mp3">>}}}\n {{{<<set $spookyClips = [$heartbeat, $spookyMusic]>>}}}\n ...\n {{{<<fadeinsounds $spookyClips>>}}}\n {{{<<timedcontinue 2s>>}}}\n ...But then things weren't so scary...\n {{{<<fadeoutsounds $spookyClips>>}}}\n\n(Speaking of, if you'd like the "timedcontinue" macro, it's available in the <a href="http://www.glorioustrainwrecks.com/node/5462" target="_blank">Glorious Trainwrecks Replace Macro Set</a>!)\n\nHuge thanks to Leon Arnott for founding these sound macros.\n\nFeel free to save this demo locally and import the HTML into Twine to inspect it. Remember that the JavaScript in this demo and on github is "beautiful," so it's easy to read. Before you paste it into a Twine project, you may want to <a href="http://jscompress.com/" target="_blank">minimize it</a> for efficiency.\n\nReady? Let's go!\n\n-> [[Walk through|playsound]] all macros\n
<table>\s\n<tr><td>Fade in Music at 1/4 Volume w/ Tiny Fade/Crossfade: </td><td><<button "Fade In">><<set $currentLoops.push($background_music)>><<fadeinsound $background_music 0.25 80>><</button>></td></tr>\s\n<tr><td>Unloop Music: </td><td><<button "Unloop">><<set $currentLoops.splice($currentLoops.indexOf($background_music),1)>>\s\n<<unloopsound $background_music>><</button>></td></tr>\s\n<tr><td>Fade Out Music: </td><td><<button "Fade Out">><<set $currentLoops.splice($currentLoops.indexOf($background_music),1)>>\s\n<<fadeoutsound $background_music>><</button>></td></tr>\s\n<tr><td>Pause Music: </td><td><<button "||">><<pausesound $background_music>><</button>></td></tr>\n<tr><td>Resume Music Loop (pick up where we left off): </td><td><<button "|>">><<set $currentLoops.push($background_music)>><<playsound $background_music true>><</button>></td></tr>\s\n<tr><td>Stop Music: </td><td><<button "[]">><<set $currentLoops.splice($currentLoops.indexOf($background_music),1)>>\s\n<<stopsound $background_music>><</button>></td></tr>\s\n</table>\s
<<set $currentLoops = []>>\s\n<<set $loud_meow = "cat_scream.mp3">>\s\n[[<- quieter & louder|quieter]]<div align="right">[[Back to intro|Start]]</div>\s\n\n<h2>jumpscare</h2>\s\n{{{<<jumpscare>>}}} plays a clip at the current system volume, regardless of whether the reader turned the story volume down.\n\nThis macro does NOT change the system volume. If the reader has their system volume down to two bars, the jumpscare sound will play at the full two bars' worth of volume.\n\nEven so, PLEASE WARN YOUR READERS BEFORE USING JUMPSCARE. Providing a warning about possible jump scares at the beginning of your story will\n\n<ul>\s\n<li>Increase suspense</li>\n<li>Increase/maintain reader goodwill toward Twine in general</li>\n<li>Prevent lawsuits</li>\n<li>Prevent me from feeling guilty for providing this macro</li>\n</ul>\s\n\nPlease use jumpscare responsibly.\n\nJump Scare: <<button "Reeeowwwr!">><<jumpscare $loud_meow>><</button>>\n\nThat's everything!\n\n<<display "questions">>\n\nEnjoy, and happy Twining.\n\n\n\n[[<- quieter & louder|quieter]]<div align="right">[[Back to intro|Start]]</div>\n
<h4>More info about these macros</h4>\s\n<ul><li><a href="https://github.com/AteYourLembas/sqTwineSound" target="_blank">Get the source and more on github</a></li><li><a href="http://sub-q.com/questions" target="_blank">FAQ/Q&A</a> (on the sub-Q site)</li>\n<li><a href="http://sub-q.com/forums/topic/what-would-you-like-to-see-sqtwinesound-do-that-its-not-doing/" target="_blank">Feature Requests</a> (on the sub-Q site)</li></ul>
<<set $currentLoops = []>>\s\n<<set $background_music = "accordion.mp3">>\s\n<<set $footsteps = "footsteps.mp3">>\s\n[[<- playsound|playsound]]<div align="right">[[loopsound, fadesound, etc. ->|loopsound]]</div>\s\n\n<h2>pausesound, stopsound</h2>\s\n\n<<display "common controls">>\n\n<h3>{{{<<pausesound $background_music>>}}}</h3>\s\nPauses $background_music at its current location. \nUse {{{<<playsound $background_music>>}}} to resume it.\n\n\n<h3>{{{<<stopsound $backgroundMusic>>}}}</h3>\s\nStop the given sound immediately.\nIf the sound is played again, it will play from the beginning.\n\n\n\n[[<- playsound|playsound]]<div align="right">[[loopsound, fadesound, etc. ->|loopsound]]</div>\s
<<set $background_music = "accordion.mp3">>\s\n\nsqTwineSound\nsource & demo\nby Tory Hoke\n<a href="http://www.twitter.com/toryhoke" target="_blank">@toryhoke</a>\n<a href="http://www.toryhoke.com">toryhoke.com</a>\n\nBased on sound macros\nby Leon Arnott\nof <a href="http://www.glorioustrainwrecks.com" target="_blank">Glorious Trainwrecks</a>\n\n\nA little background music\n<p align="right"><<display "common controls">></p>\n\n[[Audio attributions|audio attribution]]\n\nDeveloped for\nTwine v 1.4.2\nSugarcube v 1.0+
<<set $soft_meow = "soft_meow.mp3">>\s\n<<set $background_music = "accordion.mp3">>\s\n[[<- Back to start|Start]]<div align="right">[[pausesound, stopsound ->|pausesound]]</div>\s\n\n<h2>playsound, updatevolume</h2>\s\n\nPlay Cat Meow at full available volume: <<button "|>">><<stopsound $soft_meow>><<playsound $soft_meow 1.0>><</button>>\nPlay Cat Meow at 1/2 volume: <<button "|>">><<stopsound $soft_meow>><<playsound $soft_meow 0.5>><</button>>\nPlay Cat Meow at 1/4 volume, with fade in, on loop: <<button "|>">><<stopsound $soft_meow>><<fadeinsound $soft_meow 0.25>><</button>>\nStop All Sound: <<button "[]">><<stopallsound>><</button>>\n\n<h3>{{{<<playsound "introMusic.mp3" 0.5 200 true>>}}}</h3>\s\n{{{<<playsound>>}}} lets you do a little bit of sound mixing.\n\n Parameters:\n\n<ul><li>REQUIRED: clipName (e.g. "backgroundMusic.mp3" or $backgroundMusic)</li><li>OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)</li><li>OPTIONAL: number of milliseconds to overlap/crossfade the loop (1000 ms by default, must be >= 10 ms if declared)</li><li>OPTIONAL: true if you'd like to loop, false if no</li></ul>\s\n\nSo this plays a clip once, no fade, at full global volume:\n\n {{{<<playsound $walla">>}}}\n\nThis fades in a quiet background $walla that will loop and crossfade with 2000 ms (2 seconds) of overlap:\n \n {{{<<playsound $walla 0.2 2000 true>>}}}\n\n This plays $meow once, no fade, at loudest available volume:\n \n {{{<<playsound $meow 1.0>>}}}\n\nThe last used volume will be remembered when you stop and restart the clip.\n\nNOTE: It's best practice to stop a sound before attempting to play it again. Any attempt to play a sound already playing is ignored.\s\n\n<h3>{{{<<updatevolume $backgroundMusic 0.5>>}}}</h3>\s\nGiven a decimal between 0.0 and 1.0, update the clip's volume proportion and the clip's actual volume.\n\nParameters:\n\n<ul><li>REQUIRED: clipName (e.g. "backgroundMusic.mp3" or $backgroundMusic)</li><li>REQUIRED: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)</li></ul>\s\n\n\nLoop Music at 0.50 volume: <<button "|>">><<fadeinsound $background_music 0.50>><</button>>\nChange volume to 0.10 volume: <<button "Volume 0.10">><<updatevolume $background_music 0.10>><</button>>\nChange volume to 0.90 volume: <<button "Volume 0.90">><<updatevolume $background_music 0.90>><</button>>\nStop Music: <<button "[]">><<stopsound $background_music>><</button>>\n\n[[<- Back to start|Start]]<div align="right">[[pausesound, stopsound ->|pausesound]]</div>\s
/* Your story will use the CSS in this passage to style the page.\nGive this passage more tags, and it will only affect passages with those tags.\nExample selectors: */\n\nbody {\n\t/* This affects the entire page */\n\tcolor: #FFF;\n\tbackground-color: #587e7e;\n\t\n}\n\n#ui-bar {\n width: 250px;\n\n}\n\n\n#passages {\n margin-left: 125px;\n width: 75%;\n min-height: 100%;\n padding-bottom: 0;\n margin-bottom: 0;\n border: 0;\n}\n\n.passage {\n\t/* This only affects passages */\n\tfont-size: medium;\n\t\n}\n.passage a {\n\t/* This affects passage links */\n\tcolor: #F2F5A9;\n\t\n}\n.passage a:hover {\n\t/* This affects links while the cursor is over them */\n\t\n\tcolor: #FFBF00;\n\n}
<table>\s\n<tr><td>Fade in Footsteps at 3/4 volume w/ 2 second fade: </td><td><<button "Fade in">><<set $currentLoops.push($footsteps)>><<fadeinsound $footsteps 0.75 2000>><</button>></td></tr>\s\n<tr><td>Fade out Footsteps: </td><td><<button "Fade out">><<set $currentLoops.splice($currentLoops.indexOf($footsteps),1)>>\s\n<<fadeoutsound $footsteps>><</button>></td></tr>\s\n<tr><td>Fade Out All Current Loops: </td><td><<button "Fade Out All">><<fadeoutsounds $currentLoops>><<set $currentLoops.splice($currentLoops.indexOf($background_music),1)>><<set $currentLoops.splice($currentLoops.indexOf($footsteps),1)>><</button>></td></tr>\s\n<tr><td>Pause All Sounds : </td><td><<button "*||*">><<pauseallsound>><</button>></td></tr>\s\n<tr><td>Resume Any Paused Sounds/Loops : </td><td><<button "Resume All">><<playsounds $currentLoops>><</button>></td></tr>\s\n<tr><td>Stop All Sound : </td><td><<button "*[]*">><<stopallsound>><<set $currentLoops.splice($currentLoops.indexOf($background_music),1)>><<set $currentLoops.splice($currentLoops.indexOf($footsteps),1)>><</button>></td></tr>\s\n</table>
/*\nsqTwineSound HTML5 Sound Macro Suite\nCopyright 2014 Tory Hoke\n\nProgram URI: http://www.sub-q.com/plugins/sqTwineSound/\nDescription: Sound macros for Twine creations, including controls for volume, fade interval, and playing multiple tracks at once.\nVersion: 0.8.0\nAuthor: Tory Hoke\nAuthor URI: http://www.toryhoke.com\nLicense: GNU General Public License\nLicense URI: http://www.opensource.org/licenses/gpl-license.php\nRepository: https://github.com/AteYourLembas/sqTwineSound\nFAQ / Q & A: http://sub-q.com/questions (password: ThinkVast)\nBug Reports/Feature Requests: http://sub-q.com/forums/topic/what-would-you-like-to-see-sqtwinesound-do-that-its-not-doing/ (password: ThinkVast)\n\n sub-Q.com is password-protected while it's in beta (until January 2015.)\n Please kick the tires and report any issues with the website\n via the sub-Q.com Contact form.\n\n\nThis program based on:\nTwine: HTML5 sound macros by Leon Arnott of Glorious Trainwrecks\nthe source and influence of which appear under a Creative Commons CC0 1.0 Universal License\n\nThis program uses\n\n easeInOutSine()\n Copyright © 2001 Robert Penner\n All rights reserved.\n \n As distributed by Kirupa\n http://www.kirupa.com/forum/showthread.php?378287-Robert-Penner-s-Easing-Equations-in-Pure-JS-(no-jQuery)\n \n Open source under the BSD License. \n \n \n Redistribution and use in source and binary forms, with or without modification, \n are permitted provided that the following conditions are met:\n \n Redistributions of source code must retain the above copyright notice, this list of \n conditions and the following disclaimer.\n Redistributions in binary form must reproduce the above copyright notice, this list \n of conditions and the following disclaimer in the documentation and/or other materials \n provided with the distribution.\n \n Neither the name of the author nor the names of contributors may be used to endorse \n or promote products derived from this software without specific prior written permission.\n \n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY \n EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE\n GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED \n AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED \n OF THE POSSIBILITY OF SUCH DAMAGE. \n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n \nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\n*/\n\n(function () {\n version.extensions.soundMacros = {\n major: 0,\n minor: 8,\n revision: 0\n };\n\n var globalVolume = 1.0;\n var updateInterval = 10; //Update sound volume, etc. once every 10 ms\n var defaultOverlap = 1000; //Default track overlap is 1000 ms\n var minVolume = 0.01; // Minimum possible volume -- 0 is mute, so we want somethings slightly above that\n var soundInterval = 0.1; // Creates an interval of 1/10 creates ten stages of loudness. Used by quieter/louder. Feel free to tweak\n var fileExtensions = ["ogg", "mp3", "wav", "webm"]; // Acceptable file extensions for audio\n var clips = {};\n\n // Convenience vars\n var clipNameLabel = "Clip Name";\n var overlapLabel = "Overlap";\n var volumeProportionLabel = "Volume Proportion";\n var loopLabel = "Loop?";\n\n\n //------------ Robert Penner via Kirupa math methods ----------\n //-------------------------------------------------------------\n\n function easeInOutSine(currentIteration, startValue, changeInValue, totalIterations) {\n return changeInValue / 2 * (1 - Math.cos(Math.PI * currentIteration / totalIterations)) + startValue;\n }\n\n\n //------------ End Math methods -------------------------------\n //-------------------------------------------------------------\n\n //------------- pausableTimeout ---------\n //--------------------------------------\n function pausableTimeout(func, params) {\n\n this.funcToRun = func;\n this.waitStartTime = -1;\n this.waitEndTime = -1;\n this.waitDuration = -1;\n\n this.activate = function(waitDuration) {\n\n if (this.pausedAt !== undefined) { this.waitDuration = this.timeRemaining(); }\n else if (waitDuration !== undefined) this.waitDuration = waitDuration;\n else if (this.waitDuration > -1 ) { console.log("Warning: No wait duration given to pausableTimeout. Using last specified one."); }\n else return; // Don't bother to start a loop with no wait duration\n\n this.waitStartTime = new Date().getTime();\n this.waitEndTime = new Date().getTime() + this.waitDuration;\n this.timeout = setTimeout(this.funcToRun, this.waitDuration, params);\n };\n\n this.deactivate = function() {\n this.pausedAt = this.timeElapsed();\n if (this.timeout !== undefined) clearTimeout(this.timeout);\n };\n\n this.stopAndClear = function() {\n if (this.pausedAt !== undefined) delete this.pausedAt;\n if (this.timeout !== undefined) { clearTimeout(this.timeout); delete this.timeout; }\n };\n\n this.timeElapsed = function() {\n return new Date().getTime() - this.waitStartTime;\n };\n\n this.timeRemaining = function() {\n if (this.pausedAt !== undefined) return this.waitDuration - this.pausedAt;\n return this.waitEndTime - new Date().getTime();\n };\n }\n //------------- /pausableTimeout --------\n //--------------------------------------\n\n\n //------------- sqAudio ----------------\n //--------------------------------------\n function sqAudio(fullPath, clipName, fileExt) {\n\n this.fullPath = fullPath;\n this.clipName = clipName; // Let a clip know its own name\n this.fileExt = fileExt;\n\n // Defaults\n this.volumeProportion = 1.0; // By default, full volume\n this.overlap = defaultOverlap; // By default, defaultOverlap ms\n this.isPlayable = false; // Assume audio is not playable\n this.looping = false; // Assume audio not looping\n this.alternate = false;\n this.mainAudio = new Audio();\n this.partnerAudio = new Audio();\n\n this.mainAudio.setAttribute("src", this.fullPath);\n if (this.mainAudio.canPlayType) {\n for (var i = -1; i < fileExtensions.length; i += 1) {\n if (i >= 0) fileExt = fileExtensions[i];\n if (this.mainAudio.canPlayType("audio/" + fileExt)) break;\n }\n if (i < fileExtensions.length) {\n this.mainAudio.interval = null;\n this.partnerAudio.setAttribute("src", this.fullPath);\n this.partnerAudio.interval = null;\n this.isPlayable = true;\n\n } else {\n console.log("Browser can't play '" + this.clipName + "'");\n }\n } \n\n // Convenience method for getting duration\n // TODO : protect this against audio not being loaded yet\n //\n this.getDuration = function () {\n\n return this.mainAudio.duration;\n };\n\n // Get what we consider the current audio track\n //\n this._getActiveAudio = function() {\n return (this.alternate) ? this.partnerAudio : this.mainAudio;\n };\n\n // Get what we consider the idle audio track\n //\n this._getIdleAudio = function() {\n return (this.alternate) ? this.mainAudio : this.partnerAudio;\n };\n\n\n\n // Perform fade on specified audio\n // Use ease\n //\n this.__fadeSound = function(audioObj, fadeIn) {\n\n var startVolume = fadeIn ? 0 : globalVolume * this.volumeProportion;\n var deltaVolume = globalVolume * this.volumeProportion * (fadeIn ? 1 : -1);\n\n //alert("__fadeSound! fadeIn " + fadeIn + ", globalVolume " + globalVolume + ", volProp " + this.volumeProportion + " startVol " + startVolume + " deltaVolume " + deltaVolume);\n\n // Handy vars for easing\n var totalIterations = this.overlap/updateInterval;\n var currentIteration = 1;\n\n audioObj.interval = setInterval(function() {\n\n //Use easing to prevent sound popping in or out\n //\n var desiredVolume = easeInOutSine(currentIteration, startVolume, deltaVolume, totalIterations);\n \n //alert("Well desiredVol is " + desiredVolume + " cos currIter " + currentIteration + " startVol " + startVolume + " delta vol " + deltaVolume + " total iter " + totalIterations);\n //This should never happen, but if it does, skip the fade\n if (isNaN(desiredVolume)) {\n audioObj.volume = startVolume + deltaVolume;\n console.log("There was a problem with the fade. Possibly overlap " + this.overlap + " is shorter than updateInterval " + updateInterval + "? ");\n } else {\n audioObj.volume = desiredVolume;\n }\n currentIteration += 1;\n \n if (audioObj.volume === (startVolume + deltaVolume)) { \n //alert("Grats! You reached your destination of " + audioObj.volume); \n clearInterval(audioObj.interval); \n }\n\n //This effectively stops the loop and poises the volume to be played again\n //That way the clip isn't needlessly looping when no one can hear it.\n if (audioObj.volume === 0) {\n audioObj.pause();\n audioObj.currentTime = 0;\n }\n }, updateInterval);\n\n };\n\n\n // Manages starting one loop before the last play has ended\n // and cross-fading the ends\n //\n this._crossfadeLoop = function(params) {\n\n var sqAudioObj = params[0];\n var currAudioObj = params[1];\n\n // Let loop expire if no longer looping\n //\n if (!sqAudioObj.looping) { return; }\n\n var nextAudioObj = sqAudioObj.alternate ? sqAudioObj.mainAudio : sqAudioObj.partnerAudio;\n sqAudioObj.alternate = !sqAudioObj.alternate;\n\n // Don't even bother with crossfade if there's no overlap\n if (sqAudioObj.overlap !== undefined && sqAudioObj.overlap > 1) {\n\n // fade out current sound\n //\n sqAudioObj._fadeSound(currAudioObj, false);\n\n // And fade in our partner\n //\n //nextAudioObj.volume = 0; \n //if (nextAudioObj.currentTime > 0) nextAudioObj.currentTime = 0;\n //nextAudioObj.play();\n sqAudioObj._fadeSound(nextAudioObj, true);\n\n }\n else {\n sqAudioObj.updateVolume(); \n nextAudioObj.currentTime = 0;\n nextAudioObj.play();\n }\n\n // Kick off the next timer to crossfade\n // Might as well garbage collect the old crossfadeTimeout, too.\n //\n //if (sqAudioObj.crossfadeTimeout !== undefined) { sqAudioObj.crossfadeTimeout.stopAndClear(); delete sqAudioObj.crossfadeTimeout; }\n //if (isNaN(sqAudioObj.getDuration())) { throwError("Can't loop because duration is not known (audio not loaded, probably not found.)"); return; }\n //sqAudioObj.crossfadeTimeout = new pausableTimeout(sqAudioObj._crossfadeLoop, [sqAudioObj, nextAudioObj]); \n //sqAudioObj.crossfadeTimeout.activate(sqAudioObj.getDuration()*1000-sqAudioObj.overlap);\n\n };\n\n\n this._fadeSound = function(activeAudioObj, fadeIn) {\n\n // Set the goal volume as a proportion of the global volume\n // (e.g. if global volume is 0.4, and volume proportion is 0.25, overall the goal volume is 0.1)\n //\n var goalVolume = globalVolume * this.volumeProportion;\n if (activeAudioObj.interval) clearInterval(activeAudioObj.interval);\n if (fadeIn) {\n if (activeAudioObj.currentTime > 0) activeAudioObj.currentTime = 0;\n activeAudioObj.volume = 0; \n this.loop();\n\n } else {\n\n if (!activeAudioObj.currentTime) return;\n activeAudioObj.volume = goalVolume;\n activeAudioObj.play();\n }\n this.__fadeSound(activeAudioObj, fadeIn);\n\n };\n\n\n // Fade sound on whatever the active audio is\n //\n this.fadeSound = function(fadeIn) {\n if (fadeIn) {\n this.stopAndClear();\n this.looping = true;\n }\n else this.looping = false;\n this._fadeSound(this._getActiveAudio(), fadeIn);\n };\n\n // Update volume proportion and volume of both audio clips\n //\n this.setVolumeProportion = function(volumeProportion) {\n this.volumeProportion = volumeProportion;\n };\n\n // Update volume of active audio clips (assumes vol proportion and global vol already set)\n //\n this.updateVolume = function() {\n\n //alert("about to set vol to " + globalVolume + " x " + this.volumeProportion);\n this._getActiveAudio().volume = globalVolume * this.volumeProportion;\n };\n\n // Play the current audio object and reactivate any paused timer\n //\n this.play = function(loop) {\n\n //If it's a loop we want, just loop and don't make a big deal out of it\n if (loop) this.loop();\n\n else {\n\n var activeAudioObj = this._getActiveAudio();\n if (activeAudioObj) { \n activeAudioObj.play();\n }\n }\n };\n\n // Pause whatever audio is currently playing and pause the timer, too\n //\n this.pause = function() {\n if (this.crossfadeTimeout !== undefined) this.crossfadeTimeout.deactivate();\n this._getActiveAudio().pause();\n };\n\n // Stop whatever audio is currently playing and dump the timer\n //\n this.stopAndClear = function() {\n var activeAudioObj = this._getActiveAudio();\n activeAudioObj.pause();\n if (activeAudioObj.currentTime > 0) activeAudioObj.currentTime = 0;\n if (this.crossfadeTimeout !== undefined) { this.crossfadeTimeout.stopAndClear(); delete this.crossfadeTimeout; }\n };\n\n\n // Loop the track\n //\n this.loop = function() {\n\n this.looping = true;\n var activeAudioObj = this._getActiveAudio();\n\n // Create new timeout if one does not already exist; otherwise just reuse the existing one\n //\n this.crossfadeTimeout = (this.crossfadeTimeout === undefined) ? new pausableTimeout(this._crossfadeLoop, [this, activeAudioObj]) : this.crossfadeTimeout; \n if (isNaN(this.getDuration())) { return throwError("Can't loop because duration is not known (audio not loaded, probably not found.)"); }\n this.crossfadeTimeout.activate((this.getDuration()*1000)-this.overlap);\n activeAudioObj.play();\n };\n\n\n }\n //------------ /sqAudio ----------------\n //--------------------------------------\n\n\n\n /***********************************************************\n * MAIN METHOD\n /***********************************************************\n /\n / Here be monsters. Proceed with caution.\n /\n */\n\n // Verify that the audio can be played in browser\n //\n function parseAudio(c) {\n\n var d = c.exec(div.innerHTML); // returns list of form ["url/to/audio.fileType",/to/audio,fileType]\n\n while(d) {\n if (d) {\n if (!clips.hasOwnProperty(d[1])) {\n\n var parser = document.createElement('a');\n parser.href = d[1].toString();\n var pathnameSubstrings = parser.pathname.split("/");\n var clipName = pathnameSubstrings[pathnameSubstrings.length-1];\n var sqAudioObj = new sqAudio(parser.href + "." + d[2].toString(), clipName, d[2].toString());\n if (sqAudioObj.isPlayable) { clips[clipName] = sqAudioObj;}\n }\n }\n d = c.exec(div.innerHTML); // yes, we could just do a do/while, but some envs don't like that\n }\n }\n\n // Parse all used audio file names\n // Use whatever store area element is available in the story format\n //\n var storeElement = (document.getElementById("store-area") ? document.getElementById("store-area") : document.getElementById("storeArea"));\n var div = storeElement.firstChild;\n while (div) {\n var b = String.fromCharCode(92);\n var q = '"';\n var re = "['" + q + "]([^" + q + "']*?)" + b + ".(" + fileExtensions.join("|") + ")['" + q + "]";\n parseAudio(new RegExp(re, "gi"));\n div = div.nextSibling;\n }\n /***********************************************************\n * END MAIN METHOD\n /***********************************************************/\n\n\n\n /***********************************************************\n * SUPPORTING FUNCTIONS FOR THE MACROS\n /***********************************************************\n /\n / Here be monsters.\n /\n */\n\n // Given the clipName, get the active soundtrack\n //\n function getSoundTrack(clipName) {\n clipName = cleanClipName(clipName.toString());\n if (!clips.hasOwnProperty(clipName)) { return throwError("Given clipName " + clipName + " does not exist in this project. Please check your variable names."); }\n return clips[clipName];\n\n }\n\n\n // Centralized function for sound fading\n //\n function fadeSound(clipName, fadeIn) {\n\n var soundtrack = getSoundTrack(clipName);\n if (soundtrack === "undefined") { return throwError("audio clip " + clipName + " not found"); } \n soundtrack.fadeSound(fadeIn);\n \n }\n\n\n // Adjust the volume of ALL audio in the page\n //\n function adjustVolume(direction) {\n\n // Note soundInterval and minVolume are declared globally (at top of the script)\n var maxVolume = 1.0; // This is native to JavaScript. Changing will cause unexpected behavior\n globalVolume = Math.max(minVolume, Math.min(maxVolume, globalVolume + (soundInterval * direction)));\n for (var soundIndex in clips) {\n if (clips.hasOwnProperty(soundIndex)) {\n clips[soundIndex].updateVolume();\n }\n }\n }\n\n // Common argument management\n // Because of the total expected arguments (one string, one float, one int, one boolean)\n // This method attempts to be forgiving of sequence. \n // Be advised if there were even one more argument, it probably couldn't be so forgiving anymore!\n //\n function manageCommonArgs(func, requiredArgs) {\n\n // Look at the list of available arguments, clean them up, and take the first one of each desired type:\n // Recreate the arguments as a list in required sequence [clipName, volumeProportion, overlap, loop]\n\n var clipName;\n var volumeProportion;\n var overlap;\n var loop;\n\n for (var i = 0; i < func.args.length; i++) {\n switch (typeof func.args[i]) {\n case "string" :\n if (clipName === undefined) clipName = func.args[i].toString();\n break;\n case "number" :\n var tempNum = parseFloat(func.args[i]);\n if (volumeProportion === undefined && tempNum <= 1.0) volumeProportion = tempNum;\n else if (overlap === undefined && tempNum >=updateInterval) overlap = tempNum; \n break;\n case "boolean" :\n if (loop === undefined) loop = func.args[i];\n break;\n }\n }\n\n for (var requiredArg in requiredArgs) {\n if (requiredArgs.hasOwnProperty(requiredArg)) {\n switch (requiredArg) {\n case clipNameLabel :\n if (clipName === undefined) { return throwError("No audio clip name specified."); } \n break;\n case volumeProportionLabel :\n if (volumeProportion === undefined || volumeProportion > 1.0 || volumeProportion < 0.0) { return throwError("No volume proportion specified (must be a decimal number no smaller than 0.0 and no bigger than 1.0.)"); }\n break;\n case overlapLabel :\n if (overlap === undefined) { return throwError("No fade duration specified (must be a number in milliseconds greater than + " + updateInterval + " ms.)"); }\n break;\n case loopLabel :\n if (loop === undefined) { return throwError("No loop flag provided (must be a boolean, aka true or false.)"); }\n break;\n }\n }\n }\n return [clipName, volumeProportion, overlap, loop];\n }\n\n // Get the clipName up to the . if a . exists, otherwise do no harm\n //\n function cleanClipName(clipName) {\n\n var parser = document.createElement('a');\n parser.href = clipName.toString();\n var pathnameSubstrings = parser.pathname.split("/");\n clipName = pathnameSubstrings[pathnameSubstrings.length-1];\n return clipName.lastIndexOf(".") > -1 ? clipName.slice(0, clipName.lastIndexOf(".")) : clipName;\n }\n\n\n /***********************************************************\n * END SUPPORTING FUNCTIONS FOR THE MACROS\n /***********************************************************/\n\n\n\n /***********************************************************\n /***********************************************************\n * MACROS\n /***********************************************************\n /***********************************************************\n */\n\n /* updatevolume\n \n Given a decimal between 0.0 and 1.0, \n updates the clip's volume proportion and the clip's actual volume\n \n */\n macros.add("updatevolume", {\n handler: function () {\n \n var args = manageCommonArgs(this, [clipNameLabel, volumeProportionLabel]);\n var soundtrack = getSoundTrack(this.args[0]);\n soundtrack.setVolumeProportion(args[1]);\n soundtrack.updateVolume();\n }\n });\n\n /** playsound \n\n This version of the macro lets you do a little bit of sound mixing.\n \n Parameters:\n\n REQUIRED: clipName \n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (0 ms by default)\n OPTIONAL: true if you'd like to loop, false if no\n \n \n */\n macros.add("playsound", {\n handler : function () {\n\n var args = manageCommonArgs(this, [clipNameLabel]);\n\n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion;\n soundtrack.overlap = args[2] !== undefined ? args[2] : defaultOverlap;\n var loop = args[3] !== undefined ? args[3] : false;\n soundtrack.setVolumeProportion(volumeProportion);\n soundtrack.updateVolume();\n soundtrack.play(loop); \n }\n });\n\n\n /* playsounds\n \n Play multiple sounds at once (picking up where we left off)\n If you give it no sounds to play, it quietly ignores the command.\n\n Parameters:\n\n OPTIONAL: clipName\n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade (0 ms by default)\n OPTIONAL: true if you'd like to loop, false if no\n \n /\n */\n macros.add("playsounds", {\n handler: function () {\n\n var clipNameString = this.args[0];\n if (this.args[0] === undefined || this.args[0] == "") return;\n clipNameString = this.args[0].toString();\n if (clipNameString == "[]") return;\n var clipNames = clipNameString.split(",");\n if (clipNames.length < 1) return;\n var args = manageCommonArgs(this);\n for (var index = 0; index < clipNames.length; index++) {\n var soundtrack = getSoundTrack(cleanClipName(clipNames[index]));\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion;\n soundtrack.overlap = args[2] !== undefined ? args[2] : defaultOverlap;\n var loop = args[3] !== undefined ? args[3] : false;\n soundtrack.setVolumeProportion(volumeProportion);\n soundtrack.updateVolume();\n soundtrack.play(loop); \n }\n }\n });\n\n\n\n /* pausesound\n \n Pauses clip at its current location. \n Use playsound to resume it.\n\n Parameters:\n\n REQUIRED: clipName\n\n */ \n macros.add("pausesound", {\n handler: function() {\n var args = manageCommonArgs(this, [clipNameLabel]); \n getSoundTrack(this.args[0]).pause();\n }\n });\n\n\n /* <<pauseallsound>> \n \n Pauses all sounds at their current location. \n \n If you'd like the option to start multiple sounds,\n take a look at <<playsounds>> and <<fadeinsounds>>\n */ \n macros.add("pauseallsound", {\n handler: function () {\n for (var clipName in clips) {\n if (clips.hasOwnProperty(clipName)) {\n getSoundTrack(clipName).pause();\n }\n }\n }\n });\n\n /* stopsound\n \n Stop the given sound immediately\n If the sound is played again, it will play from the beginning\n \n Parameters:\n\n REQUIRED: clipName \n */ \n macros.add("stopsound", {\n handler: function() {\n var args = manageCommonArgs(this, [clipNameLabel]); \n getSoundTrack(this.args[0]).stopAndClear();\n }\n });\n\n\n /* <<stopallsound>>\n \n Stops all sounds immediately.\n If any stopped sound is played again, it will play from the beginning\n \n If you'd like the option to start multiple sounds,\n take a look at <<playsounds>> and <<fadeinsounds>>\n */ \n macros.add("stopallsound", {\n handler: function () {\n for (var clipName in clips) {\n if (clips.hasOwnProperty(clipName)) {\n if (clips[clipName] !== undefined) clips[clipName].stopAndClear();\n }\n }\n }\n });\n\n /* loopsound\n \n Starts playing the given clip on repeat.\n Note that browsers will not necessarily play looping audio seamlessly.\n For seamless audio, use a fade duration/overlap (third parameter) greater than 1 millisecond\n (Well, you probably want something more perceptibe than 1 millisecond!)\n \n Parameters:\n\n REQUIRED: clipName \n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (0 ms by default)\n */ \n macros.add("loopsound", {\n handler: function () {\n \n var args = manageCommonArgs(this, [clipNameLabel]);\n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion;\n soundtrack.overlap = args[2] !== undefined ? args[2] : defaultOverlap;\n soundtrack.setVolumeProportion(volumeProportion);\n soundtrack.updateVolume();\n soundtrack.loop();\n }\n });\n\n\n /* unloopsound \n \n Let the given sound stop when it finishes its current loop\n (so the sound no longer repeats.)\n\n Parameters:\n\n REQUIRED: clipName \n\n */ \n macros.add("unloopsound", {\n handler: function () {\n var args = manageCommonArgs(this, [clipNameLabel]); \n getSoundTrack(this.args[0]).looping = false;\n }\n });\n\n\n /* fadeinsound\n \n Identical to loopsound, but fades in the sound over 2 seconds.\n\n Parameters:\n\n REQUIRED: clipName\n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (defaults to clip's last set overlap)\n\n */\n macros.add("fadeinsound", {\n handler: function () {\n\n var args = manageCommonArgs(this, [clipNameLabel]);\n \n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion; \n soundtrack.overlap = args[2] !== undefined ? args[2] : soundtrack.overlap;\n soundtrack.volumeProportion=volumeProportion;\n soundtrack.fadeSound(true);\n }\n });\n\n /* fadeinsounds\n\n Fade in multiple sounds at once.\n \n Parameters:\n\n REQUIRED: clipNames as list \n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (defaults to clip's last set overlap)\n \n */\n macros.add("fadeinsounds", {\n handler: function () {\n\n var clipNameString = this.args[0];\n if (this.args[0] === undefined || this.args[0] == "") return;\n clipNameString = this.args[0].toString();\n if (clipNameString == "[]") return;\n var clipNames = clipNameString.split(",");\n if (clipNames.length < 1) return;\n\n var args = manageCommonArgs(this);\n\n for (var index = 0; index < clipNames.length; index++) {\n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion; \n soundtrack.overlap = args[2] !== undefined ? args[2] : soundtrack.overlap;\n soundtrack.volumeProportion=volumeProportion;\n soundtrack.fadeSound(true); \n }\n }\n });\n\n /* fadeoutsound\n \n Identical to stopsound, but fades out the sound over the stored fade duration (overlap).\n \n Parameters:\n\n REQUIRED: clipName \n\n */\n macros.add("fadeoutsound", {\n handler: function () {\n var args = manageCommonArgs(this, [clipNameLabel]); \n fadeSound(this.args[0].toString(), false);\n }\n });\n\n\n /* fadeoutsounds\n \n Fade out multiple sounds at once.\n If you give it no sounds to play, it quietly ignores the command.\n\n Parameters:\n\n REQUIRED: clipNames as list \n \n */\n macros.add("fadeoutsounds", {\n handler: function () {\n\n var clipNameString = this.args[0];\n if (this.args[0] === undefined) return;\n clipNameString = this.args[0].toString();\n if (clipNameString == "[]") return;\n var clipNames = clipNameString.split(",");\n if (clipNames.length < 1) return;\n\n for (var index = 0; index < clipNames.length; index++) {\n fadeSound(cleanClipName(clipNames[index]), false);\n }\n }\n });\n\n\n /* <<quieter>>\n \n Reduces the story's globalVolume by 1/10th of the reader's system volume.\n Thus creates a 10-unit volume range for the story\n \n */\n macros.add("quieter", {\n handler: function () {\n adjustVolume(-1);\n }\n });\n\n /* <<louder>>\n \n Increases the story's globalVolume by 1/10th of the reader's system volume.\n Thus creates a 10-unit volume range for the story\n \n */\n macros.add("louder", {\n handler: function () {\n adjustVolume(1);\n }\n });\n\n\n /* jumpscare\n \n Play the clip at maximum story volume\n Don't affect any stored volume options\n PLEASE GIVE THE READER A STARTLE WARNING BEFORE USING THIS.\n \n */\n macros.add("jumpscare", {\n handler: function () {\n var args = manageCommonArgs(this, [clipNameLabel]);\n var soundtrack = getSoundTrack(this.args[0]);\n soundtrack.setVolumeProportion(1.0);\n soundtrack.updateVolume();\n soundtrack.play();\n }\n });\n\n /***********************************************************\n * END MACROS\n /***********************************************************/\n\n\n\n}());\n\n// You read the whole thing! THAT'S PRETTY RAD. Keep up the good work, and happy Twining.\n\n