Reggae tutorial: Playing a sound from memory

From MorphOS Library

Grzegorz Kraszewski

Introduction

While playing a sound from file is the most common way, there are applications where it has several disadvantages. When a sound is short, played many times and low latency is required, playing this sound from memory will be better option. Seeking in the sound, or restarting it will be substantially faster then, as it does not involve any disk activity.

On the other hand playing from memory should be used with care. Audio data are very space consuming usually. Five seconds audio effect stored as PCM in audio CD quality takes 861 kB of memory. A solution for this problem is to use compressed formats and let Reggae decompress it on the fly.

There are two ways of placing audio file in memory. Firstly, it can be loaded from disk. Secondly the audio file contents may be embedded into the executable file.

Buffering sound file from disk

In this method sounds are stored as files separated from application executable. Memory buffers for sounds are allocated dynamically. This approach allows for easy changing of sounds or – for example – delivering low quality versions for machines having less memory, or even running without sound, when memory is low. The code and error handling is a bit more complicated however. The process of sound buffering does not involve Reggae at all. File is opened and sized, then buffer is allocated, file is read into it and closes. These tasks may be performed by native dos.library calls (Open(), Read(), Close()), or C standard library calls (fopen(), fread(), fclose()). As the later ones are just wrappers on native calls, using native ones is recommended, unless code portability is important. The following code buffers a file in memory with minimal error checking:


BPTR handle;
LONG size;
APTR buffer;
struct FileInfoBlock *fib;

if (fib = AllocDosObject(DOS_FIB, NULL))
{
  if (handle = Open("PROGDIR:sounds/anysound.wav", MODE_OLDFILE))
  {
    if (ExamineFH(handle, fib))
    {
      size = fib->fib_Size;

      if (buffer = AllocVecTaskPooled(size))
      {
        if (Read(handle, buffer, size) == size)
        {
          /* use buffer as memory.stream data here */
        }
        FreeVecTaskPooled(buffer);
      }
    }
    Close(handle);
  }
  FreeDosObject(DOS_FIB, fib);
}

Programmers new to MorphOS may notice some new things in the code above. While not related to Reggae, they are worth some explanation. The first one is PROGDIR: assign (or link in Unix nomenclature). It just means the directory, where running executable file is located. It is then an easy way to refer to application data with relative paths. When user moves the application directory somewhere, paths using PROGDIR: are still valid.

The second thing is AllocVecTaskPooled() call. Every MorphOS process has an automatically assigned memory pool, which is disposed when process ends. Using this pool for allocations, an application need not track them, as all memory not freed explicitly with FreeVecTaskPooled() will be freed when memory pool is disposed.

Embedding sound in application executable

Embedding sound in the code has the advantage of simplicity. The application is more self-contained. There are no disk operations involved, so there is no need for error handling code. On the other hand it can make executable very big, and there is no chance for user to change sounds. Audio file of any fomat can be converted to C code (as a large table) with BinToC tool. Generated source is added to the project and compiled. Then address of the table (denoted in C just as the table name) and its length in bytes, are passed as parameters to memory.stream object (see code fragment below).

Memory stream as data source

Reggae uses the memory.stream class to access data located in system memory. Its usage is similar to file.stream, there are some differencies however. The first one is stream name. For memory.stream it is a string containing stream address as a hexadecimal number, like for example "2749FA0C". MMA_StreamName attribute is not used often however. One usually has the address just as number, not as text. Converting it to text just to make Reggae to convert it back to number makes not much sense. Then MMA_StreamHandle attribute comes with help. Its value is just the address of stream, passed as number. Another very important attribute is MMA_StreamLength. Memory based streams have no "natural" end. When one is reading a file, DOS just reports EOF (end of file) condition, when the file ends. In memory one can read endlessly, until he hits the end of physical memory space. That is why MMA_StreamLenght is a required attribute for memory streams. Reggae will refuse to create a stream object, if the attribute is not specified. Note also that the attribute in general is 64-bit one, and takes a pointer to 64-bit number. Passing just a 32-bit number as the value is a common mistake here. Code snippet below shows typical creation of memory stream object from a sound embedded in executable file:


CONST UBYTE SoundData [12837] = { /* audio data here */ }; /* The length is just example. */
QUAD length = 12837;
Object *sound;

sound = MediaNewObject(
  MMA_StreamType, "memory.stream",
  MMA_StreamHandle, SoundData,
  MMA_StreamLength, &length,
TAG_END);


When sound is buffered from file, one has to check the file size first, then allocate a buffer and load the file into it using usual dos.library calls, or C standard library calls. The process is shown in the complete example source code. After the buffer is loaded, memory.stream object is created the same way as above.

Playback and playback control

To play the sound, one connects created media object with an audio.output object, exactly the same as for playing from disk. There is also no difference in controlling the playback, or waiting for sound end. Thanks to stream abstraction Reggae "does not care" what the stream is. Just seek and retrigger operations are much faster. It is important for short sound effects retriggered many times (think of a shot sound in a game). The example code linked above allows user for retriggering the sound pressing ENTER key. It can be done very fast without delays, assuming some simple audio compression is used (just press and hold ENTER, retrigger rate will be as fast as key repetition rate set in system preferences).

The example shows also "launch and forget" strategy of playing sounds with Reggae. There is no check for sound end. MMM_Stop() just stops and does seek to the start. Then MMM_Play() starts playback. It does not matter if retrigger happens while previous sound is still playing or not. There is also no sound end check when user stops the program. Disposing an active (playing) audio.output object is perfectly safe.