The openFrameworks' ofSoundPlayer
class is designed for playing and controlling sound samples. The basic usage of the ofSoundPlayer
sound object is the following:
There are a number of functions for controlling sample playback. They are as follows:
stop()
function stops sample playing.getIsPlaying()
function returns true
if our sample is currently playing.setPaused( pause )
function enables or disables pause in sample playing, with pause
of type bool
.setPosition( pos )
function sets sample playing position, where pos
is a float
value from 0.0
to 1.0
. Here 0.0
means the start of the sample and 1.0
means the end of the sample.getPosition()
function returns the current playing position as a float
value from 0.0
to 1.0
.setPositionMS( ms )
function sets the sample playing position in milliseconds, where ms
has type int
.getPositionMS()
function returns the int
value with the current sample playing position in milliseconds.setLoop( looping )
function enables or disables the sample loop mode in which the sample repeats infinitely, looping
has a type bool
.setMultiPlay( multi )
function is a very important function. It enables or disables the special mode for playing multiple copies of the sample simultaneously. The multi
attribute has a type bool
. By default, this mode is disabled, so if you start playing with sample.play()
, wait some time and call it again, then you will hear that the first sound has stopped and the second sound has started. If you call setMultiPlay( true )
before playing, then you will hear two samples playing simultaneously.To stop all the playing samples, call the ofSoundStopAll()
global function.
There are two functions for controlling the process of loading samples from files. They are especially useful when your project uses many sample files:
isLoaded()
function returns true
if the sound is successfully loaded and is ready to play.unloadSound()
function unloads a sample from the memory. This function is useful for memory saving in mobile devices. On PCs, it is not so crucial. Note that the loadSound()
function unloads the previous loaded sample automatically, so you will probably never use this function on a PC.Until now, we learned how to play sound samples in an unchangeable way. But when you play the sample several times, it will sound exactly the same and can be boring, especially for short samples. One method for adding diversity in the output sound is by loading many different samples and selecting them to play randomly. Another great method is to use not as many samples, but to change its parameters such as speed, volume, and stereo panorama:
setSpeed( speed )
function sets the speed of sample playing. Here speed
has type float
. If speed
is equal to 1.0
, the sample plays unchangeable. Value 2.0
means the sample plays two times faster and with doubled tone. Value -1.0
means the sample plays reversed.setVolume( vol )
function sets the volume of the sample, where vol
is a float
value from 0.0
to 1.0
.setPan( pan )
function sets stereo panorama position of the sample, where pan
is a float
value from -1.0
(left) to 1.0
(right). The default value is 0.0
(center).getSpeed()
, getVolume()
, and getPan()
functions return the current value of the corresponding parameter of the sample.The global function setSoundVolume( vol )
changes the overall volume of all the sounds playing. Note that it affects only the samples playing and has no effect on sound generation (which is discussed in the Generating sounds section).
Let's consider an example of playing sound samples and changing their parameters based on a simple physical model.
Consider a ball bouncing on the floor. Let the ball jump in the left or right direction after each bounce and let's play a sound sample at each bounce, with the speed (and hence a tone) of the sample depending on the ball position. Over time, some random sequences of samples with different tones will be played. It is a piece of computer-generated music, based on physical modeling.
The example is based on the emptyExample
project in openFrameworks. Before running it, copy the bounce.wav
file into the bin/data
folder of your project.
In the testApp.h
file, in the class testApp
declaration, add declarations of sound samples and the ball moving function, after the #include "testApp.h" line
:
ofSoundPlayer sound; //Sound sample bool updateBall(); //Move ball function
Now let's consider the testApp.cpp
file. For simplicity, we place the model constants and variables not in the testApp
class definition, but right into the beginning of the .cpp
file:
float mass = 0.007; //Mass of point float g = 9.8; //Gravity force float time0; //Time value, used for time step computing ofPoint pos, vel; //Ball position and velocity
The setup()
function does the sound sample loading and model initialization:
void testApp::setup(){ //Set up sound sample sound.loadSound( "bounce.wav" ); //Load sound sample sound.setMultiPlay( true ); //Set multiplay mode //Model setup time0 = ofGetElapsedTimef(); //Get current time pos = ofPoint( ofGetWidth() / 2, 100 ); //Ball's initial position vel = ofPoint( 0, 0 ); //Initial velocity //Set up background to not clear each frame ofSetBackgroundAuto( false ); ofBackground( 255, 255, 255 ); //Clear background to white }
Note that we use the ofSetBackgroundAuto( false )
function calling, which disables clearing of the screen at each testApp::draw()
calling, so the drawing will be accumulated on the screen (see details in the Drawing with an uncleared background section in Chapter 2, Drawing in 2D).
The update()
function moves the ball, and if bouncing occurs then the sample starts to play. The important thing here is calling ofSoundUpdate()
to update the sound engine for each update()
call, for the samples to play correctly:
void testApp::update(){ //Update ball position and check if it is bounced bool bounced = updateBall(); if ( bounced ) { //Start sample playing sound.play(); //Set play speed, in dependence of x float speed = ofMap( pos.x, 0, ofGetWidth(), 0.2, 2 ); sound.setSpeed( speed ); } //Update sound engine ofSoundUpdate(); }
The draw()
function draws the floor line and the ball:
void testApp::draw(){ float bottom = 300.0; //The floor position on the screen //Draw the floor line in black color ofSetColor( 0, 0, 0 ); ofLine( 0, bottom, ofGetWidth(), bottom ); //Draw the ball in red color ofSetColor( 255, 0, 0 ); ofFill(); ofCircle( pos.x, bottom - pos.y, 3 ); }
The last function to consider is updateBall()
. It changes the position and the velocity of the ball using the Euler method, according to Newton's second law of motion, with gravitational force.
Details on the Euler method can be seen in the Defining the particle functions section in Chapter 3, Building a Simple Particle System. The information on the second Newton's law of motion and gravity force can be seen at http://en.wikipedia.org/wiki/Newton's_laws_of_motion and http://en.wikipedia.org/wiki/Gravitational_field.
When the ball bounces on the floor, it bounces in the y axis and changes its x velocity randomly. When the ball jumps out of the screen, it appears on the opposite side of the screen. The function returns true
if the ball is bounced off the floor:
bool testApp::updateBall() { bool bounced = false; //Compute dt float time = ofGetElapsedTimef(); float dt = ofClamp( time - time0, 0, 0.1 ); time0 = time; //Compute gravity force acceleration //using the second Newton's law ofPoint acc( 0, -g/mass ); //Change velocity and position using Euler's method vel += acc * dt; pos += vel * dt; //Check if the ball bounced off floor if ( pos.y < 0 ) { //Elastic bounce with momentum conservation pos.y = -pos.y; vel.y = -vel.y; //Set random velocity by x axe in range [-300, 500] vel.x = ofRandom( -300, 500 ); bounced = true; } //Check if the ball is out of screen if ( pos.x < 0 ) { pos.x += ofGetWidth(); } if ( pos.x > ofGetWidth() ) { pos.x -= ofGetWidth(); } return bounced; }
The dt
is a time step value, which is computed as a time difference between the current time and the time of the previous calling of the updateBall()
function.
We use the ofClamp()
function for limiting its value by 0.1
. The reason for this is that sometimes time - time0
can be a large value. (For example, if the user drags the window or hides the application's window, testApp::update()
callings can be paused - it depends on the operating system.) So if we don't limit this, formulas in the Euler method will work in an unstable manner, and the model literally explodes.
Run the example. You will see a flying red dot which bounces of the line (the floor) and also draws its trajectory on the screen. Each time the bouncing occurs, you will hear a sound. Over time you will see the ball's path as shown in the following screenshot:
Run the example a few more times. You will notice that the resulting trajectories and the music differ. But the structure of the music will be the same. Actually it is the structured randomness effect, which is typical for many creative coding and generative art projects.
You can play with parameters such as mass, the y value of the ball's initial position (100
), the range of dependence of the sample speed of pos.x
, range for random velocity, and explore how model behavior and the music structure changes.
There is a simple but fruitful method to make interesting and evolving sounds with samples. It is based on playing several different samples simultaneously and changing the parameters continuously inside testApp::update()
. Let's consider the simplest case of changing just the volumes of the samples. Namely, let's get a number of vocal samples singing different notes, start playing them, and randomly change the volume of each sample. The resulting sound will be like a live choir singing a tonic chord.
This example is based on the emptyExample
project in openFrameworks. Before running it, copy the files vox1.wav
to vox6.wav
into the bin/data
folder of your project.
For simplicity, we place all constants and variables not in the class testApp
definition, but right at the beginning of the testApp.cpp
file, after the #include "testApp.h"
line:
const int N = 6; //Number of the samples ofSoundPlayer sound[ N ]; //Array of the samples float vol[ N ]; //Volumes of the samples
It is a best practice to use vector
instead of fixed arrays whenever it is possible. So it would be better to declare sound
and vol
as follows:
vector<ofSoundPlayer> sound; vector<float> vol;
Currently, such an approach does not work properly in openFrameworks for Mac OS X—the project plays just one sound due to an undesired interrelation between vector
and ofSoundPlayer
.
The setup()
function loads samples and sets up their parameters. Note how we place the samples uniformly in stereo panorama ranging from -0.5
to 0.5
using setPan()
:
void testApp::setup(){ //Load and set up the sound samples for ( int i=0; i<N; i++) { sound[i].loadSound( "vox" + ofToString( i + 1 ) + ".wav" ); sound[i].setLoop( true ); //Do some stereo panoraming of the sounds sound[i].setPan( ofMap( i, 0, N-1, -0.5, 0.5 ) ); sound[i].setVolume( 0 ); sound[i].play(); //Start a sample to play } //Decrease overall volume to eliminate volume overload //(audio clipping) ofSoundSetVolume( 0.2 ); }
The update()
function slowly changes the values of the vol
array using Perlin noise (see more details in Appendix B, Perlin Noise), and sets its values to the sample's volumes:
void testApp::update(){ float time = ofGetElapsedTimef(); //Get current time //Update volumes float tx = time*0.1 + 50; //Value, smoothly changed over time for (int i=0; i<N; i++) { //Calculate the sample volume as 2D Perlin noise, //depending on tx and ty = i * 0.2 float ty = i * 0.2; vol[i] = ofNoise( tx, ty ); //Perlin noise sound[i].setVolume( vol[i] ); //Set sample's volume } //Update sound engine ofSoundUpdate(); }
The first parameter for noise computation is as follows:
float nx = time*0.1 + 50;
It starts from 50
and increases by 0.1
for each second. These two constants set the initial distribution and the speed of fluctuations.
The second parameter is as follows:
float ty = i * 0.2;
It is equally distributed from 0.0
to (N-1) * 0.2
. Parameter 0.2
specifies the smoothness of just the volumes distributed in the given time. Increasing this value leads to smoothness decreasing.
The draw()
function draws current volumes as narrow vertical rectangles:
void testApp::draw(){ ofBackground( 255, 255, 255 ); //Set the background color //Draw volumes as vertical lines ofSetColor( 0, 0, 0 ); for (int i=0; i<N; i++) { ofRect( i * 20 + 100, 400, 5, -vol[i] * 300 ); } }
When you run this example, you will hear an evolving sound and see slow moving lines which correspond to the current levels of each of the six playing samples:
Note that normally testApp::update()
runs not more than 60 frames per second. And changes of sound parameters at such a rate can be audible. So the described technique of controlling volumes is very simple, but resulted changes in sound can be not so smooth as it should be. To reach the perfect sound, you need to change the parameters smoothly for each audio sample. See the example of such parameters changing techniques in the The PWM synthesis example section.