Skip to content

Allegro Vivace: Sound

Doug Thompson edited this page Nov 11, 2019 · 79 revisions

◀ Input


Some thoughts have a certain sound, that being equivalent to a form. Through sound and motion, you will be able to paralyze nerves, shatter bones, set fires, suffocate an enemy or burst his organs. We will kill until no Harkonnen breathes Arrakeen air. — Paul Atreides

Luckily, sound in Allegro 5 is markedly simpler than what this Paul bloke is on about. If you just want to load a sample and play it, your life will be easy. If you want to put the fear of God into your players by creating an all-encompassing orchestral soundscape, juggling lots of samples and music, your life will be a bit more complicated.

We'll start with the easy life.

Setting things up

Firstly, we'll need to link against the audio and audio codecs addons.

As before, Visual Studio users should enable these addons through the GUI. The rest of you will want to update your compile-and-run commands:

gcc hello.c -o hello $(pkg-config allegro-5 allegro_font-5 allegro_primitives-5 allegro_audio-5 allegro_acodec-5 --libs --cflags)
./hello

Once again, we're going to scale our code back so that it's distraction-free - so save any bizarre experimentations you may have done in the input section, because we're going back to a relatively clean slate. Grab the following code:

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_audio.h>
#include <allegro5/allegro_acodec.h>

void must_init(bool test, const char *description)
{
    if(test) return;

    printf("couldn't initialize %s\n", description);
    exit(1);
}

int main()
{
    must_init(al_init(), "allegro");
    must_init(al_install_keyboard(), "keyboard");

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 30.0);
    must_init(timer, "timer");

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
    must_init(queue, "queue");

    al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
    al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
    al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

    ALLEGRO_DISPLAY* disp = al_create_display(640, 480);
    must_init(disp, "display");

    ALLEGRO_FONT* font = al_create_builtin_font();
    must_init(font, "font");

    must_init(al_init_primitives_addon(), "primitives");

    must_init(al_install_audio(), "audio");
    must_init(al_init_acodec_addon(), "audio codecs");
    must_init(al_reserve_samples(16), "reserve samples");

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(disp));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    bool done = false;
    bool redraw = true;
    ALLEGRO_EVENT event;

    al_start_timer(timer);
    while(1)
    {
        al_wait_for_event(queue, &event);

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                // game logic goes here.
                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
            case ALLEGRO_EVENT_DISPLAY_CLOSE:
                done = true;
                break;
        }

        if(done)
            break;

        if(redraw && al_is_event_queue_empty(queue))
        {
            al_clear_to_color(al_map_rgb(0, 0, 0));
            al_draw_text(font, al_map_rgb(255, 255, 255), 640/2, 480/2, ALLEGRO_ALIGN_CENTER, "Silence");

            al_flip_display();

            redraw = false;
        }
    }

    al_destroy_font(font);
    al_destroy_display(disp);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

Compile and run; you'll agree that it produces no sound yet. Before we get to that, let's go over the ground work:

#include <allegro5/allegro_audio.h>
#include <allegro5/allegro_acodec.h>

You'll probably be getting used to including the header files for the addons we're using by now, so here we are. Likewise, we've initialized them before starting our game loop:

must_init(al_install_audio(), "audio");
must_init(al_init_acodec_addon(), "audio codecs");
must_init(al_reserve_samples(16), "reserve samples");

The only thing of note here is the call to al_reserve_samples. For the moment, all you need to know is that this tells Allegro to expect that we might play as many as 16 sounds at once. This might sound excessive, but you haven't seen (or heard) what's coming next.

Load sample, play sample

You'll need a sound to play, which should obviously be this elephant. It's WavSource's finest.

Download it and put it in your program's directory as elephant.wav. You can then load it into your program with al_load_sample. Add this to your init code:

ALLEGRO_SAMPLE* elephant = al_load_sample("elephant.wav");
must_init(elephant, "elephant");

