What's the best way of telling when a sound file has finished playing?

I'm putting together an application which plays a series of sound files in sequence; I want to be able to tell when a file has finished.

The obvious ways of doing this (checking for the currentTime() >= duration() and checking for !.isPlaying() && !.isPaused()) don't work - when a sound file finishes playing, currentTime() returns 0, and the second statement is also true when there's nothing playing at all.

I can probably sort this by tracking the currentTime and watching for a jump down from a non-zero value, but this seems like a hacky way of doing things. Have I missed something obvious or is there a feature that isn't documented that would solve my problem?

Relatedly, it seems like it would be useful for the soundFile to raise some sort of event when it finishes playing, or alternatively for its currentTime to remain equal to its duration rather than resetting to zero.

Answers

  • @dhmstark===

    • i had the same kind of problems using minim lib and saw that the isPlaying() sometimes work and sometimes fails: it seems to depend of the sound itself (mp3, wav, compressed, uncompressed...); never found a way to solve that except using the "hacky way" you suggest, getting currentTime() and putting a duration < (some millis) what was the duration returned...
    • i cannot remember which one (ESS, beads???) but i think that there is (was???) some lib with a method returning sound completion.
  • edited November 2015

    You should request some kinda "finishedPlayback" callback capability here:
    https://GitHub.com/processing/p5.js-sound/issues

  • @dhmstark - having tested a solution locally I went ahead and added an issue.

    I didn't figure it out in detail; but it looks to me like the sound library checks the length of the audio against how much has played internally to determine when it has finished; probably because media event browser support appears patchy at present...

    As far as I could see adding an optional callback should be trivial; though not knowing the inner complexities of the library I'll leave it to the authors to confirm ;)

  • Thanks for the comments and in particular to @blindfish for adding the issue! I'm never sure how to go about these things so it's really appreciated. :)

    I'll go with my hacky solution for now as at the very least it's working, but will keep an eye on that issue to see if it gets anywhere.

  • @dhmstark: The issue has been closed and an onended callback has been implemented :)

    Get the latest p5.js-sound. Add an onended callback to your sound file like so:

    var sound,
         myCallback = function() {
               console.info("sound finished");
           }
    
    function setup() {
      createCanvas(200, 200);
    
      // initialize sound 
      sound = loadSound('pop.mp3');
      sound.onended(myCallback);
    
    }
    
  • I wish they'd do the same for Processing's sound & video libraries too. [-O<
    It'd be cool if "p5.sound.js" had chainable methods too... :-\"

  • It'd be cool if "p5.sound.js" had chainable methods too

    Like: sound.play().onended(myCallback);?

    That's a pattern heavily used in jQuery. I've read criticisms claiming chaining can make it harder to debug your code; but it definitely makes it easier to read.

    The definite downside with adding sound.onended in setup, as I did in my example, is that you're going to have to look in two separate places (or three if you define a separate callback function instead of using an anonymous function) to figure out the result of playing a sound. Worse: you might forget that you attached the onended event and be wondering why something unexpected always happens after you play a sound...

    On reflection I should have made that point when therewasaguy suggested the separate method. Adding the callback as an argument to sound.play() makes more sense to me; but might have meant more work in terms of implementation since it would need careful thought around argument overloading...

  • edited November 2015

    Looking at p5.js eco-system, in general only loading functions got callbacks as parameters.
    Other types of callbacks are plugged in as optional later.
    Since play() isn't a loader, onended() ended up as a plugin callback event for the p5.sound object. :-B

  • edited November 2015

    Works wonderfully, thank you!

    For reference, if anyone cares, this is the result of what I was working on - just a prototype right now, but I'm using p5.sound to handle the background music which should now play a continuous loop of random loaded tracks. The callback is basically used to pick the next one, which is much simpler than my previous method. :)

    Only one issue that I've run into - it seems like the callback is also called on pause, as well as on end. Is this intentional? Easy enough for me to work around but it seemed slightly unintuitive.

  • Only one issue that I've run into - it seems like the callback is also called on pause, as well as on end. Is this intentional? Easy enough for me to work around but it seemed slightly unintuitive.

    Provide feedback - either as a comment on the existing issue or raise a new one. The feature was implemented quickly (a good thing) so such issues may not have been considered (not so good).

    A solution might be for the callback function to be passed the name of the event that fired it: you'd then be able to use this directly to determine how to respond. So in pseudocode:

    // in p5js sound
    sound.paused = function() {
      sound.onended.callback("paused");
    }
    
    sound.ended = function() {
      sound.onended.callback("ended");
    }
    
    // in your code
    function myCallback = function(eventName) {
      if(eventName == "paused") {
        // do this...
      }
      else if (eventName == "eneded") {
        // do that...
      }
      else {
        // default fallback - just in case...
      }
    
      sound.onended(myCallback);
    
    }
    

    Alternatively you should be able to specify which events fire the callback; though I suspect that would be more complex in terms of implementation.

    I haven't tested the onended solution; but another possible issue I wondered about was whether sound files that have been explicitly set to loop would fire the callback at each repetition... That again may not be desirable/expected.

  • edited November 2015

    IMO, onended() should only fire when a file reaches its end naturally. Not when paused or stopped.
    For loop() I still think it should fire so we know when it has restarted.
    Should definitely ask the onended()'s implementator to make these adjustments.
    And btW, fantastic rain cloud music animation! :-bd

  • IMO, onended() should only fire when a file reaches its end naturally. Not when paused or stopped.

    Fair comment; but it would then make sense to add onPaused and onStopped events too. And instead of calling onended when audio loops, instead have an onLoop event... Looking at a couple of existing JS audio libraries (SoundJS and howler.js they both allow listening for multiple event types; but take subtly different approaches in their implementation; neither of which involve sending a flag to the callback function...

  • ... but it would then make sense to add onPaused and onStopped events too.

    • Medias don't pause or stop by themselves. Instead those 2 are manual command actions.
    • Nothing wrong having some other code listening to those 2 events. But they're not essential.

    And instead of calling onended when audio loops, instead have an onLoop event...

    • Nothing wrong w/ splitting the end of a file as 2 separate events, depending whether it's in play or loop mode.
    • But all of those extra events depend on how far & complex p5.Sound's developer wants to go to.
  • I'm trying to use onended() but I might be misunderstanding something here. I thought onended() executed the callback only after the sound has been played AND ended. Yet, if I simply load a sound in preload and then call onended() on it, without ever playing it, it does execute the callback.

    Example: `function preload() { soundOne = loadSound('audio/later.mp3'); }

    function setup()
    {
        createCanvas(800, 400);
        background(255, 255, 255);
        fill(0, 0, 0, 255);
    
        soundOne.setVolume(0.1);
    
        soundOne.onended(ended());
    
    }
    
    function ended() {
        console.log('sound finished playing');
                // returns 'sound finished playing' as soon as I load the sketch
    }`
    

    I'm trying to accomplish the same as the OP: play several files in sequence, but am probably missing something on how to implement onended() here.

  • edited November 2015
    • You're passing undefined to onended() here: soundOne.onended(ended()); :-\"
    • All functions w/o some explicit return something; always return undefined.
    • Therefore your ended() function returns undefined when it's invoked. b-(
    • In order to fix that, you need to pass function ended()'s reference to method onended()
      instead of invoking the former w/ (): soundOne.onended(ended); *-:)
  • Wow, that was a great explanation @GoToLoop! It works (and I finally got it how it works). Thanks!

Sign In or Register to comment.