3

UPDATE

Combining both solutions below, I wrote:

const startMusic = async () => {
    let currentSong
    let songPath
    const songArray = [
      { path: require("../assets/sounds/Katsu.mp3"), song: mainTheme },
      { path: require("../assets/sounds/MainTheme2.mp3"), song: mainTheme2 },
      { path: require("../assets/sounds/MainTheme3.mp3"), song: mainTheme3 },
    ]

    for (var i = 0; i < songArray.length; i++) {
      currentSong = songArray[i].song
      songPath = songArray[i].path
      try {
        await currentSong.loadAsync(songPath)
        await currentSong.playAsync()
        // setSoundObject(currentSong)
        console.log("Music will start")
        return new Promise(resolve => {
          currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
            if (playbackStatus.didJustFinish) {
              console.log("Just finished playing")
              resolve()
            }
          })
        })
      } catch (error) {
        console.log(`Error: ${error}`)
        return
      }
    }
  }

This actually plays the song, and the console logs occur on time ("Just finished playing" happens exactly when the song ends) I'm trying to figure out how to play the next song.. and how will it know when it has reached the final song?

return new Promise(resolve => {
  currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
    if (playbackStatus.didJustFinish) {
      console.log("Just finished playing")
      resolve()
    }
  })
}).then(() => console.log("Next song?"))

Figured how where to put the .then to get it to console log right after "Just finished playing" I'm just trying to see how to actually put the next song there (then of course, telling it when to go back to the first song in the array)


Original Post

Working on an assignment for a react native app using expo-av library for Sound files. Right now, the app has a startMusic function set in a Context file that is responsible for playing the app's background music. It only has one song for now:

const startMusic = async () => {
    try {
      await mainTheme.loadAsync(require("../assets/sounds/Katsu.mp3"))
      await mainTheme.playAsync()
      setSoundObject(mainTheme)
      console.log("The first song is playing! Enjoy!")
    } catch (error) {
      console.log(`Couldnt load main theme: ${error}`)
      return
    }
  }

It is used in the homescreen component's file like so:

const { startMusic } = useContext(MusicContext)

useEffect(() => {
  startMusic()
}, [])

For the second song, I wrote another const in the MusicContext file:

const secondSong = async () => {
    try {
      await mainTheme2.loadAsync(require("../assets/sounds/MainTheme2.mp3"))
      await mainTheme2.playAsync()
      setSoundObject(mainTheme2)
      console.log("Now playing the second track. Enjoy!")
    } catch (error) {
      console.log(`Could not play the second song: ${error}`)
      return
    }
  }

Annnnnd… here is where my trouble lies. I know this wasn't gonna work but I wrote this in the component file to try to get the second song playing after the first song

useEffect(() => {
    startMusic()
      .then(secondSong())
  }, [])

I know there's more to it than that but I'm having trouble.

1
  • 1
    given what we have to work with, @chiragrtr has the easiest solution to understand, but @Max brings up a good point: if you want your songs to play sequentially, you're going to want to find the point at which the first song ends, and return a Promise from startMusic that resolves at that point. I would rename startMusic then to playFirstSong Commented May 24, 2020 at 0:26

3 Answers 3

4

