Audio streaming for background music

The Sound interface gives us a great deal of flexibility. However, it's not suitable for all needs due to one fundamental reason: sounds are fully loaded in memory before playback can start. This is not a problem for short effects, but it rapidly becomes one if we try to use it to play our soundtracks, especially in memory-limited devices such as phones. While effects lie around the order of tens to hundreds of KB, reasonably decent-quality song files can take up around 10 MB, even if they are not very long.

How do we solve this dire situation? Streaming is the answer. This technique only needs to keep in memory the next chunk of file that needs to be played, which makes for an epic win. However, not everything is happiness. Streaming means we need to decode the audio files as we go, rather than at load time. This inevitably has a CPU-wise performance impact that we did not suffer from with SFX. Still, streaming is the preferable way when it comes to playing long audio files.

Throughout this recipe, we will discover how to wield audio streaming to play background music with Libgdx. The presented sample consists of a very minimalistic music player, without an interface. The user can use some keys to control playback and select the next or previous song.

Getting ready

You will need the sample projects in your Eclipse workspace before going ahead.

How to do it…

This sample is hosted by the MusicSample.java file. Follow the next steps to learn how to play music files:

  1. First, we define a constant representing the value by which the volume is going to change every time the user presses the appropriate keys:
    private static final float VOLUME_CHANGE = 0.2f;
  2. Libgdx uses the Music interface to allow us to stream and play audio from disk. Our sample counts with an Array of Music objects accompanied by the index of the instance that is currently playing. We also keep track of the current volume level. Finally, we have SongListener, which is a custom implementation of the OnCompletionListener interface that we will examine later:
    private Array<Music> songs;
    private int currentSongIdx;
    private float volume;
    private SongListener listener;
  3. The create() method is quite simple; we initialize our members and populate the songs array with the files inside the data/music folder. We register our sample as the current InputProcessor, log the instructions, and play the first song with a custom playSong() method:
    public void create() {	
      listener = new SongListener();
    
      songs = new Array<Music>();
      songs.add(Gdx.audio.newMusic(Gdx.files.internal("data/music/song_1.mp3")));
      …
    
      currentSongIdx = 0;
      volume = 1.0f;
    
      Gdx.input.setInputProcessor(this);
    
      Gdx.app.log("MusicSample", "Instructions");
      …
    
      playSong(0);
    }
  4. Before we exit the application, we need to ensure the Music objects are disposed to avoid memory leaks:
    public void dispose() {
      for (Music song : songs) {
        song.dispose();
      }
    }
  5. The playSong() method gets the Music reference pointed by songIdx from the array and plays it. In order to do this, it needs to fetch the previous reference and call stop() on it. Music objects are played with the play() method. We also set the current volume on the objects with setVolume(), and tell each object we want to be notified when it is done with our listener:
    void playSong(int songIdx) {
      Music song = songs.get(currentSongIdx);
      song.setOnCompletionListener(null);
      song.stop();
    
      currentSongIdx = songIdx;
      song = songs.get(currentSongIdx);
      song.play();
      song.setVolume(volume);
      song.setOnCompletionListener(listener);
    }

    Note

    Unlike Sound, we cannot have multiple instances of the same Music object playing at the same time.

  6. The OnCompletionListener interface has only one method, onCompletion(), that gets called when the Music instance we are listening to is over. In our case, we simply play the next song, going back to the start of the array, if necessary:
    private class SongListener implements OnCompletionListener {
      @Override
      public void onCompletion(Music music) {
        playSong((currentSongIdx + 1) % songs.size);
        Gdx.app.log("MusicSample", "Song finished, play next song");
      }
    }
  7. In order to change the volume, we provide the convenience method, changeVolume(). It simply makes sure the volume we pass in is between 0.0f and 1.0f, calls setVolume() on the current song, and caches the current volume:
    void changeVolume(float volumeChange) {
      Music song = songs.get(currentSongIdx);
      volume = MathUtils.clamp(song.getVolume() + volumeChange, 0.0f, 1.0f);
      song.setVolume(volume);
    }
  8. The keyDown() event handler checks which key has been pressed. If the user presses P, we call pause() on the current song:
    if (keycode == Keys.P) {
      songs.get(currentSongIdx).pause();
      Gdx.app.log("MusicSample", "Song paused");
    }
  9. Upon the R key-down event, we resume the current song's playback with the play() method. Note that as we cannot play the same Music object twice, this will have no effect if it is already playing:
    else if (keycode == Keys.R) {
      songs.get(currentSongIdx).play();
      Gdx.app.log("MusicSample", "Song resumed");
    }
  10. Whenever the user presses the Up or Down arrow keys, we call changeVolume(), passing in positive or negative VOLUME_CHANGE, respectively:
    else if (keycode == Keys.UP) {
      changeVolume(VOLUME_CHANGE);
      Gdx.app.log("MusicSample", "Volume up");
    }
    else if (keycode == Keys.DOWN) {
      changeVolume(-VOLUME_CHANGE);
      Gdx.app.log("MusicSample", "Volume down");
    }
  11. The user can use the Right and Left arrow keys to play the next and previous song, respectively, for which we call playSong() with the appropriate argument:
    else if (keycode == Keys.RIGHT) {
      playSong((currentSongIdx + 1) % songs.size);
      Gdx.app.log("MusicSample", "Next song");
    }
    else if (keycode == Keys.LEFT) {
      int songIdx = (currentSongIdx - 1) < 0 ? songs.size - 1 : currentSongIdx - 1;
      playSong(songIdx);
      Gdx.app.log("MusicSample", "Previous song");
    }
  12. Not to discriminate against touchscreens, we added a touchDown() handler that simply plays the next song, as shown in the following code. We could have used GestureDetector to add full controls, but decided not to do so for the sake of briefness:
    public boolean touchDown (int screenX, int screenY, int pointer, int button) {
      playSong((currentSongIdx + 1) % songs.size);
      Gdx.app.log("MusicSample", "Next song");
      return true;
    }

As you can see, getting Libgdx to play background music is fairly straightforward. Run the sample and play around with the controls to see the effects of your little sample.

How it works…

The Music interface lies with Sound inside the Libgdx Audio module. Backends use the same underlying methods they used for Sound to support Music.

There's more…

We covered the basic operations you can perform with a Music object. However, there are a couple more things you can do.

Checking the playback state

The isPlaying() method returns whether or not our Music instance is currently being played. You can also check for how long the file has been playing in milliseconds with getPosition().

Looping the background music

The background music can be set to loop indefinitely with the setLooping(boolean isLooping) method. This can be useful if your soundtrack is not long, and some situations can be active for longer than the audio file lasts. The looping state of a Music object can be checked at any point in time with the isLooping() method.

See also

Now that you know how to manage SFX and background music with Libgdx, feel free to move on to the following recipes:

  • Optimizing audio files to reduce download sizes
  • Procedural audio generators
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.10.1