rustrodio

How to I detect when a sink moves to the next source?


I am using rust with the rodio crate to make a basic command line mp3 player. Everything works fine, except I have no way to display the currently playing song.

I made a Song struct with a title, artist, path, and source. I then made a vector queue of Song structs to keep track of the song that I am playing and songs that will play next. I appended the sources of the songs to a sink, and made a variable queueindex that keeps track of the index of the currently playing song in the queue. The problem? I have no way to tell when a song finishes playing, and thus cannot increment the queueindex variable. sink.empty() doesn't help because it only returns true when the sink is completely empty. Is there any way to tell when a source finishes playing and another one starts? Edit: Should I add the full source code to the question?


Solution

  • (Yes, providing more of your source would be helpful.)

    I don't have rodio set up locally, but the docs say that the actual samples of a Source are obtained by simply iterating over it, i.e. a Source must also be an Iterator<Item = Sample>. So, to execute some code when the iterator finishes (to increment the queue index), we will create a struct SourceWithFn.

    struct SourceWithFn<S, F>(S, Option<F>);
    

    The struct must be an Iterator, to pass data through from the original source, and to detect when the source finishes.

    impl<S, F> Iterator for SourceWithFn<S, F>
    where
        S: Source,
        S::Item: Sample,
        F: FnOnce(),
    {
        type Item = S::Item;
        #[inline]
        fn next(&mut self) -> Option<S::Item> {
            match self.0.next() {
                Some(n) => Some(n),
                None => {
                    self.1.take().unwrap()();
                    None
                }
            }
        }
        #[inline]
        fn size_hint(&self) -> (usize, Option<usize>) {
            self.0.size_hint()
        }
    }
    

    Note that the unwrap will panic if the iterator is invoked after it has finished. You can change the implementation to FnMut (and remove the Option) if this is needed.

    The struct must also be a Source:

    impl<S, F> Source for SourceWithFn<S, F>
    where
        S: Source,
        S::Item: Sample,
        F: FnOnce(),
    {
        #[inline]
        fn current_frame_len(&self) -> Option<usize> {
            self.0.current_frame_len()
        }
        #[inline]
        fn channels(&self) -> u16 {
            self.0.channels()
        }
        #[inline]
        fn sample_rate(&self) -> u32 {
            self.0.sample_rate()
        }
        #[inline]
        fn total_duration(&self) -> Option<Duration> {
            self.0.total_duration()
        }
    }
    

    And finally, a convenience constructor:

    impl<S, F> SourceWithFn<S, F>
    where
        S: Source,
        S::Item: Sample,
        F: FnOnce(),
    {
        fn wrap(source: S, f: F) -> Self {
            Self(source, Some(f))
        }
    }
    

    Now, you can wrap an existing source like so:

    let source = todo!();
    let wrapped_source = SourceWithFn::Wrap(source, || queue_index += 1);
    

    However, note that queue_index is captured mutably by the above! To make this work with multiple sources and rodio in general, you might want to put the queue_index in a Cell or similar.