Like the rest of stuff we've created or loaded, you'll want to destroy it when the program ends - al_destroy_sample will take care of this. Put this above the rest of your shutdown code:

al_destroy_sample(elephant);

We're going to make the sample play every time the E key is pressed. Get your towels ready. At the moment, our program behaves as it did before we tamed the keyboard in the previous section: every key quits. As before, we'll stop that from happening, and 'Esc' will be the kill switch.

So, change the ALLEGRO_EVENT_KEY_DOWN case as follows:

case ALLEGRO_EVENT_KEY_DOWN:
    if(event.keyboard.keycode == ALLEGRO_KEY_E)
        al_play_sample(elephant, 1.0, 0.0, 1.0, ALLEGRO_PLAYMODE_ONCE, NULL);
    if(event.keyboard.keycode != ALLEGRO_KEY_ESCAPE)
        break;

// keep this case as it is:
case ALLEGRO_EVENT_DISPLAY_CLOSE:
    done = true;
    break;

Also very important: update your call to al_draw_text.

al_draw_text(font, al_map_rgb(255, 255, 255), 640/2, 480/2, ALLEGRO_ALIGN_CENTER, "Knock knock, it's Nelly");

Compile and run. Press the E key. Listen to what you've created. Additionally, for a rapid-fire experience, swap ALLEGRO_EVENT_KEY_DOWN for ALLEGRO_EVENT_KEY_CHAR.

Sound engineering primer

al_play_sample is what's doing the work here. The parameters we've given it might look a bit cryptic, so let's explain it:

al_play_sample(
    elephant,              // the sample to play
    1.0,                   // gain ('volume')
    0.0,                   // pan ('balance')
    1.0,                   // speed
    ALLEGRO_PLAYMODE_ONCE, // play mode
    NULL                   // sample id
);
  • Gain: how loudly to play the sample. 1.0 means the sample will be played at its original power, 0.0 means it'll be silent, and anything above 1.0 will amplify the sample. Use this to destroy your users' eardrums without consent, if that's your thing. Also, this isn't technically the same as 'volume'; we won't go into why, but by all means read up on it.
  • Pan: your in-car stereo probably calls this 'balance' - yet trivially, this setting pans the sample between your left and right speakers. 0.0 is centered, -1.0 is fully to the left, and 1.0 is fully to the right.
  • Speed: yes, you can make the elephant sound like a mouse with a trumpet if you so choose.
  • Play mode: if you like, you can set this to loop the sample - see the docs. With our current setup, this means the sample will never stop playing. Consider yourself warned.
  • Sample ID: you can use these to change the sample's parameters whilst playing it. We won't be doing this here - but if you want to investigate, have a look at al_lock_sample_id.

Before we move on, try messing around with all of these (including the sample ID if you're a big nerd).

Here's a recap:

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_audio.h>
#include <allegro5/allegro_acodec.h>

void must_init(bool test, const char *description)
{
    if(test) return;

    printf("couldn't initialize %s\n", description);
    exit(1);
}

int main()
{
    must_init(al_init(), "allegro");
    must_init(al_install_keyboard(), "keyboard");

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 30.0);
    must_init(timer, "timer");

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
    must_init(queue, "queue");

    al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
    al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
    al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

    ALLEGRO_DISPLAY* disp = al_create_display(640, 480);
    must_init(disp, "display");

    ALLEGRO_FONT* font = al_create_builtin_font();
    must_init(font, "font");

    must_init(al_init_primitives_addon(), "primitives");

    must_init(al_install_audio(), "audio");
    must_init(al_init_acodec_addon(), "audio codecs");
    must_init(al_reserve_samples(16), "reserve samples");

    ALLEGRO_SAMPLE* elephant = al_load_sample("elephant.wav");
    must_init(elephant, "elephant");

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(disp));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    bool done = false;
    bool redraw = true;
    ALLEGRO_EVENT event;

    al_start_timer(timer);
    while(1)
    {
        al_wait_for_event(queue, &event);

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                // game logic goes here.
                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
                if(event.keyboard.keycode == ALLEGRO_KEY_E)
                    al_play_sample(elephant, 1.0, 0.0, 1.0, ALLEGRO_PLAYMODE_ONCE, NULL);
                if(event.keyboard.keycode != ALLEGRO_KEY_ESCAPE)
                    break;

            case ALLEGRO_EVENT_DISPLAY_CLOSE:
                done = true;
                break;
        }

        if(done)
            break;

        if(redraw && al_is_event_queue_empty(queue))
        {
            al_clear_to_color(al_map_rgb(0, 0, 0));
            al_draw_text(font, al_map_rgb(255, 255, 255), 640/2, 480/2, ALLEGRO_ALIGN_CENTER, "Knock knock, it's Nelly");

            al_flip_display();

            redraw = false;
        }
    }

    al_destroy_sample(elephant);
    al_destroy_font(font);
    al_destroy_display(disp);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

