The Allegro Wiki is migrating to github at https://github.com/liballeg/allegro_wiki/wiki

Echo effect

From Allegro Wiki
Jump to: navigation, search

You want to make an echo sound effect you ask? Well, there are basically two approaches:

  1. play the same sample several times, each time with lower volume
  2. code a proper echo effect (or delay as it's sometimes called)

The first is a rather simple brute force approach which requires a lot of resources (i.e. voices) so I won't go into that. Instead I'll explain how to do it properly. It's a little complicated at first but once you get whole system layed out you can very easily add all sorts of other interesting effects, such as filters, reverb, chorus, dynamic compression, distortion, flange, phaser, etc.

What you need is basically a whole digital sound processing (DSP) pipeline system with mixing and processing and all that stuff. Built-in Allegro sample and voice function won't do, instead you need to write your own sample playback and mixing code which writes its output to an Allegro AUDIOSTREAM. Actually you could just as well do something else with it, for example you could stream it to a wave file on disk or you could use some other form of output.

A DSP pipeline might look something like this:


  _______________      ______________      _______________
 |               |    |              |    |               |
 |   generator   |--->|    effect    |--->|     mixer     |--->AUDIOSTREAM
 |_______________|    |______________|    |_______________|

You can chain more effects and you can connect more generators to a single effect or mixer. In fact, your actual pipeline might be a complex tree or graph with many nodes and connections.

Each of the above boxes is called a "machine". There are basically three kinds of machines:

  • generators
  • effects
  • master/output

Generators generate sound in some way, most typically by playing back a sample loaded from disk. They only have output and no input. Effects take sound data at the input level, process it and output the processed sound. The master machine doesn't have an output as such, but only mixes all the inputs together and passes the output to some outside device such as an Allegro AUDIOSTREAM.

Once you know all that, creating a DSP system is easy. Just check the code in audiobase.h and audiobase.cpp files.

<highlightSyntax language="cpp">#ifndef AUDIOBASE_H

  1. define AUDIOBASE_H
  1. include <allegro.h>
  2. include <string.h>
  3. include <vector>

// Number of samples in each buffer. A larger buffer will cause more // latency, but a too small buffer can cause the sound to stutter. At // 22kHz a 1k buffer corresponds to ~46 ms.

  1. define BUFFER_SIZE 1024

// Sampling frequency for our audio stream.

  1. define SAMPLING_FREQUENCY 22050

// Base class for all audio modules. All processing and mixing is done // in floating point accuracy, with signed values for samples. Each // machine can have an arbitrary number of inputs and can connect to // an arbitrary number of outputs. When connecting machines, special // care should be taken to avoid making closed loops. class Machine {

  protected:
     // output buffer - machines to which this machine connects
     // will read their input data from this buffer; only applicable
     // for machines that actually produce output
     float *out;
  
     // input buffer - outputs from all input machines will be mixed
     // into this buffer, processed and written to the output buffer;
     // only applicable for machines that have input
     float *in;
  
     // an array of input machines for this machine
     std::vector<Machine *> inputs;
  
     // clears the input buffer
     void ClearInputBuffer();
  
  public:
     Machine();
     virtual ~Machine();
  
     // mix sound data into the input buffer
     void Input(float *in);
  
     // process input sound
     virtual void Process();      
  
     // return output sound
     float *Output();
  
     // connect an input machine
     void Connect(Machine *m);

};


// base class for all generators class Generator : public Machine {

  protected:
     bool playing;
  
  public:
     Generator();
     ~Generator();
  
     virtual void Play();
     virtual void Stop();

};


// base class for all effects - nothing special so far class Effect : public Machine {

  public:
     Effect();
     ~Effect();

};


  1. endif //AUDIOBASE_H
</highlightSyntax>

<highlightSyntax language="cpp">#include "audiobase.h"


Machine::Machine() {

  // create input and output buffers and fill them with zeroes
  out = new float[BUFFER_SIZE];
  in = new float[BUFFER_SIZE];
  for (int i=0; i<BUFFER_SIZE; i++) {
     in[i] = 0.0f;
     out[i] = 0.0f;
  }

}


