recette préparation installation bonjour sinus ! boîte humaine

Boîte humaine

Un exemple un peu plus complet qui réalise une petit application et son interface Qt : une boîe à rythme humaine groovy. Nous vous conseillons d'être familiers avec la programmation Qt pour les parties concernant les éléments d'interface et les mécanismes de signaux et slots.

Pour lire les fichiers .wav utilisés dans la boîte on va utiliser libsndfile que, si vous l'avez pas déjà, vous pouvez trouver là :

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

Le code

Le code de cet exemple se trouve dans le dossier examples/human_box du répertoire d'installation de la Marmite. Nous allons en détailler ici les composants :

MPlaySampleProcess

Nous allons créer un nouveau processus ( MProcess ) qui sera en charge de la lecture d'un sample à partir d'un fichier. À sa création il lira le contenu du fichier et stockera les données du sample dans la mémoire. Il recopiera ensuite, sur demande de son client, ce sample bout par bout dans dans le buffer d'un port de sortie.

MPlaySampleProcess.h

#ifndef M_PLAY_SAMPLE_PROCESS
#define M_PLAY_SAMPLE_PROCESS

#include "core/MProcess.h"

class MClientAudioOutputPort;

class MPlaySampleProcess : public MProcess

Le processus doit hériter de la classe MProcess qui est l'interface générale des processus.

{

public :

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

La création d'un MPlaySampleProcess nécessite d'indiquer le nom d'un fichier ( fileName ) et le port de sortie d'un client ( audioOutput ) vers lequel les bouts du sample seront écrits.

J'utilise ici, comme souvent, une méthode statique à la place du constructeur ( qui reste privé ) ce qui permet d'éviter la création d'objets inutilisables. Si, par exemple, le nom de ficher n'est pas valide, la fonction retournera NULL et aucun objet ne sera créé. Ça s'appelle une usine ( bien que celle-là en soit une très basique ) et c'est bien.

  void Run ( unsigned int n );

La méthode principale du processus, elle est héritée de MProcess. C'est elle qui sera invoquée automatiquement par le client et qui doit réaliser les traitements du processus.

  void Play ( );

La méthode qu'il faudra invoquer pour relancer la lecture du sample au début.

protected :

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

Le constructeur prend comme paramètres les données audio du sample ( sample ) qui auront déjà été lues par l'usine Create, la longueur de ces données, le port de sortie du client et un nom à donner au processus. On initialise les variables de l'objet.

  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"

Les interfaces dont on va avoir besoin :

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

L'usine que l'on utilise pour créer le processus. Ce sera elle qui sera en charge de lire le fichier wav.

{

  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;
  }

On essaie d'ouvrir le fichier avec libsndfile.

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

Allocation de la place nécessaire pour les données du sample. On alloue un espace plus grand que le sample afin de stocker du blanc à la fin. La taille de ce blanc correspond à la taille du buffer des ports JACK. Ce blanc sera utilisé pour ne rien jouer lorsque la lecture du sample sera finie ( c'est pas très joli mais c'est moins compliqué ).

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

Lecture des données du fichier dans sample.

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

On rajoute le blanc à la fin de sample.

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

  return playSample;

On crée enfin, puisque tout c'est bien passé, l'objet que l'on retourne.

}

void MPlaySampleProcess::Run ( unsigned int n )

La méthode Run sera appellée automatique par le client lorsqu'il sera lui-même appellé par JACK. On y réalise donc la lecture du sample dans le port du client.

{

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

  _position += n;

On recopie dans le buffer du port du client, le bout du sample correspondant à la position de lecture courante _position. Et on l'avance d'autant.

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

Après cette lecture, si le sample a été completement lu ( la position de lecture courante est après la fin des données du fichier ), on boucle sur la partie de blanc que l'on a placé à la fin de _sample. Malin !

}

void MPlaySampleProcess::Play ( )

Le client invoquera cette méthode pour relancer la lecture du sample au début.

{

  _position = _sample;

On replace la position de lecture courant au début du sample.

}

MMultiSampleClient

Il nous faut maintenant créer un client ( MClient ) qui gérera la lecture indépendante de plusieurs samples ( 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
{

Le client doit hériter de la classe MClient qui est l'interface générale des clients. Rappellons qu'il y a deux manières de créer ses propres clients : soit, comme ici, en héritant de MClient soit, comme dans le bonjour sinus !, en utilisant la classe MUserClient.

public :

  static MMultiSampleClient * Create ( const string & name );

Pour la création, comme pour MPlaySampleProcess, on utilise une petite usine pour éviter de retourner un client bidon.

  void AddSample ( const string & fileName );

Cette méthode rajoute un sample au client. Le sample est crée à partir du fichier fileName.

  void Play ( const unsigned int n );

Lance la lecture du nième sample du client.

protected :

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

Le constructeur de ce client ne prend pas plus d'arguments que la création d'un MClient de base.

};

#endif // M_MULTI_SAMPLE_CLIENT

MMultiSampleClient.cpp

#include "MMultiSampleClient.h"

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

Nous allons avoir besoin de l'interface core/MProcessComposite.h qui nous permettra de crée pour le client un processus composite ( MProcessComposite ) composé de plusieurs sous-processus fils.

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

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

On tente de créer un client JACK avec la méthode createJackClient. Si la méthode échoue on laisse tomber.

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

On crée notre client MMultiSampleClient pour le client JACK qu'on vient de créer.

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

La méthode setCallbackProcess du client définit le processus qui sera appellé par la méthod Run du client lorsqu'il sera lui-même rappellé par JACK. La Marmite cache ce méchanisme et le processus du client sera appellé automatiquement.

Ici le processus de ce client sera un processus composite. Un processus composite se compose de plusieurs processus fils et qu'il enchaîne automatiquement lorsqu'il est appellé appellé via sa méthode Run.

  return multiSample;

On retourne le client créé puisque tout c'est bien passé.

}

