recipe preparation installation hello sine ! human box

Human box

A more interesting examples which creates a little application and it's Qt interfacet : a groovy human beat box. I advise you to be familiar with Qt programming for the parts of the code dealing with the elements of the interface and the signals and slots mechanism.

To read the .wav files included in the box, we will use the libsndfile library that you can find there, if you haven't got it already :

http://www.mega-nerd.com/libsndfile

The code

The code for this example is placed in the examples/human_box of the installation directory. We will detail here its components :

MPlaySampleProcess

We will now create a new process ( MProcess ) which will be in charge of reading a sound from a file. Upon creation, it will read the content of the file and store the audio data in memory. It will then copy, on demand of the client, parts of it in the buffer of an audio output port.

MPlaySampleProcess.h

#ifndef M_PLAY_SAMPLE_PROCESS
#define M_PLAY_SAMPLE_PROCESS

#include "core/MProcess.h"

class MClientAudioOutputPort;

class MPlaySampleProcess : public MProcess

The process has to inherit the MProcess class which is the base class for processes.

{

public :

  static MPlaySampleProcess * Create ( const string & fileName,
                                       MClientAudioOutputPort * audioOutput );

To create a MPlaySampleProcess we need to indicate the name of a file to read ( fileName ) as well as an output port from a client ( audioOutput ) in which the parts of the sound will be written to.

I'm using here, as I'm doing quite often, a static method instead of the constructor ( which stays protected ) ; this allows to avoid the creation of unusable objects. If, for example, the name of the file is not valid, the function will return NULL and no object will be created. This is called a factory ( though this one is a very basic one ) and it's good.

  void Run ( unsigned int n );

The most important method of the process, it is inherited from MProcess. This method will be automatically invocated by the client and have to realise the actual audio processing of the process.

  void Play ( );

The method to invocate in order to restart the playback of the sound from the beginning.

protected :

  MPlaySampleProcess ( float * sample, unsigned int length,
                       MClientAudioOutputPort * audioOutput, string name )
    : MProcess ( name ), _sample ( sample ), _length ( length ),
      _position ( sample + length ), _audioOutput ( audioOutput ) { };

The constructor takes as parameters the audio data of the sound ( sample ) that were already read by the factory Create, the length of those data, the client's output port and a name to be given to the process. We initialize the object's variables.

  float * _sample;
  unsigned int _length;
  float * _position;
  MClientAudioOutputPort * _audioOutput;
};

#endif // M_PLAY_SAMPLE_PROCESS

MPlaySampleProcess.cpp

#include "MPlaySampleProcess.h"

#include 
#include 
#include 

#include "core/MClientAudioOutputPort.h"
#include "core/MSupervisor.h"

The interfaces that we will need :

MPlaySampleProcess * MPlaySampleProcess::Create ( const string & fileName,
                                           MClientAudioOutputPort * audioOutput )

The factory that we use to create the process. It will be in charge of reading the .wav file.

{

  SNDFILE * file;
  SF_INFO sfinfo;

  if ( ! ( file = sf_open ( fileName.c_str ( ), SFM_READ, & sfinfo ) ) ) {
    cerr << "error : could not open file : " << fileName << endl;
    puts ( sf_strerror ( NULL ) );
    return NULL;
  }

We try to open the file with libsndfile.

  float * sample;
  unsigned int length = sfinfo.frames;
  sample = ( float * ) malloc ( ( length + BufferSize ( ) ) *  sizeof ( float ) );

Allocates the space needed for the audio data of the sound. We allocate a larger space than the sound itself in order to store some silence at the end of it. The size of the silence corresponds to the size of the audio buffers provided by JACK. This silence will be used to stop the play back at the end of the sound ( it's not very nice but at least it's simple ).

  if ( sf_read_float ( file, sample, length ) != length ) {
    cerr << "error : could not read entire file : " << fileName << endl;
    return NULL;
  }

Reads the data of the file in sample.

  memset ( sample + length, 0, BufferSize ( ) );

Adds some silence at the end of sample.

  MPlaySampleProcess * playSample;
  playSample = new MPlaySampleProcess ( sample, length,
                                        audioOutput, "play_sample_" + fileName );

  return playSample;

Creates finally, as every went ok, the object and returns it.

}

void MPlaySampleProcess::Run ( unsigned int n )

The Run method will be called automatically by the client when it will be itself called back by JACK. We thus realize here the playback of the sound in the client's port.

{

  memcpy ( _audioOutput->Buffer ( ), & _sample[_position-_sample],
           sizeof ( float ) * n );

  _position += n;

We copy in the buffer of the client's port, the part of the sound corresponding to the current playback position _position. And we push it forward.

  if ( ( _position - _sample ) > ( int ) _length ) {
    _position = _sample + _length;
  }

After that, if the sound was completely read ( the current playback position is after the end of the file's data ), we loop on the silence part that we placed at the end of _sample. Hea !

}

void MPlaySampleProcess::Play ( )

The client will invocate this method to restart the playback of the sound from the beginning.

{

  _position = _sample;

We replace the current playback position to the beginning of the sound.

}

MMultiSampleClient

We'll now have to create a ( MClient ) client that will handle the independent playback of several sounds ( one sound on each of it's un sample différent sur chacun de ses ports de sortie ) et qui utilisera pour cela un MPlaySampleProcess pour chacun de ces samples.