Machine::~Machine() {

  delete [] out;
  delete [] in;

}


// fills the input buffer with zeroes - called after processing the input // data to initialize the input buffer for the next time void Machine::ClearInputBuffer() {

  for (std::vector<Machine *>::iterator i = inputs.begin(); i != inputs.end(); ++i) {
     Machine *m = *i;
     m->ClearInputBuffer();
  }
  for (int i=0; i<BUFFER_SIZE; i++) {
     in[i] = 0.0f;
  }

}


// mixes a buffer of sound with the input buffer - assumes the input // buffer has been cleared before mixing starts void Machine::Input(float *in_) {

  for (int i=0; i<BUFFER_SIZE; i++) {
     in[i] += in_[i];
  }

}


// processes the input buffer and stores the results in the output buffer void Machine::Process() {

  // for each input machine
  for (std::vector<Machine *>::iterator i = inputs.begin(); i != inputs.end(); ++i) {
     Machine *m = *i;
     
     // process the input first
     m->Process();
     
     // mix the input machine's output with the input buffer
     Input(m->Output());
  }

}


float *Machine::Output() {

  return out;

}


void Machine::Connect(Machine *m) {

  inputs.push_back(m);

}


Generator::Generator() : Machine(), playing(false) { }


Generator::~Generator() {

  Stop();

}


void Generator::Play() {

  playing = true;

}


void Generator::Stop() {

  playing = false;

}


Effect::Effect() : Machine() { }


Effect::~Effect() { }

</highlightSyntax>

The code is quite self explanatory and loaded with comments so it shouldn't be too hard to understand.

Sample.h and sample.cpp implement a simple generator module that loads a sample and plays it back. The purpose of this "article" is to explain how to make an echo effect so the sampler is very simple, just enough to test the system. It assumes the sample is in 8bit 22kHz mono format (such as welcome.wav from the Allegro demo game) and can only play it at a fixed pitch, volume and panning.

<highlightSyntax language="cpp">#ifndef SAMPLE_H

  1. define SAMPLE_H
  1. include "audiobase.h"

// A simple sampler module. Just loads one sample and plays it on demand. // Assumes the sample is 8 bit, mono and sampled at the same sampling rate // as the output sampling rate. A real-life application would either convert // all loaded samples to the same uniform format or perform the necessary // conversions on the fly. class Sampler : public Generator {

  private:
     SAMPLE *smp;
     int pos;
  
  public:
     Sampler();
     ~Sampler();
     
     void Load(const char *filename);
     void Play();
     void Stop();
     void Process();

};


  1. endif //SAMPLE_H
</highlightSyntax>

<highlightSyntax language="cpp">#include "sample.h"


Sampler::Sampler() : Generator() {

  smp = NULL;
  pos = 0;

}


Sampler::~Sampler() {

  if (smp) {
     destroy_sample(smp);
     smp = NULL;
  }

}


void Sampler::Load(const char *filename) {

  if (smp) {
     destroy_sample(smp);
     smp = NULL;
  }
  
  smp = load_sample(filename);

}


void Sampler::Play() {

  Generator::Play();
  pos = 0;

}


void Sampler::Stop() {

  Generator::Stop();
  pos = 0;

}


void Sampler::Process() {

  Generator::Process();
  // copy the next chunk of sample data to the output buffer
  if (playing && smp) {
     int len = smp->len - pos;
     len = MIN(len, BUFFER_SIZE);
     int i;
     
     for (i=0; i<len; i++, pos++) {
        // sample data needs to be converted from unsinged 8 bit format
        // signed floating point format - in an actual application this
        // step would have been performed at sample load time
        out[i] = (float)(((unsigned char *)smp->data)[pos] - 128);
     }
     
     // clear the rest of the buffer to 0
     for (; i<BUFFER_SIZE; i++) {
        out[i] = 0.0f;
     }
     if (pos >= smp->len) {
        Stop();
     }
  }
  // there's nothing to play -> clear the output buffer to 0
  else {
     for (int i=0; i<BUFFER_SIZE; i++) {
        out[i] = 0.0f;
     }
  }

}

</highlightSyntax>