Problem with your code is not just running one function after another (that would be as simple as startMusic().then(() => secondSong()) but still won't solve the problem), but the fact that your functions actually don't wait for a song to finish playing before resolving

You expect this line await mainTheme.playAsync() to pause function execution until the song has finished, but what it in fact does according to docs https://docs.expo.io/versions/latest/sdk/av/ is exactly only starting the playback (without waiting for it to finish)

With that being said, you need to determine the moment your playback finishes, then create a Promise that will only resolve after the playback is finished so that your second song can only start after the first

In the simplest form without error handling and such, it can look like this

const startAndWaitForCompletion = async () => {
  try {
    await mainTheme.loadAsync(require('../assets/sounds/Katsu.mp3'))
    await mainTheme.playAsync()
    console.log('will start playing soon')
    return new Promise((resolve) => {
      mainTheme.setOnPlaybackStatusUpdate(playbackStatus => {
        if (playbackStatus.didJustFinish) {
          console.log('finished playing')
          resolve()
        }
      }
    })
  } catch (error) {
    console.log('error', error)
  }
}

the trick is of course the .setOnPlaybackStatusUpdate listener that will be called every so often with playback status, and by analyzing the status you can tell the song has finished playing. If you scroll to the bottom of the page I linked you will find other examples with status update


updated

const startAndWaitForCompletion = async (playbackObject, file) => {
  try {
    await playbackObject.loadAsync(file)
    await playbackObject.playAsync()
    console.log('will start playing soon')
    return new Promise((resolve) => {
      playbackObject.setOnPlaybackStatusUpdate(playbackStatus => {
        if (playbackStatus.didJustFinish) {
          console.log('finished playing')
          resolve()
        }
      }
    })
  } catch (error) {
    console.log('error', error)
  }
}

////

const songs = [
  { path: require('../assets/sounds/Katsu.mp3'), song: mainTheme },
  { path: require('../assets/sounds/MainTheme2.mp3'), song: mainTheme2 },
  { path: require('../assets/sounds/MainTheme3.mp3'), song: mainTheme3 },
]


useEffect(() => {
  (async () => {
    for (let i = 0; i < songs.length; i++) {
      await startAndWaitForCompletion(songs[i].song, songs[i].path)
    }
  })()
}, [])
Sign up to request clarification or add additional context in comments.

4 Comments

This is excellent thank you. You and Apollo explained what I knew to do, but didn't know how. Using your solution combined with Apollo's I have it so that it loops through an array of songs, but it only plays the first one and then stops still. Trying to leverage .then() after that promise
.then(() => { console.log("Next song?") nextSong = currentSong + 1 nextSong.title.loadAsync(nextSong.path) nextSong.title.playAsync() })
You shouldn't be combining our answers as the other one is also based on the wrong assumption that await mainTheme.playAsync() is awaiting for the song to finish. The whole point of returning promise from playback function is so that you can await for it to finish playing one song. Naturally, playing all the songs in that one function is not going to work. updated my asnwer
That makes a lot of sense. The only discrepancy here is that the const and array are in MusicContext.js, while useEffect() is in the component. MusicContext.js is importing the 'expo-av' library that creates the song objects you see in the songs array. Looks like mainTheme = new Audio.Sound(). Seems I need to put the Array in the component file for this to work, but the songs are being instantiated in the context file.. I'll work at figuring it out. You pointed me in the right direction nonetheless friend. TY
0

I think you need to rethink this problem/solution to be more abstract.

Instead of making a new const and promise for every single song you want to play (which, as you said, isn't workable, and isn't scalable, like say if you wanted 10 songs instead of 2), make "startMusic" a function that plays an array of songs (each array index being a filepath to an MP3, like in your example), and then resolve/reject the promise as needed.

A quick "startMusic" rewrite:

const startMusic(songArray) = async () => {
    for (var songIndex in songArray) {
      try {
        await mainTheme.loadAsync(require(songArray[songIndex]))
        await mainTheme.playAsync()
        setSoundObject(mainTheme)
        console.log("Song #", songIndex, "of ", songArray.length " is playing. Enjoy!")
    } catch (error) {
      console.log(`Couldnt load song: ${error}`)
      return
    }
  }
}

A "Promise.all" chain could be useful here, too, if the above for-try-catch structure doesn't work: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Not familiar with the expo-av library, so there might be some specific quirks to look out for there, but I think re-writing "startMusic" to be an abstract function that plays an array of "N" songs is a much more optimal approach, and will minimize/eliminate your problems.

2 Comments

Thank you Apollo. I did `for (var i=0; i < songArray.length; i++) {} and it plays the first song (beginning and ending with those console logs properly) but it won't play the next track. I'm trying to figure out how to get it to play through. I also tried forEach() and map(). I read an article about using reduce() as well but their applications are different enough that i'm having trouble integrating it with this code
Downvoted because this will play all of the songs in the array at the same time.
0

.then() accepts a function but you've provided the result of function execution by calling secondSong.

Do:

useEffect(() => {
    startMusic()
      .then(() => secondSong())
  }, [])

Or just get rid of () after secondSong:

useEffect(() => {
    startMusic()
      .then(secondSong)
  }, [])

1 Comment

@richytong Thanks. I removed await suggestion. I haven't really started using hooks as my workplace doesn't let us. I'll explore hooks and read more about this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.