MMultiSampleClient.h

#ifndef M_MULTI_SAMPLE_CLIENT
#define M_MULTI_SAMPLE_CLIENT

#include "core/MClient.h"

class MMultiSampleClient : public MClient
{

The client must inherit the MClient class which is the base interface for client. Let's recall that there is two ways of creation you rown clients : either, like this, by inheriting the MClient interface either, as described in the hello sine !, by using the MUserClient class.

public :

  static MMultiSampleClient * Create ( const string & name );

For the creation we use, as for MPlaySampleProcess, a small factory to avoid the creation of invalid clients.

  void AddSample ( const string & fileName );

This method add a sound to the client. The sound is created from the file fileName.

  void Play ( const unsigned int n );

Start the playback of the nth sample.

protected :

  MMultiSampleClient ( const string & name, jack_client_t * jackClient )
    : MClient ( name, jackClient ) { };

This client's constructor is taking no more arguments than the basic MClient's constructor.

};

#endif // M_MULTI_SAMPLE_CLIENT

MMultiSampleClient.cpp

#include "MMultiSampleClient.h"

#include "MPlaySampleProcess.h"
#include "core/MProcessComposite.h"

We will need the core/MProcessComposite.h interface that will allow us to create, for the client, a composite process ( MProcessComposite ) composed of several sub-processes.

MMultiSampleClient * MMultiSampleClient::Create ( const string & name )
{

  jack_client_t * jackClient;
  if ( ! ( jackClient = createJackClient ( name ) ) ) {
    return NULL;
  }

We try to create a JACK client with the createJackClient method. If the method fail we give up.

  MMultiSampleClient * multiSample;
  multiSample = new MMultiSampleClient ( name, jackClient );

We create our MMultiSampleClient client for the JACK client we just created.

  multiSample->setCallbackProcess ( new MProcessComposite ( "multi_sample_process" ) );

The setCallbackProcess method defines the process that will be call by the Run of the client when it will be itself called back by JACK. La Marmite hides this mecanism and the client's process will be called automatically.

Here, this client's process will be a composite process. A composite process is composed of several child processes that he chains up automatically when it is called via its Run method.

  return multiSample;

We return the client since everything when ok.

}

void MMultiSampleClient::AddSample ( const string & fileName )

This methods add a sound to be managed by the client.

{

  MClientAudioOutputPort * audioOutput = createAudioOutput ( fileName );

We create an output port that will correspond to the playback of the new sound.

  MPlaySampleProcess * playSample;
  playSample = MPlaySampleProcess::Create ( fileName, audioOutput );

We create a MPlaySampleProcess process which will play back this sound. The process will have to write the audio data of the sound in the port we just created.

  ( ( MProcessComposite * ) Callback ( ) )->AddChild ( playSample );

We add the MPlaySampleProcess we just created to the composite process of the client.

}

void MMultiSampleClient::Play ( const unsigned int n )

This method starts the playback of the nth sound of the client.

{

  MPlaySampleProcess * playSample = ( MPlaySampleProcess * ) Callback ( )->Child ( n );
  playSample->Play ( );

We simply call the Play of the nth child process ( which is the nth MPlaySampleProcess that was created via the AddSample method ).

}

MBox

This object is a Qt widget. It will be in charge of the display of the beat box, the creation of a MMultiSampleClient for playing back the sound and the sequencing mechanism.

MBox.h

#ifndef M_BOX_H
#define M_BOX_H

#include 

#define TRACKS 13
#define BEATS 16

TRACKS : number of tracks = number of sounds to play.
BEATS : number of beats.

class MMultiSampleClient;
class QTimer;
class QHBoxLayout;
class QVBoxLayout;
class QPushButton;
class QSlider;

class MBox : public QWidget
{

  Q_OBJECT

MBox is a Qt widget, it thus have to inherit the QWidget class and, since we want to add some slots to it, we have to include the Q_OBJECT macro.

public:

  MBox ( QWidget * parent = 0, const char * name = 0 );

The basic creation of a widget.

public slots :

  void playBeat ( );

Plays one beat.

  void changeTempo ( int msec );

Changes the tempo : 1 beat every msec milliseconds.

protected :

  static char * bank[];

The file names to use to create the sounds.

  MMultiSampleClient * _client;
  unsigned short _position;
  QTimer * _beatTimer;
  QVBoxLayout * _privateLayout;
  QSlider * _tempoSlider;
  QHBoxLayout * _trackLayout[TRACKS];
  QPushButton * _sampleButton[TRACKS][BEATS];

The Qt interface elements.

};

#endif // M_BOX_H

MBox.cpp

#include "MBox.h"