The master module (master.h and master.cpp) is also quite simple but it really doesn't need to be much more complex. It just mixes the inputs and writes them to an Allegro AUDIOSTREAM buffer.

<highlightSyntax language="cpp">#ifndef MASTER_H

  1. define MASTER_H
  1. include "audiobase.h"

// Master audio module for all Allegro AUDIOSTREAM mixing. There should be // one Master in a program to which all sound generators should be connected // either directly or through effects. The master writes output to an Allegro // audiostream which is created in the constructor and destoryed in the // destructor. The Process method should be called in regular intervals. class Master : public Machine {

  private:
     AUDIOSTREAM *stream;
  
  public:
     Master();
     ~Master();
  
     void Process();

};

  1. endif //MASTER_H
</highlightSyntax>

<highlightSyntax language="cpp">#include "master.h"

Master::Master() : Machine() {

  // Start playing an audiostream. 8 bit, 22kHz, mono for sake of
  // simplicity. Should be 16 bit, 44kHz, stereo in the final product.
  stream = play_audio_stream(BUFFER_SIZE, 8, FALSE, SAMPLING_FREQUENCY, 255, 128);

}


Master::~Master() {

  stop_audio_stream(stream);

}


// This should be called at regular intervals. Queries whether the underlying // audiostream requires its buffer to be updated and if it does, it updates // it by mixing in the outputs from all the input machines. void Master::Process() {

  // does audiostream need its buffer updated with new data?
  unsigned char *buf = (unsigned char *)get_audio_stream_buffer(stream);
  if (buf) {
     // process all input machines
     Machine::Process();
     
     // clear the audiostream buffer (unsinged values, 128 is 0 DC)
     memset(buf, 128, BUFFER_SIZE);
     // copy the input buffer over to the audiostream buffer, converting
     // the data to the appropriate format along at the same time
     for (int i=0; i<BUFFER_SIZE; i++) {
        // each sample is converted to unsigned format and clipped to [0,255]
        buf[i] = MID(0, (int)in[i]+0x7f, 0xff);
     }
     // let the audiostream know it got new data to play
     free_audio_stream_buffer(stream);
     
     // clear the input buffer to prepare for the next "frame"
     ClearInputBuffer();
  }

}

</highlightSyntax>

Now to the actual echo (delay) effect. The code is in echo.h and echo.cpp and is also well commented.

<highlightSyntax language="cpp">#ifndef ECHO_H

  1. define ECHO_H
  1. include "audiobase.h"

// A simple echo (or delay) class. Stores just enough history data // and mixes it with the input. History data is amplified with values // less than 1 to produce echoes. class Echo : public Effect {

  private:
     float *history;   // history buffer
     int pos;      // current position in history buffer
     int amp;      // amplification of echoes (0-256)
     int delay;      // delay in number of samples
     int ms;         // delay in miliseconds
  
     float f_amp;   // amplification (0-1)
  
  public:
     Echo();
     ~Echo();
     
     void SetDelay(int ms);
     void SetAmp(int amp);
     int GetDelay();
     int GetAmp();
     void Process();

};


  1. endif //ECHO_H
</highlightSyntax>

<highlightSyntax language="cpp">#include "echo.h"


Echo::Echo() : Effect() {

  history = NULL;
  SetDelay(200);      // default delay is 200ms
  SetAmp(128);      // default amplification is 50%
  pos = 0;

}


Echo::~Echo() {

  delete [] history;
  history = NULL;

}


void Echo::SetDelay(int ms) {

  // calculate number of samples needed for history
  int newDelay = ms * SAMPLING_FREQUENCY / 1000;
  
  // create new history buffer
  float *newHistory = new float[newDelay];
  for (int i=0; i<newDelay; i++) {
     newHistory[i] = 0.0f;
  }
  // if there already is a history buffer, fill the new one with
  // old data (might not work right in all situations)
  if (history) {
     int howMuch = delay-pos;
     howMuch = MIN(howMuch, newDelay);
     for (int i=0, j=pos; i<howMuch; i++, j++) {
        newHistory[i] = history[j];
     }
     if (howMuch < newDelay) {
        int i=howMuch;
        howMuch = newDelay - howMuch;
        howMuch = MIN(howMuch, delay);
        howMuch = MIN(howMuch, pos);
        for (int j=0; j<howMuch; i++, j++) {
           newHistory[i] = history[j];
        }
     }
     delete [] history;
     
  }
  
  // remember new values
  history = newHistory;
  pos = 0;
  delay = newDelay;
  this->ms = ms;

}