void MMultiSampleClient::AddSample ( const string & fileName )

Cette méthode ajoute la gestion d'un sample au client.

{

  MClientAudioOutputPort * audioOutput = createAudioOutput ( fileName );

On crée un port de sortie qui correspondra à la lecture du nouveau sample.

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

On crée un MPlaySampleProcess pour la lecture de ce sample. Le processus écrira les données du sample dans le port qu'on vient de créer.

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

On ajoute au processus composite du client le MPlaySampleProcess qu'on vient de créer.

}

void MMultiSampleClient::Play ( const unsigned int n )

Cette méthode lance la lecture du nième sample du client.

{

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

On appelle tout simplement la méthode Play du nième fils du processus du client ( qui est le nième MPlaySampleProcess qu'on a créé via la méthode AddSample ).

}

MBox

Cet objet est un widget Qt. Il se chargera de l'affichage de la boîte à rythme, de la création d'un MMultiSampleClient pour jouer les samples et du méchanisme de séquençage.

MBox.h

#ifndef M_BOX_H
#define M_BOX_H

#include 

#define TRACKS 13
#define BEATS 16

TRACKS : nombre de pistes = nombre de samples à jouer.
BEATS : nombre de temps de la séquence.

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

class MBox : public QWidget
{

  Q_OBJECT

MBox est un widget Qt, il devra donc hériter de la classe QWidget et, puisque nous voulons y rajouter des slots, il nous faut inclure la macro Q_OBJECT.

public:

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

La création de base d'un widget.

public slots :

  void playBeat ( );

Joue un temps.

  void changeTempo ( int msec );

Change le tempo de lecture : 1 temps toutes les msec millisecondes.

protected :

  static char * bank[];

Les noms des fichiers à utiliser pour créer les samples.

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

Les éléments de l'interface Qt.

};

#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" };

Les noms des fichiers sons que l'on va utiliser. Ils doivent être en format wav, mono et avec une fréquence d'echantillonnage équivalente à celle de JACK ( ici 44'100 Hz ).

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

  // draw widget

On commence la création de la MBox par le dessin de l'interface.

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

Le slider utilisé pour régler le 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] );

On crée un boutton pour chaque temps de chaque piste. Les bouttons sont bi-stables ( toggle buttons ).

On rajoute une petite bidouille ( avec srandom et random ) pour que, aléatoirement, au démarrage certains boutons soient déjà enfoncés.

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

  }

  // initialize audio

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

On crée le MMultiSampleClient qui lira les samples.

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

On ajoute les samples au 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 ) );
  }

On connecte chacun des ports de sortie du client avec les ports des entrées physiques ( sorties de la carte son ) qui sont proposés par le Supervisor.

  // start looping

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

On démarre un timer qui se déclanchera pour chaque temps. La durée initiale est fixée par le slider de tempo. Par défaut, le timer tourne en boucle.

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

On connecte les signaux du timer ( chaque temps ) au slot playNext qui jouera le temps suivant. On connecte également le changement de valeur du slider au slot changeTempo qui mettra à jour l'interval de temps du timer.

}

void MBox::playBeat ( )
{

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

Pour chaque piste, on lance la lecture du sample correspondant si le bouton du temps _position est enfoncé. Ce slot est connecté au signal du timer.

  _position = ( _position + 1 ) % BEATS;

Avance _position et boucle si on est arrivé à la fin.

}

void MBox::changeTempo ( int msec )
{

  _beatTimer->changeInterval ( msec );

Change l'interval de temps du timer. Ce slot est connecté au signal du changement de valeur du slider de tempo.

}

Programme principal

Il suffit maintenant de crée un programme qui ouvrira un widget MBox.

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 ( );

}

Rien de bien sorcier, c'est un main de Qt tout à fait classique.

Groovy ?

Des oreilles attentives pourront remarquer que la régularité de la boîte à ryhtme n'est pas très bonne. Les temps semblent peu précis ... Cela est du aux simplification que l'on a faite pour le déclanchement de la lecture des samples. Une fois le timer écoulé, si un sample doit être joué, la _position dans le MPlaySampleProcess est remise au début du sample. Mais les premières données audio ne seront transmises à JACK qu'au prochain callback et seront inscrites forcement au début du buffer. Tout cela n'est pas très précis mais bon ... ça fait un peut groover le truc :)

Les méthodes employées dans cet exemple ne sont peut être pas très académiques mais t'as-t-on déjà proposer d'en faire autant avec moins de 200 lignes de code ?