Playing sound samples

The openFrameworks' ofSoundPlayer class is designed for playing and controlling sound samples. The basic usage of the ofSoundPlayer sound object is the following:

  • Loading a sound sample, specifying its filename using:
    sound.loadSound( fileName );
  • Playing the sample using:
    sound.play();
  • Updating the sound engine using the global function:
    ofSoundUpdate();

    You need to call the ofSoundUpdate(); function in testApp::update() for all the samples to play correctly.

There are a number of functions for controlling sample playback. They are as follows:

  • The stop() function stops sample playing.
  • The getIsPlaying() function returns true if our sample is currently playing.
  • The setPaused( pause ) function enables or disables pause in sample playing, with pause of type bool.
  • The 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.
  • The getPosition() function returns the current playing position as a float value from 0.0 to 1.0.
  • The setPositionMS( ms ) function sets the sample playing position in milliseconds, where ms has type int.
  • The getPositionMS() function returns the int value with the current sample playing position in milliseconds.
  • The setLoop( looping ) function enables or disables the sample loop mode in which the sample repeats infinitely, looping has a type bool.
  • The 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.

    Note

    If you enable the multiplay mode for the sample and play many sounds by calling sample.play(), please note that you can change the playing parameters of the last sound that started. So you should call sample.play() and then change its parameters before the next call of sample.play().

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:

  • The isLoaded() function returns true if the sound is successfully loaded and is ready to play.
  • The 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:

  • The 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.
  • The setVolume( vol ) function sets the volume of the sample, where vol is a float value from 0.0 to 1.0.
  • The 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).
  • The 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.

The bouncing ball example

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.

Note

This is example 06-Sound/01-BouncingBall.

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.

Note

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.

Note

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:

The bouncing ball example

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.

The singing voices example

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.

Note

This is example 06-Sound/02-SingingVoices.

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

Tip

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:

The singing voices example

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.

..................Content has been hidden....................

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