12 min read

(For more resources related to this topic, see here.)

Playing audio files

JUCE provides a sophisticated set of classes for dealing with audio. This includes: sound file reading and writing utilities, interfacing with the native audio hardware, audio data conversion functions, and a cross-platform framework for creating audio plugins for a range of well-known host applications. Covering all of these aspects is beyond the scope of this article, but the examples in this section will outline the principles of playing sound files and communicating with the audio hardware. In addition to showing the audio features of JUCE, in this section we will also create the GUI and autogenerate some other aspects of the code using the Introjucer application.

Creating a GUI to control audio file playback

Create a new GUI application Introjucer project of your choice, selecting the option to create a basic window. In the Introjucer application, select the Config panel, and select Modules in the hierarchy.

For this project we need the juce_audio_utils module (which contains a special Component class for configuring the audio device hardware); therefore, turn ON this module. Even though we created a basic window and a basic component, we are going to create the GUI using the Introjucer application.

Navigate to the Files panel and right-click (on the Mac, press control and click) on the Source folder in the hierarchy, and select Add New GUI Component… from the contextual menu.

When asked, name the header MediaPlayer.h and click on Save. In the Files hierarchy, select the MediaPlayer.cpp file. First select the Class panel and change the Class name from NewComponent to MediaPlayer. We will need four buttons for this basic project: a button to open an audio file, a Play button, a Stop button, and an audio device settings button. Select the Subcomponents panel, and add four TextButton components to the editor by right-clicking to access the contextual menu. Space the buttons equally near the top of the editor, and configure each button as outlined in the table as follows:

Purpose

member name

name

text

background (normal)

Open file

openButton

open

Open…

Default

Play/pause file

playButton

play

Play

Green

Stop playback

stopButton

stop

Stop

Red

Configure audio

settingsButton

settings

Audio Settings…

Default

Arrange the buttons as shown in the following screenshot:

For each button, access the mode pop-up menu for the width setting, and choose Subtracted from width of parent. This will keep the right-hand side of the buttons the same distance from the right-hand side of the window if the window is resized. There are more customizations to be done in the Introjucer project, but for now, make sure that you have saved the MediaPlayer.h file, the MediaPlayer.cpp file, and the Introjucer project before you open your native IDE project.

Make sure that you have saved all of these files in the Introjucer application; otherwise the files may not get correctly updated in the file system when the project is opened in the IDE.

In the IDE we need to replace the MainContentComponent class code to place a MediaPlayer object within it. Change the MainComponent.h file as follows:

#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
#include "MediaPlayer.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void resized();
private:
MediaPlayer player;

};
#endif

Then, change the MainComponent.cpp file to:

#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
addAndMakeVisible (&player);
setSize (player.getWidth(),player.getHeight());
}
void MainContentComponent::resized()
{
player.setBounds (0, 0, getWidth(), getHeight());
}

Finally, make the window resizable in the Main.cpp file, and build and run the project to check that the window appears as expected.

Adding audio file playback support

Quit the application and return to the Introjucer project. Select the MediaPlayer.cpp file in the Files panel hierarchy and select its Class panel. The Parent classes setting already contains public Component. We are going to be listening for state changes from two of our member objects that are ChangeBroadcaster objects. To do this, we need our MediaPlayer class to inherit from the ChangeListener class. Change the Parent classes setting such that it reads:

public Component, public ChangeListener

Save the MediaPlayer.h file, the MediaPlayer.cpp file, and the Introjucer project again, and open it into your IDE. Notice in the MediaPlayer.h file that the parent classes have been updated to reflect this change. For convenience, we are going to add some enumerated constants to reflect the current playback state of our MediaPlayer object, and a function to centralize the change of this state (which will, in turn, update the state of various objects, such as the text displayed on the buttons). The ChangeListener class also has one pure virtual function, which we need to add. Add the following code to the [UserMethods] section of MediaPlayer.h:

//[UserMethods]-- You can add your own custom methods...
enum TransportState {
Stopped,
Starting,
Playing,
Pausing,
Paused,
Stopping
};
void changeState (TransportState newState);
void changeListenerCallback (ChangeBroadcaster* source);
//[/UserMethods]

We also need some additional member variables to support our audio playback. Add these to the [UserVariables] section:

//[UserVariables] -- You can add your own custom variables...
AudioDeviceManager deviceManager;
AudioFormatManager formatManager;
ScopedPointer<AudioFormatReaderSource> readerSource;
AudioTransportSource transportSource;
AudioSourcePlayer sourcePlayer;
TransportState state;
//[/UserVariables]

