Preload and replay sounds in Expo

Expo comes with an Audio API that provides basic sound playback and recording capabilities in React Native projects. It includes a lot of useful features, but at times can be tricky to work with because of its asynchronous nature. I collected a few bits of information that can help with setting up and playing audio in Expo. 🎛️

Each sound before it can be played needs a SoundObject to be created. Initially I was creating these SoundObjects each time a sound was about to be played. This operation is quite expensive though, which led to delayed sounds and sometimes no sound at all. 🤦‍♂️

Another approach, which worked much better for me was to create a library of sounds, preload the sounds and have the SoundObjects ready before the playback needs to be started.

A sound library can simply be an object similar to the one below:

const soundLibrary = {
  wish: require('./assets/sounds/wish.m4a'),
  woosh: require('./assets/sounds/woosh.m4a'),
  bell: require('./assets/sounds/bell.m4a')
}

export default soundLibrary

Player is a class that allows to load the library and wraps the replayAsync method.

The key is to use the replayAsync method and not the playAsync method to allow to play the same sounds multiple times when necessary.

import { Audio } from 'expo'

const soundObjects = {}

class Player {
  static load(library) {
    const promisedSoundObjects = []

    for (const name in library) {
      const sound = library[name]

      soundObjects[name] = new Audio.Sound()

      promisedSoundObjects.push(
        soundObjects[name].loadAsync(sound)
      )
    }

    return promisedSoundObjects
  }

  static async playSound(name) {
    try {
      if (soundObjects[name]) {
        await soundObjects[name].replayAsync()
      }
    } catch (error) {
      console.warn(error)
    }
  }
}

export default Player

To preload sounds I'm making use of the AppLoading component (docs) and preload sounds in a similar way to how images or fonts are loaded at application startup.

import { Component } from 'react'
import { Font, AppLoading } from 'expo'
import Player from './Player.js'
import soundLibrary from './soundLibrary'

export default class App extends Component {
  state = {
    isReady: false
  }

 loadAssets() {
    const sounds = Player.load(soundLibrary)

    return Promise.all([
      Font.loadAsync({
        'montserrat-medium': require('./assets/fonts/Montserrat-Medium.otf')
      }),
      ...sounds
    ])
  }

  render() {
    if (!this.state.isReady) {
      return (
        <AppLoading
          startAsync="{this.loadAssets}"
          onFinish="{() => this.setState({ isReady: true })}
          onError={console.warn}
        />
      )
    }

    ...
  }
}

Finally, the sounds can be played by passing the sound name to the Player without having to worry about the implementation details, which are now all abstracted away:

Player.playSound('bell')