// amp is in [0, 256] where 0 means no echoes and 256 means infinite echoes void Echo::SetAmp(int amp) {

  this->amp = amp;
  f_amp = (float)amp/256.0f;

}


int Echo::GetDelay() {

  return ms;

}


int Echo::GetAmp() {

  return amp;

}


// do the echo effect void Echo::Process() {

  Effect::Process();
  
  // mix each sample in the input buffer with the appropriately
  // delayed and amplified sample in the history buffer
  float smp;
  for (int i=0; i<BUFFER_SIZE; i++, pos++) {
     // wrap around in the history buffer
     if (pos >= delay) {
        pos = 0;
     }
     
     smp = history[pos];         // read sample from history
     smp *= f_amp;            // amplify
     smp += in[i];            // add to the matching sample from the input buffer
     out[i] = smp;            // write sample to output
     history[pos] = smp;         // write sample to the history buffer
  }

}

</highlightSyntax>

The algorithm the echo module uses to achieve its effect is simple:

  1. . out = in + history*amp
  2. . history = out

The input data is copied to the output but at the same time it is mixed with the data that was played a fixed number of miliseconds before, but amplified with a factor less than 1. Then the output data is saved so it can be used the next time. The effect has two parameters: delay and amplification. Delay is given in miliseconds and this determines the length of the history buffer that is needed to produce the echo effect. If delay is 500 ms, the module must at all times store the sound data that was played 500 ms ago so it can be mixed with the current input. Amplification determines how fast the echoes die out and should be a number between 0 and 1.

That's it. The main.cpp file shows how this system is used. A master module is created, and echo module is connected to it and a sampler module is connected to the echo module. In the main loop the master's Process() method is called continuously and every time the user presses the space key, the sample is played and passed through the echo effect.

Note: to run the test you will need an appropriate sample. The test sample module assumes the sample is 8bit, 22kHz. I used the "Welcome to Allegro" sample from the Allegro demo game.

<highlightSyntax language="cpp">#include "audiobase.h"

  1. include "master.h"
  2. include "sample.h"
  3. include "echo.h"


int main() {

  // setup everything - leave error checking for later :)
  allegro_init();
  set_color_depth(32);
  set_gfx_mode(GFX_AUTODETECT_WINDOWED, 320, 240, 0, 0);
  install_keyboard();
  install_sound(DIGI_AUTODETECT, 0, 0);
  BITMAP *buffer = create_bitmap(SCREEN_W, SCREEN_H);
  // create a sampler and load an 8bit, 22kHz, mono sample
  Sampler smp;
  smp.Load("welcome.wav");
  
  // create an echo effect and connect the sampler to it
  Echo echo;
  echo.Connect(&smp);
  
  // create the master module and connect the echo to it
  Master master;
  master.Connect(&echo);
  // main loop
  while (!key[KEY_ESC]) {
     // handle keyboard input (play sample on KEY_SPACE)
     while (keypressed()) {
        int c = readkey()>>8;
        
        switch (c) {
           case KEY_SPACE:      smp.Play();      break;
        }
     }
     
     // poll the DSP pipeline
     master.Process();
  }
  // clean up
  destroy_bitmap(buffer);
  return 0;

} END_OF_MAIN()

</highlightSyntax>

Adding other effects is very easy. Just derive a class from Effect and implement the Process() method. Filters, distortion, dynamic compression and other effects like these are almost trivial to code. Others like reverb, chorus, flange, phaser, and so on might be a little trickier but lots of tutorial exist on the subject. The only problem with this system is that you need to write your own sample playback routines which in light of the fact that Allegro already has such functionality means that you're reinventing the wheel. But then again, this system also gives you the ability to do much more than just play samples. You can just as easily write a complete software synthesizer :)