The AudioDeviceManager object will manage our interface between the application and the audio hardware. The AudioFormatManager object will assist in creating an object that will read and decode the audio data from an audio file. This object will be stored in the ScopedPointer<AudioFormatReaderSource> object. The AudioTransportSource object will control the playback of the audio file and perform any sampling rate conversion that may be required (if the sampling rate of the audio file differs from the audio hardware sampling rate). The AudioSourcePlayer object will stream audio from the AudioTransportSource object to the AudioDeviceManager object. The state variable will store one of our enumerated constants to reflect the current playback state of our MediaPlayer object.

Now add some code to the MediaPlayer.cpp file. In the [Constructor] section of the constructor, add following two lines:

playButton->setEnabled (false);
stopButton->setEnabled (false);

This sets the Play and Stop buttons to be disabled (and grayed out) initially. Later, we enable the Play button once a valid file is loaded, and change the state of each button and the text displayed on the buttons, depending on whether the file is currently playing or not. In this [Constructor] section you should also initialize the AudioFormatManager as follows:

formatManager.registerBasicFormats();

This allows the AudioFormatManager object to detect different audio file formats and create appropriate file reader objects. We also need to connect the AudioSourcePlayer, AudioTransportSource and AudioDeviceManager objects together, and initialize the AudioDeviceManager object. To do this, add the following lines to the [Constructor] section:

sourcePlayer.setSource (&transportSource);
deviceManager.addAudioCallback (&sourcePlayer);
deviceManager.initialise (0, 2, nullptr, true);

The first line connects the AudioTransportSource object to the AudioSourcePlayer object. The second line connects the AudioSourcePlayer object to the AudioDeviceManager object. The final line initializes the AudioDeviceManager object with:

  • The number of required audio input channels (0 in this case).
  • The number of required audio output channels (2 in this case, for stereo output).
  • An optional “saved state” for the AudioDeviceManager object (nullptr initializes from scratch).
  • Whether to open the default device if the saved state fails to open. As we are not using a saved state, this argument is irrelevant, but it is useful to set this to true in any case.

The final three lines to add to the [Constructor] section to configure our MediaPlayer object as a listener to the AudioDeviceManager and AudioTransportSource objects, and sets the current state to Stopped:

deviceManager.addChangeListener (this);
transportSource.addChangeListener (this);
state = Stopped;

In the buttonClicked() function we need to add some code to the various sections. In the [UserButtonCode_openButton] section, add:

//[UserButtonCode_openButton] -- add your button handler...
FileChooser chooser ("Select a Wave file to play...",
File::nonexistent,
"*.wav");
if (chooser.browseForFileToOpen()) {
File file (chooser.getResult());
readerSource = new AudioFormatReaderSource(
formatManager.createReaderFor (file), true);
transportSource.setSource (readerSource);
playButton->setEnabled (true);
}
//[/UserButtonCode_openButton]

When the openButton button is clicked, this will create a FileChooser object that allows the user to select a file using the native interface for the platform. The types of files that are allowed to be selected are limited using the wildcard *.wav to allow only files with the .wav file extension to be selected.

If the user actually selects a file (rather than cancels the operation), the code can call the FileChooser::getResult() function to retrieve a reference to the file that was selected. This file is then passed to the AudioFormatManager object to create a file reader object, which in turn is passed to create an AudioFormatReaderSource object that will manage and own this file reader object. Finally, the AudioFormatReaderSource object is connected to the AudioTransportSource object and the Play button is enabled.

The handlers for the playButton and stopButton objects will make a call to our changeState() function depending on the current transport state. We will define the changeState() function in a moment where its purpose should become clear.

In the [UserButtonCode_playButton] section, add the following code:

//[UserButtonCode_playButton] -- add your button handler...
if ((Stopped == state) || (Paused == state))
changeState (Starting);
else if (Playing == state)
changeState (Pausing);
//[/UserButtonCode_playButton]

This changes the state to Starting if the current state is either Stopped or Paused, and changes the state to Pausing if the current state is Playing. This is in order to have a button with combined play and pause functionality.

In the [UserButtonCode_stopButton] section, add the following code:

//[UserButtonCode_stopButton] -- add your button handler...
if (Paused == state)
changeState (Stopped);
else
changeState (Stopping);
//[/UserButtonCode_stopButton]

This sets the state to Stopped if the current state is Paused, and sets it to Stopping in other cases. Again, we will add the changeState() function in a moment, where these state changes update various objects.

In the [UserButtonCode_settingsButton] section add the following code:

