Howdy, Stranger!

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

Hover sounds

I'm trying to get a sound to play on mouse hover over links. I've managed to get it working for the UI Bar (sugarcube) save and restart options, but it plays for no other links. Here's the code I'm using:

$("a").mouseenter(function(){
$("<audio></audio>").attr({
'src':'sound/transition1.ogg',
'autoplay':'autoplay'
})
});
Taken from: https://css-tricks.com/play-sound-on-hover/

Comments

  • That will only attach a handler to the &lt;a&gt; elements existing in the page when it's executed.  If I understand what you want, you'll need to use a delegated handler.

    For example:

    $(document.body).on("mouseenter", "a", function () {
    $("<audio></audio>").attr({
    src : "sound/transition1.ogg",
    autoplay : "autoplay"
    });
    });
  • That works... actually a little too well. :)

    Is it possible to exclude particular links from that function?

    Like I have different styled links in the form of

    a
    as well as

    a.newlink
    Where I might go <span class="newlink">[[my link]] </span>

    I wouldn't want the sound on the newlink styled links in this example.


    Although, in practice it doesn't sound quote as good as I'd hoped, so, might not use it anyway.

  • NOTE: Something I should have mentioned before.  Since this is a delegated handler, you only want to execute it once at startup, so it should go in your Story JavaScript.  Do not use it in either the task objects or the PassageReady & PassageDone special passages.


    Claretta wrote:

    Is it possible to exclude particular links from that function?


    Yes.  You'd simply use a different selector, instead of &quot;a&quot;.


    Claretta wrote:

    Like I have different styled links in the form of

    a
    as well as

    a.newlink
    Where I might go <span class="newlink">[[my link]] </span>

    I wouldn't want the sound on the newlink styled links in this example.


    Your &lt;span&gt; example produces the following:
    span.newlink a
    Which, while it does allow you specifically target those &lt;a&gt; elements for something (styling, modification, whatever), does not work so well when you're trying to exclude them.  In particular, it would require you to filter the elements manually (which you do not want to do, if you can avoid it).  For example:

    $(document.body).on("mouseenter", "a", function () {
    if (!$(this).parent().is("span#newlink")) {
    $("<audio></audio>").attr({
    src : "sound/transition1.ogg",
    autoplay : "autoplay"
    });
    }
    });
    In this case, it would be better to use the &lt;a&gt; markup, rather than wrapping the wiki link markup in a &lt;span&gt;.  For example:
    <a class="newlink" data-passage="passage title">link text</a>
    Which produces the following:
    a.newlink
    After that, you could then modify the first line of my original example thus:

    $(document.body).on("mouseenter", "a:not(.newlink)", function () {
    $("<audio></audio>").attr({
    src : "sound/transition1.ogg",
    autoplay : "autoplay"
    });
    });
    Though, the following, which only handles #ui-bar menu links and story links (not bearing the newlink class), might be a bit better:

    $(document.body).on("mouseenter", "#ui-bar nav a, a.link-internal:not(.newlink)", function () {
    $("<audio></audio>").attr({
    src : "sound/transition1.ogg",
    autoplay : "autoplay"
    });
    });
  • Cheers for the detailed reply. Helps me understand it a bit better. :)
  • My last hurdle with getting this to work properly is installing a spam blocker - preventing the sound from playing more than once every two seconds (otherwise it just builds up if someone is a bit trigger happy with link hovers). I have tried using an "if" condition and a setTimeout like I have used to solve this problem in ActionScript in the past, but JavaScript has syntax I don't understand.

    Something like this:

    {
    var spam = 0;
    if (spam == 0) {
    $(document.body).on("mouseenter", ".start button", function () {
    $("<audio></audio>").attr({
    src : "sound/static.ogg",
    autoplay : "autoplay"
    });
    });
    spam = 1;
    setTimeout(function(){ spam = 0; }, 2000);
    }
    }

    Now that code doesn't work, but you can hopefully see what I'm trying to do. The problem lies with the reassigning the variable to 1 and then back to 0. The if statement works fine - if I initially define spam to 1, the sound will be blocked completely. The last two parts of the code, however, are not firing at all.

    I'm pretty sure I'm not using functions and variables properly.
  • You were on the right track to creating a debounce mechanism, you were simply doing it in the wrong place (you want to debounce the event handler, not the creation of said event handler, especially since you should only be creating the delegated event handler once).  That said, don't reinvent the wheel.  SugarCube includes the jQuery throttle/debounce library, so leverage that instead.  Try the following, which triggers the audio on an initial mouseenter event and then ignores subsequent instances of the event until a pause of the specified delay has occurred (i.e. once activated, the debounced function deactivates until no new instances of the event have occurred for the delay):

    $(document.body).on("mouseenter", ".start button", $.debounce(1000, true, function () {
    $("<audio></audio>").attr({
    src : "sound/static.ogg",
    autoplay : "autoplay"
    });
    }));
  • Works perfect, thank you!
  • I wanted to expand this to playing a random sound, and I managed to get it working all by myself. :P

    But I'm unsure of one little thing. The code I'm using is:

    $(document.body).on("mouseenter", ".start button", $.debounce(2000, true, function () {
    var sounds = [ "sound/radio1.ogg",
    "sound/radio2.ogg",];

    var soundFile = sounds[Math.floor(Math.random()*sounds.length)];

    $("<audio></audio>").attr({
    src : soundFile,
    autoplay : "autoplay"
    });
    }));

    But what I don't get is if I take out this semi colon at the end of the array, it works just as well:

    var sounds = [ "sound/radio1.ogg",
    "sound/radio2.ogg",];

    The semi colon was in the example I pulled this code from, but took it out after seeing an array TheMadExile created for image preloading didn't have one. So... semi colon not needed?
  • Claretta wrote:

    The semi colon was in the example I pulled this code from, but took it out after seeing an array TheMadExile created for image preloading didn't have one. So... semi colon not needed?


    I feel like you're confusing semi-colons (;) and commas (,).


    Commas are allowed, but unnecessary, after the final element/property in either array or object literals.  For example:

    // With final comma
    var sounds = [
    "sound/radio1.ogg",
    "sound/radio2.ogg",
    ];

    // Without final comma
    var sounds = [
    "sound/radio1.ogg",
    "sound/radio2.ogg"
    ];

    Semi-colons should always be used where appropriate.  JavaScript has an automatic semi-colon insertion feature (henceforth: ASI), which allows you, in theory, to exclude semi-colons in various places.  The problem is that the ASI rules are stupid and will happily insert semi-colons in places which will break your code.  And, even more unfortunately, these breakages might not even be syntax errors, so the bug might go undetected for quite a while.


    Also, you're reinventing the wheel again.  SugarCube provides both the &lt;Array&gt;.random() method and the either() function.  For example:

    // Using the random() method of the Array
    var soundFile = sounds.random();

    // Using the either() function
    var soundFile = either(sounds);
    You also don't need the extra variable for what you're doing, so my suggestion:

    $(document.body).on("mouseenter", ".start button", $.debounce(2000, true, function () {
    var sound = [
    "sound/radio1.ogg",
    "sound/radio2.ogg"
    ].random();
    $("<audio></audio>").attr({
    src : sound,
    autoplay : "autoplay"
    });
    }));
  • Thanks, that's all working.

    I now want to create a duplicate version of the sound macros specifically for music play with a 4 minute 30 second debounce in place so music tracks don't start overlapping each other. So I renamed all instances of "sound" to "music" and shoved a huge debounce at the front (not knowing much about javascript).

    The macros themselves work, but the debounce isn't debouncing. Can someone take a quick look and rearrange my debounce?

    This code is super overkill too, since I don't need anything but the <<playmusic>> macro, but left the rest in as I didn't know what to delete. I doubt it causes any problems, though I could  do with a <<fadeinmusic>> that doesn't loop, so might start looking at that.

    ($.debounce(270000, true, function() {
    "use strict";
    version.extensions['musicMacros'] = {
    major: 1,
    minor: 2,
    revision: 0
    };
    var p = macros['playmusic'] = {
    musictracks: {},
    handler: function(a, b, c, d) {
    var loop = function(m) {
    if (m.loop == undefined) {
    m.loopfn = function() {
    this.play();
    };
    m.addEventListener('ended', m.loopfn, 0);
    } else m.loop = true;
    m.play();
    };
    var s = eval(d.fullArgs());
    if (s) {
    s = s.toString();
    var m = this.musictracks[s.slice(0, s.lastIndexOf("."))];
    if (m) {
    if (b == "loadmusic" || b == "playmusic" || b == "loopmusic" || b == "fadeoutmusic" || b == "fadeinmusic") {
    if (m.readyState < HTMLAudioElement.HAVE_CURRENT_DATA) {
    m.preload = "auto";
    m.load();
    }
    }
    if (b == "playmusic") {
    m.play();
    } else if (b == "loopmusic") {
    loop(m);
    } else if (b == "pausemusic") {
    m.pause();
    } else if (b == "unloopmusic") {
    if (m.loop != undefined) {
    m.loop = false;
    } else if (m.loopfn) {
    m.removeEventListener('ended', m.loopfn);
    delete m.loopfn;
    }
    } else if (b == "stopmusic") {
    m.pause();
    m.currentTime = 0;
    } else if (b == "fadeoutmusic" || b == "fadeinmusic") {
    if (m.interval) clearInterval(m.interval);
    if (b == "fadeinmusic") {
    if (m.currentTime > 0) return;
    m.volume = 0;
    loop(m);
    } else {
    if (!m.currentTime) return;
    m.play();
    }
    var v = m.volume;
    m.interval = setInterval(function() {
    v = Math.min(1, Math.max(0, v + 0.005 * (b == "fadeinmusic" ? 1 : -1)));
    m.volume = Math.easeInOut(v);
    if (v == 0 || v == 1) clearInterval(m.interval);
    if (v == 0) {
    m.pause();
    m.currentTime = 0;
    m.volume = 1;
    }
    }, 30);
    }
    }
    }
    }
    };
    macros['loadmusic'] = p;
    macros['fadeinmusic'] = p;
    macros['fadeoutmusic'] = p;
    macros['unloopmusic'] = p;
    macros['loopmusic'] = p;
    macros['pausemusic'] = p;
    macros['stopmusic'] = p;
    macros['stopallmusic'] = {
    handler: function() {
    var s = macros.playmusic.musictracks;
    for (var j in s) {
    if (s.hasOwnProperty(j)) {
    s[j].pause();
    if (s[j].currentTime) {
    s[j].currentTime = 0;
    }
    }
    }
    }
    };

    var store = document.querySelector("tw-storydata");
    if (store == null) {
    store = document.querySelector("#store-area,#storeArea");
    }
    if (store == null) {
    return false;
    }
    store = store.firstChild;

    var fe = ["ogg", "mp3", "wav", "webm"];
    while (store != null) {
    var b = String.fromCharCode(92);
    var q = '"';
    var re = "['" + q + "]([^" + q + "']*?)" + b + ".(ogg|mp3|wav|webm)['" + q + "]";
    k(new RegExp(re, "gi"));
    store = store.nextSibling;
    }

    function k(c, e) {
    do {
    var d = c.exec(store.innerHTML);
    if (d &amp;&amp; !macros.playmusic.musictracks.hasOwnProperty(d[1])) {
    var a = new Audio();
    if (a.canPlayType) {
    for (var i = -1; i < fe.length; i += 1) {
    if (i >= 0) d[2] = fe[i];
    if (a.canPlayType("audio/" + d[2])) break;
    }
    if (i < fe.length) {
    a.src = d[1] + "." + d[2];
    a.interval = null;
    macros.playmusic.musictracks[d[1]] = a;
    } else console.log("Browser can't play '" + d[1] + "'");
    }
    }
    } while (d);
    }
    }()));
  • How about you explain how you want this to work first, because using debounce here is not the solution.  Do you simply want looping background music or something?  Randomized or not?
  • Sure. :) I explained what I wanted to do in this thread: http://twinery.org/forum/index.php/topic,2548.0.html

    But I found those solutions problematic because of the save game feature. If I simply set a track to start at one particular point, the player will probably miss the music loop if they loaded in past that trigger point (music does not trigger at the very start of the game, but about 6 passages in, so cannot just trigger on game start). And if the player loads game while a music track is still playing, they'll probably queue up another on top.

    So those required mucking around with the saved game config that I did not want to do.

    Debounce seemed to be the simplest way to do it, by putting a "playmusic" macro at the start of every passage, and ensuring that it could not trigger if music was already playing. It could then be randomised by using the random function within Twine itself.

    I could not use the existing unmodified sound macros to do this because they offer no protection against double playback of music tracks if I have 5-10 tracks to choose from (the user triggering two playback events within a short time, so having two tracks play on top of each other), and if I try to get round this by simply having all music in one huge file, it gets up to 30-50MB of MP3s. So, I:

    1. Want to be able to load each track independently
    2. Want to randomize playback to keep tracks fresh
    3. Don't want double playback to happen
    4. Want the tracks to load at appropriate times, which will probably mean not at game start, but on every load game use, as well as if the user turns the music off in the options, then on again

    It seems like a somewhat complex thing, sorry, a bit beyond usual music implementation. So was just trying to come up with the simplest, most direct way of doing it. It seemed that a playsound macro on every passage that I could tailor with <<if>> statements, then a timer running in the background to prevent double playback that could either stop music playback on its own, or alter the <<if>> variable, was the most fitting solution.
  • Okay.  What does your current music option code look like?
  • It's currently just the normal <<playsound>> macros out of the sound macro you updated.

    I currently have in key passages:

    <<if $music is 1>><<playsound "sound/musicfull.ogg">><</if>>
    This way the music will trigger playing and assuming the player continues moving through the story, when it stops, it should trigger again fairly soon. I don't use <<loopsound>> due to wanting more control than a simple looping of tracks provides.

    I then have in a passage called "music toggle" in my options area:

    <<if $music is 1>><<set $music to 0>><<stopsound "sound/musicfull.ogg">>Music is now off. To turn it back on, reactivate this control.
    <<elseif $music is 0>><<set $music to 1>><<playsound "sound/musicfull.ogg">>Music is now on. To turn it back off, reactivate this control.<</if>>
    This allows players to turn the music on/off at will.


    Currently to prevent double play I simply rely on the fact that the browser will not play more than one instance of the same file at once, so "musicfull.ogg" contains all my music tracks in sequence. So if a player runs across <<playsound "sound/musicfull.ogg">> while the music is already playing, it will not trigger. This allows me to spread <<playsound>> commands throughout the story to ensure a player will always hit them on load game or at a point when the music has finished playing, without fear of doubling playback.

    However, this is not ideal due to file size issues and the fact that they will always experience the same order of tracks. I would like to be able to separate this file into individual tracks to prevent the player from loading a huge 25MB+ file in at once, but cannot right now since my method relies on there only being one music file.
  • I have something which should work.  I may not be able to get it out for a day or two, however.
  • Thank you for your continued interest, it is much appreciated.
  • I am trying really hard to understand this thread. I am trying do a hover sound on a choice for a link in Twine 1. I have tried some of the code you guys have used but have not had any luck. Here is the sound I am trying to use: http://s3.amazonaws.com/SBI-phrases-mp3/say-hello-in-chinese.mp3 Any help is greatly appreciated!
  • @KBuck08
    Which Story Format are you using?
    The code in this thread is designed to work with SugarCube but it should be possible to convert it to work with a different story format.
  • I am using SugarCane. I am very new to this in case it wasn't obvious. Ha ha. I got so frustrated I used the amazon player in my game. *sigh* Someday I will learn :)
Sign In or Register to comment.