Streams

Note: those of you who don't need convincing that streams are useful and cool should just skip ahead to the code. This section is mainly education on why we use them.

Why bother streaming?

When we called al_load_sample, we loaded elephant.wav into memory in its entirety and kept it there. Most game sound effects aren't too long - probably a few seconds at most - so, provided you don't have too many, it's often more than reasonable to load absolutely all of them into memory when your program starts, and leave them there until it quits.

With this in mind: how do we handle longer pieces - specifically music? Your average 3:30 pop track, when encoded as a 320kbps MP3, weighs in at 8.4MB. On today's computers, where even the cheapest laptop can bring 4GB RAM to the table, keeping this one track in memory wouldn't be too bad. Right?

Well: even if your game did only need to play this one piece of music, we haven't considered how much space it'll occupy once it's decoded. So, what does this mean for our program?

  • Most audio formats - MP3 included - are heavily compressed to take up less storage space.
  • When playing back an MP3, it needs to be decoded - temporarily removing the compression.
  • So, for snappy playback, files read into ALLEGRO_SAMPLEs are thus stored decoded and uncompressed in memory. If they weren't, we'd need to decode them every time we wanted to play them, which would be slow.
  • If you've ever converted an MP3 to WAV, you'll probably have noted the crazy increase in size; this is because WAVs are generally uncompressed, unlike MP3s.
  • We can therefore assume that the size of the decoded, uncompressed audio data, as stored in memory, is also going to be big.

So, when loading your 3:30 track into an ALLEGRO_SAMPLE, Allegro will actually be allocating something more like 37MB of RAM. Scale this up to multiple tracks, and you're easily into the hundreds of megabytes; safe to say that things aren't looking so fresh now.

If only there was an easy way to seamlessly load in a bit of the music at a time, decode it, play it, and then move onto the next bit? That way, we'd only have to store a small part of the track in memory at any given time! Great? Great. Streaming audio lets you do this.

Using streams

To have Allegro take care of this whole rigmarole for you, al_load_audio_stream is your starting point. This returns a pointer to an ALLEGRO_AUDIO_STREAM. Note that this is a totally separate thing from an ALLEGRO_SAMPLE, as they store data very differently.

But alas, where would we be without some beautiful music to play? When assembling this tutorial, we considered plenty of amusingly terrible royalty-free MP3s to force upon you - but, sadly, none quite fitted the bill. This is due in no small part to Allegro not actually supporting MP3 out of the box - however, at the time of writing, implementation is ongoing.

*cough*

Instead, we'll use the far superior Opus format. As a demo, its website provides a montage of various music; download it and move it next to your program as music.opus.

At long last, it's time to actually write some code again. Add this to your init code:

ALLEGRO_AUDIO_STREAM* music = al_load_audio_stream("music.opus", 2, 2048);
must_init(music, "music");
al_set_audio_stream_playmode(music, ALLEGRO_PLAYMODE_LOOP);

al_load_audio_stream demands more info from you than al_load_sample. The latter parameters (2, 2048) refer to the stream's buffers: the memory areas where the music will be stored as it is streamed. For more information, see the docs.