//[UserButtonCode_settingsButton] -- add your button handler...
bool showMidiInputOptions = false;
bool showMidiOutputSelector = false;
bool showChannelsAsStereoPairs = true;
bool hideAdvancedOptions = false;
AudioDeviceSelectorComponent settings (deviceManager,
0, 0, 1, 2,
showMidiInputOptions,
showMidiOutputSelector,
showChannelsAsStereoPairs,
hideAdvancedOptions);
settings.setSize (500, 400);
DialogWindow::showModalDialog(String ("Audio Settings"),
&settings,
TopLevelWindow::getTopLevelWindow (0),
Colours::white,
true); //[/UserButtonCode_settingsButton]

This presents a useful interface to configure the audio device settings.

We need to add the changeListenerCallback() function to respond to changes in the AudioDeviceManager and AudioTransportSource objects. Add the following to the [MiscUserCode] section of the MediaPlayer.cpp file:

//[MiscUserCode] You can add your own definitions...
void MediaPlayer::changeListenerCallback (ChangeBroadcaster* src)
{
if (&deviceManager == src) {
AudioDeviceManager::AudioDeviceSetup setup;
deviceManager.getAudioDeviceSetup (setup);
if (setup.outputChannels.isZero())
sourcePlayer.setSource (nullptr);
else
sourcePlayer.setSource (&transportSource);
} else if (&transportSource == src) {
if (transportSource.isPlaying()) {
changeState (Playing);
} else {
if ((Stopping == state) || (Playing == state))
changeState (Stopped);
else if (Pausing == state)
changeState (Paused);
}
}
}
//[/MiscUserCode]

If our MediaPlayer object receives a message that the AudioDeviceManager object changed in some way, we need to check that this change wasn’t to disable all of the audio output channels, by obtaining the setup information from the device manager. If the number of output channels is zero, we disconnect our AudioSourcePlayer object from the AudioTransportSource object (otherwise our application may crash) by setting the source to nullptr. If the number of output channels becomes nonzero again, we reconnect these objects.

If our AudioTransportSource object has changed, this is likely to be a change in its playback state. It is important to note the difference between requesting the transport to start or stop, and this change actually taking place. This is why we created the enumerated constants for all the other states (including transitional states). Again we issue calls to the changeState() function depending on the current value of our state variable and the state of the AudioTransportSource object.

Finally, add the important changeState() function to the [MiscUserCode] section of the MediaPlayer.cpp file that handles all of these state changes:

void MediaPlayer::changeState (TransportState newState)
{
if (state != newState) {
state = newState;
switch (state) {
case Stopped:
playButton->setButtonText ("Play");
stopButton->setButtonText ("Stop");
stopButton->setEnabled (false);
transportSource.setPosition (0.0);
break;
case Starting:
transportSource.start();
break;
case Playing:
playButton->setButtonText ("Pause");
stopButton->setButtonText ("Stop");
stopButton->setEnabled (true);
break;
case Pausing:
transportSource.stop();
break;
case Paused:
playButton->setButtonText ("Resume");
stopButton->setButtonText ("Return to Zero");
break;
case Stopping:
transportSource.stop();
break;
}
}
}

After checking that the newState value is different from the current value of the state variable, we update the state variable with the new value. Then, we perform the appropriate actions for this particular point in the cycle of state changes. These are summarized as follows:

  • In the Stopped state, the buttons are configured with the Play and Stop labels, the Stop button is disabled, and the transport is positioned to the start of the audio file.
  • In the Starting state, the AudioTransportSource object is told to start. Once the AudioTransportSource object has actually started playing, the system will be in the Playing state. Here we update the playButton button to display the text Pause, ensure the stopButton button displays the text Stop, and we enable the Stop button.
  • If the Pause button is clicked, the state becomes Pausing, and the transport is told to stop. Once the transport has actually stopped, the state changes to Paused, the playButton button is updated to display the text Resume and the stopButton button is updated to display Return to Zero.
  • If the Stop button is clicked, the state is changed to Stopping, and the transport is told to stop. Once the transport has actually stopped, the state changes to Stopped (as described in the first point).
  • If the Return to Zero button is clicked, the state is changed directly to Stopped (again, as previously described).
  • When the audio file reaches the end of the file, the state is also changed to Stopped.

Build and run the application. You should be able to select a .wav audio file after clicking the Open… button, play, pause, resume, and stop the audio file using the respective buttons, and configure the audio device using the Audio Settings… button. The audio settings window allows you to select the input and output device, the sample rate, and the hardware buffer size. It also provides a Test button that plays a tone through the selected output device.

Summary

This article has covered a few of the techniques for dealing with audio files in JUCE. The article has given only an introduction to get you started; there are many other options and alternative approaches, which may suit different circumstances. The JUCE documentation will take you through each of these and point you to related classes and functions.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here