#include 
#include 
#include 
#include 
#include 
#include 

#include "MMultiSampleClient.h"
#include "core/MSupervisor.h"

char * MBox::bank[TRACKS] = { "a.wav", "b.wav", "c.wav", "d.wav",
                              "e.wav", "f.wav", "g.wav", "h.wav",
                              "i.wav", "j.wav", "k.wav", "l.wav", "m.wav" };

The names of the sound files we're going to use. They must be in the wav format, mono and we a sample rate corresponding to the sample of the JACK server ( 44'100 Hz in this case ).

MBox::MBox ( QWidget * parent, const char * name )
  : QWidget ( parent, name ), _position ( 0 )
{

  // draw widget

We start creating the MBox by drawing the interface.

  _privateLayout = new QVBoxLayout ( this, 0, 5 );
  
  _tempoSlider = new QSlider ( 100, 400, 25, 250, QSlider::Horizontal, this );
  _privateLayout->addWidget ( _tempoSlider );

The slider used to change the tempo.

  srandom ( time ( NULL ) );

  for ( unsigned short i = 0 ; i < TRACKS ; i++ ) {

    _trackLayout[i] = new QHBoxLayout ( 0, 0, 5 );
    for ( unsigned int j = 0 ; j < BEATS ; j++ ) {
      _sampleButton[i][j] = new QPushButton ( "", this );
      _sampleButton[i][j]->setToggleButton ( true );
      if ( ! ( random ( ) % 8 ) ) {
	_sampleButton[i][j]->toggle ( );
      }
      _trackLayout[i]->addWidget ( _sampleButton[i][j] );

We create a button for each beat of each track. The buttons are toggle buttons.

We add a small trick ( with srandom and random ) in order to toggle randomly some buttons at the creation.

    }
    _privateLayout->addLayout ( _trackLayout[i] );

  }

  // initialize audio

  _client = MMultiSampleClient::Create ( "human_box" );

We create the MMultiSampleClient client that will read the sounds.

  for ( unsigned int i = 0 ; i < TRACKS ; i++ ) {
    _client->AddSample ( bank[i] );
  }

We add the sounds to the client.

  _client->Activate ( );

  for ( unsigned int i = 0 ; i < TRACKS ; i++ ) {
    _client->AudioOutput ( i )->Connect ( Supervisor ( )->AudioInput ( 0 ) );
    _client->AudioOutput ( i )->Connect ( Supervisor ( )->AudioInput ( 1 ) );
  }

We connect each of the output ports of the client to the physical input ports proposed by the Supervisor ( which corresponding to the actual output of the soundcard ).

  // start looping

  _beatTimer = new QTimer ( this );
  _beatTimer->start ( _tempoSlider->value ( ) );

We start a timer that will signal each beat. The initial interval values is fixed by the tempo slider. By default, the timer is looping.

  connect( _beatTimer, SIGNAL ( timeout ( ) ), this, SLOT ( playBeat ( ) ) );
  connect( _tempoSlider, SIGNAL ( valueChanged ( int ) ),
           this, SLOT ( changeTempo ( int ) ) );

We connect the timer's signals ( each beat ) to the playNext slot that will play the next beat. We also connect the slider's change of value to the changeTempo slot that will update the time interval of the timer.

}

void MBox::playBeat ( )
{

  for ( unsigned int i = 0 ; i < TRACKS ; i++ ) {
    if ( _sampleButton[i][_position]->isOn ( ) ) {
      _client->Play ( i );
    }
  }

For each track, we start the playback of the corresponding sound if the button of the beat _position is down. This slot is connected to the timer's signal.

  _position = ( _position + 1 ) % BEATS;

Increments _position and loops if we reached the end.

}

void MBox::changeTempo ( int msec )
{

  _beatTimer->changeInterval ( msec );

Changes the timer's time interval. This slop is connected to the value changes of the tempo slider.

}

Programme principal

We just now have to create a program that will open a MBox widget.

HumanBox.cpp

#include 
#include "MBox.h"

int main( int argc, char * argv[] )
{

  QApplication app ( argc, argv );

  MBox * humanBox = new MBox ( );
  humanBox->setCaption ( "H U M A N B O X" );
  humanBox->show ( );
  humanBox->move ( 200, 200 );

  QObject::connect ( qApp,SIGNAL ( lastWindowClosed ( ) ), qApp, SLOT ( quit ( ) ) );
  return app.exec ( );

}

Nothing tricky, this is a very basic main for a Qt program.

Groovy ?

If you listen attentively, you would notice that the regularity of the box is not so good. The beats seams to be imprecise ... This is due to the simplifications that we made in the playback of the sounds. When the timer emits a signal, if a sound has to be played back, the _position of the MPlaySampleProcess is put back to the beginning of the sound. But the first audio data will be transmitted to JACK only for the next callback and will always be written at the beginning of the buffer. All that is no very accurate, but well ... ... it sounds groovy :)

The technics used in this example may not be very academic but have you already be offered to do that much with less than 200 lines of code ?