You might recall the play modes from earlier. Here, we've used al_set_audio_stream_playmode to specify that the music loops, because silence is for the weak.

As per usual, you'll need to destroy it once you're done with it. Add this at the top of your shutdown code:

al_destroy_audio_stream(music);

Compile and run. You should notice absolutely nothing is playing. Bamboozled again!

Jak's first words

Streams are set to automatically play when they're created, but unlike samples played with al_play_sample, the sound they produce isn't sent anywhere by default.

Luckily and unbeknownst to you, Allegro has been maintaining two other audio-related types behind your back: voices and mixers. Mixers in particular can be used for some very useful audio trickery - and though we won't get into their functionality here, we'd recommend reading up on them before creating a full-blown game.

For now, all you need to know is that you should attach the stream to a mixer. Add this to your init code:

al_attach_audio_stream_to_mixer(music, al_get_default_mixer());

Compile and run. You'll hopefully be greeted with an eclectic variety of music - and yes, can play elephant noises over it.

Try:

Here's your recap:

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_audio.h>
#include <allegro5/allegro_acodec.h>

void must_init(bool test, const char *description)
{
    if(test) return;

    printf("couldn't initialize %s\n", description);
    exit(1);
}

int main()
{
    must_init(al_init(), "allegro");
    must_init(al_install_keyboard(), "keyboard");

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 30.0);
    must_init(timer, "timer");

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
    must_init(queue, "queue");

    al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
    al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
    al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

    ALLEGRO_DISPLAY* disp = al_create_display(640, 480);
    must_init(disp, "display");

    ALLEGRO_FONT* font = al_create_builtin_font();
    must_init(font, "font");

    must_init(al_init_primitives_addon(), "primitives");

    must_init(al_install_audio(), "audio");
    must_init(al_init_acodec_addon(), "audio codecs");
    must_init(al_reserve_samples(16), "reserve samples");

    ALLEGRO_SAMPLE* elephant = al_load_sample("elephant.wav");
    must_init(elephant, "elephant");

    ALLEGRO_AUDIO_STREAM* music = al_load_audio_stream("music.opus", 2, 2048);
    must_init(music, "music");
    al_set_audio_stream_playmode(music, ALLEGRO_PLAYMODE_LOOP);
    al_attach_audio_stream_to_mixer(music, al_get_default_mixer());

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(disp));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    bool done = false;
    bool redraw = true;
    ALLEGRO_EVENT event;

    al_start_timer(timer);
    while(1)
    {
        al_wait_for_event(queue, &event);

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                // game logic goes here.
                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
                if(event.keyboard.keycode == ALLEGRO_KEY_E)
                    al_play_sample(elephant, 1.0, 0.0, 1.0, ALLEGRO_PLAYMODE_ONCE, NULL);
                if(event.keyboard.keycode != ALLEGRO_KEY_ESCAPE)
                    break;

            case ALLEGRO_EVENT_DISPLAY_CLOSE:
                done = true;
                break;
        }

        if(done)
            break;

        if(redraw && al_is_event_queue_empty(queue))
        {
            al_clear_to_color(al_map_rgb(0, 0, 0));
            al_draw_text(font, al_map_rgb(255, 255, 255), 640/2, 480/2, ALLEGRO_ALIGN_CENTER, "Knock knock, it's Nelly");

            al_flip_display();

            redraw = false;
        }
    }

    al_destroy_audio_stream(music);
    al_destroy_sample(elephant);
    al_destroy_font(font);
    al_destroy_display(disp);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

Once again, we've Covered A Lot Of Ground And Yet Barely Scratched The Surface™ - though especially in the case of Allegro's audio, it's worth digging deeper and finding out how things work under the hood, so please go right ahead. That said, the configuration we have here is fine for simpler games.

And on the subject of simple games... shall we make one?

Gameplay ▶

You can’t perform that action at this time.