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.
This sample is hosted by the MusicSample.java
file. Follow the next steps to learn how to play music files:
private static final float VOLUME_CHANGE = 0.2f;
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;
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); }
Music
objects are disposed to avoid memory leaks:public void dispose() { for (Music song : songs) { song.dispose(); } }
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); }
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"); } }
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); }
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"); }
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"); }
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"); }
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"); }
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.
The Music
interface lies with Sound
inside the Libgdx Audio
module. Backends use the same underlying methods they used for Sound
to support Music
.
We covered the basic operations you can perform with a Music
object. However, there are a couple more things you can do.
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()
.
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.
3.141.31.125