Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: Pip-dox
Fetching contributors…

Cannot retrieve contributors at this time

726 lines (517 sloc) 22.128 kb

Sound and Music in SDL are handled by the Audio and SDL_Mixer components. Enabling Audio devices is provided with the Core SDL Library and only supports wav files. SDL_Mixer supports more audio file formats and has additional features that we need for sound in Game Development.

Similarly to video in SDL, there are several way for perl developers to access the Sound components of SDL. For the plain Audio component the SDL::Audio and related modules are available. SDL_Mixer is supported with th SDL::Mixer module. There is currently a SDLx::Sound module in the work, but not completed at the time of writing this manual. For that reason this chapter will use SDL::Audio and SDL::Mixer.

Simple Sound Script

To begin using sound we must enable and open an audiospec:

   use strict;
   use warnings;
   use SDL;
   use Carp;
   use SDL::Audio;
   use SDL::Mixer;
   
   SDL::init(SDL_INIT_AUDIO);
   
   unless( SDL::Mixer::open_audio( 44100, AUDIO_S16SYS, 2, 4096 ) == 0 )
   {
      Carp::croak "Cannot open audio: ".SDL::get_error(); 
   }

open_audio will open an audio device with frequency at 44100 Mhz, audio format AUDIO_S16SYS (Note: This is currently the most portable format, however there are others), 2 channels and a chunk size of 4096. Fiddle with these values if you are comfortable with sound terminology and techniques.

Loading Samples

Next we will load sound samples that generally used for sound effects and the like. Currently SDL_Mixer reserves samples for .WAV, .AIFF, .RIFF .OGG, and .VOC formats.

Samples run on one of the 2 channels that we opened up, while the other channel will be reserved for multiple plays of the sample. To load samples we will be doing the following:

   +use SDL::Mixer::Samples;

   +#Brillant Lazer Sound from U<HTTP://FreeSound.Org/samplesViewSingle.php?id=30935>
   +my $sample = SDL::Mixer::Samples::load_WAV('data/sample.wav');

   +unless($sample)
   +{
   +   Carp::croak "Cannot load file data/sample.wav: ".SDL::get_error();   
   +}

Playing the sample and closing audio

Now we can play that sample on any open channel looping forever:

    use SDL::Mixer::Samples;
   +use SDL::Mixer::Channels;
   
   my $sample =  SDL::Mixer::Samples::load_WAV('data/sample.wav');
   unless( $sample)
   {
      Carp::croak "Cannot load file data/sample.wav: ".SDL::get_error(); 
   }

   +my $playing_channel = SDL::Mixer::Channels::play_channel( -1, $sample, 0 );

play_channel allows us to assign a sample to the channel -1 which indicates any open channel. 0 indicates we want to play the sample only once.

Note that since the sound will be playing in an external process we will need to keep the perl script running. In a game this is no problem but for a single script like this we can just use a simple sleep function. Once we are done we can go ahead and close the audio device.

   +sleep(1);
   +SDL::Mixer::close_audio();

Streaming Music

Next we will use SDL::Mixer::Music to add a background music to our script here.

    use SDL::Mixer::Channels;
    +use SDL::Mixer::Music;

    +#Load our awesome music from U<HTTP://8BitCollective.Com>
    +my $background_music = 
        +         SDL::Mixer::Music::load_MUS('data/music/01-PC-Speaker-Sorrow.ogg');


    +unless( $background_music )
    +{
    +   Carp::croak "Cannot load music file data/music/01-PC-Speaker-Sorrow.ogg: ".SDL::get_error() ;
    +}

Music types in SDL::Mixer run in a separate channel from our samples which allows us to have sound effects (like jump, or lasers etc) to play at the same time.

   +SDL::Mixer::Music::play_music($background_music,0); 

play_music also takes a parameter for how many loops you would like to play the song for, where 0 is 1.

To stop the music we can call halt_music.

    sleep(2);
   +SDL::Mixer::Music::halt_music();
    SDL::Mixer::close_audio();

Code so far

Sound Applications

Now that we know how to prepare and play simple sounds we will apply it to an SDLx::App.

SDLx::App Audio Initialization

SDLx::App will initialize everything normally for us. However for a stream line application it is recommend to initialize only the things we need. In this case that is SDL_INIT_VIDEO and SDL_INIT_AUDIO.

    use strict;
    use warnings;
    use SDL;
    use Carp;
    use SDLx::App;
    use SDL::Audio;
    use SDL::Mixer;
    use SDL::Event;
    use SDL::Events;
    use SDL::Mixer::Music;
    use SDL::Mixer::Samples;
    use SDL::Mixer::Channels;

    my $app = SDLx::App->new(
        init  => SDL_INIT_AUDIO | SDL_INIT_VIDEO,
        width => 250,
        height => 75,
        title => "Sound Event Demo",
        eoq   => 1
  
  );

Loading Resources

It is highly recommended to perform all resource allocations before a SDLx::App::run() method is called.

    # Initialize the Audio
    unless ( SDL::Mixer::open_audio( 44100, AUDIO_S16SYS, 2, 4096 ) == 0 ) {
        Carp::croak "Cannot open audio: " . SDL::get_error();
    }

    #Something to show while we play music and sounds
    my $channel_volume = 100;
    my $music_volume   = 100;
    my $laser_status   = 'none';
    my $music_status   = 'not playing';

    # Load our sound resources
    my $laser = SDL::Mixer::Samples::load_WAV('data/sample.wav');
    unless ($laser) {
        Carp::croak "Cannot load sound: " . SDL::get_error();
    }

    my $background_music =
    SDL::Mixer::Music::load_MUS('data/music/01-PC-Speaker-Sorrow.ogg');
    unless ($background_music) {
        Carp::croak "Cannot load music: " . SDL::get_error();
    }

The Show Handler

For the purposes of describing the current state of the music lets draw text to the screen in a show_handler.

    $app->add_show_handler(
    sub { 

        $app->draw_rect([0,0,$app->w,$app->h], 0 );

        $app->draw_gfx_text( [10,10], [255,0,0,255], "Channel Volume : $channel_volume" );
        $app->draw_gfx_text( [10,25], [255,0,0,255], "Music Volume   : $music_volume" );
        $app->draw_gfx_text( [10,40], [255,0,0,255], "Laser Status   : $laser_status" );
        $app->draw_gfx_text( [10,55], [255,0,0,255], "Music Status   : $music_status" );

        $app->update();

    }
    );

This will draw the channel volume of our samples, and the volume of the music. It will also print the status of our two sounds in the application.

The Event Handler

Finally our event handler will do the actual leg work and trigger the music and sound as we need it.

    $app->add_event_handler(
        sub {
            my $event = shift;

            if ( $event->type == SDL_KEYDOWN ) {
                my $keysym  = $event->key_sym;
                my $keyname = SDL::Events::get_key_name($keysym);

                if ( $keyname eq 'space' ) {

                    $laser_status = 'PEW!';
                    #fire lasers!
                    SDL::Mixer::Channels::play_channel( -1, $laser, 0 );

                }
                elsif ( $keyname eq 'up' ) {
                    $channel_volume += 5 unless $channel_volume == 100;
                }
                elsif ( $keyname eq 'down' ) {
                    $channel_volume -= 5 unless $channel_volume == 0;
                }
                elsif ( $keyname eq 'right' ) {
                    $music_volume += 5 unless $music_volume == 100;
                }
                elsif ( $keyname eq 'left' ) {
                    $music_volume -= 5 unless $music_volume == 0;
                }
                elsif ( $keyname eq 'return' ) {
                    my $playing = SDL::Mixer::Music::playing_music();
                    my $paused  = SDL::Mixer::Music::paused_music();

                    if ( $playing == 0 && $paused == 0 ) {
                        SDL::Mixer::Music::play_music( $background_music, 1 );
                        $music_status = 'playing';
                    }
                    elsif ( $playing && !$paused ) {
                        SDL::Mixer::Music::pause_music();
                        $music_status = 'paused'
                    }
                    elsif ( $playing && $paused ) {
                        SDL::Mixer::Music::resume_music();
                        $music_status = 'resumed playing';
                    }

                }

                SDL::Mixer::Channels::volume( -1, $channel_volume );
                SDL::Mixer::Music::volume_music($music_volume);

            }

          }

    );

The above event handler fires the laser on pressing the 'Space' key. Go ahead and press it multiple times as if you are firing a gun in a game! You will notice that depending on how fast you fire the laser the application will still manage to overlap the sounds as needed. The sample overlapping is accomplished by requiring multiple channels in the open_audio call. If your game has lots of samples that may play at the same time you may need more channels allocated. Additionally you can see that the volume control is easily managed both on the channels and the music with just incrementing or decrementing a value and calling the appropriate function.

Finally it is worth noticing the various state the background music can be in.

Lets run this application and the make sure to clean up the audio on the way out. $app->run(); SDL::Mixer::Music::halt_music(); SDL::Mixer::close_audio;

Completed Code

Music Visualizer

The music visualizer example processes real-time sound data--data as it plays--and displays the wave form on the screen. It will look something like:

The Code and Comments

The program begins with the usual boilerplate of an SDL Perl application:

        use strict;
        use warnings;

        use Cwd;
        use Carp;
        use File::Spec;

        use threads;
        use threads::shared;

        use SDL;
        use SDL::Event;
        use SDL::Events;

        use SDL::Audio;
        use SDL::Mixer;
        use SDL::Mixer::Music;
        use SDL::Mixer::Effects;

        use SDLx::App;

It then creates an application with both audio and video support:

        my $app = SDLx::App->new(
                init   => SDL_INIT_AUDIO | SDL_INIT_VIDEO,
                width  => 800,
                height => 600,
                depth  => 32,
                title  => "Sound Event Demo",
                eoq    => 1,
                dt     => 0.2,
        );

The application must initialize the audio system with a format matching the expected audio input. AUDIO_S16 provides a 16-bit signed integer array for the stream data:

        # Initialize the Audio
        unless ( SDL::Mixer::open_audio( 44100, AUDIO_S16, 2, 1024 ) == 0 ) {
                Carp::croak "Cannot open audio: " . SDL::get_error();
        }

The music player needs the music files from the data/music/ directory:

        # Load our music files
        my $data_dir = '.';
        my @songs = glob 'data/music/*.ogg';

A music effect reads stream data, then serializes it to share between threads:

    my @stream_data : shared;

    #  Music Effect to pull Stream Data
    sub music_data {
        my ( $channel, $samples, $position, @stream ) = @_;

        {
            lock(@stream_data);
            push @stream_data, @stream;
        }

        return @stream;
    }

    sub done_music_data { }

... and that effect gets registered as a callback with SDL::Mixer::Effects:

        my $music_data_effect_id = 
                  SDL::Mixer::Effects::register( MIX_CHANNEL_POST, "main::music_data",
                        "main::done_music_data", 0 );

The program's single command-line option governs the number of lines to display in the visualizer. The default is 50.

    my $lines = $ARGV[0] || 50;

The drawing callback for the SDLx::App runs while a song plays. It reads the stream data and displays it on the screen as a wave form. The math behind calculating the graphics to display is more detail than this article intends, but the graphic code is straightforward:

    #  Music Playing Callbacks
    my $current_song = 0;

    my $current_music_callback = sub {
        my ( $delta, $app ) = @_;

        $app->draw_rect( [ 0, 0, $app->w(), $app->h() ], 0x000000FF );
        $app->draw_gfx_text(
            [ 5, $app->h() - 10 ],
            [ 255, 0, 0, 255 ],
            "Playing Song: " . $songs[ $current_song - 1 ]
        );

        my @stream;
        {
            lock @stream_data;
            @stream      = @stream_data;
            @stream_data = ();
        }

        # To show the right amount of lines we choose a cut of the stream
        # this is purely for asthetic reasons.

        my $cut = @stream / $lines;

        # The width of each line is calculated to use.
        my $l_wdt = ( $app->w() / $lines ) / 2;

        for ( my $i = 0 ; $i < $#stream ; $i += $cut ) {

            #  In stereo mode the stream is split between two alternating streams
            my $left  = $stream[$i];
            my $right = $stream[ $i + 1 ];

            #  For each bar we calculate a Y point and a X point
            my $point_y = ( ( ($left) ) * $app->h() / 4 / 32000 ) + ( $app->h / 2 );
            my $point_y_r =
              ( ( ($right) ) * $app->h() / 4 / 32000 ) + ( $app->h / 2 );
            my $point_x = ( $i / @stream ) * $app->w;

            # Using the parameters
            #   Surface, box coordinates and color as RGBA
            SDL::GFX::Primitives::box_RGBA(
                $app,
                $point_x - $l_wdt,
                $app->h() / 2,
                $point_x + $l_wdt,
                $point_y, 40, 0, 255, 128
            );
            SDL::GFX::Primitives::box_RGBA(
                $app,
                $point_x - $l_wdt,
                $app->h() / 2,
                $point_x + $l_wdt,
                $point_y_r, 255, 0, 40, 128
            );

        }

        $app->flip();

    };

Whenever a song finishes SDL::Mixer::Music::playing_music returns 0. We detect this change in state and call music_finished_playing() where the program attaches our $play_next_song_callback callback to switch to the next song gracefully:

    my $cms_move_callback_id;
    my $pns_move_callback_id;
    my $play_next_song_callback;

    sub music_finished_playing {
        SDL::Mixer::Music::halt_music();
        $pns_move_callback_id = $app->add_move_handler($play_next_song_callback)
          if ( defined $play_next_song_callback );

    }

    $play_next_song_callback = sub {
        return $app->stop() if $current_song >= @songs;
        my $song = SDL::Mixer::Music::load_MUS( $songs[ $current_song++ ] );
        SDL::Mixer::Music::play_music( $song, 0 );

        $app->remove_move_handler($pns_move_callback_id)
          if defined $pns_move_callback_id;
    };

A move handler is attached to detect if music is playing or not:

    $app->add_move_handler(
        sub {
            my $music_playing = SDL::Mixer::Music::playing_music();
            music_finished_playing() unless $music_playing;
        }
    )       

The first callback to trigger the $play_next_song_callback gets the first song:

        $app->add_show_handler($current_music_callback);
    $pns_move_callback_id = $app->add_move_handler($play_next_song_callback);

... and a keyboard event handler for a keypress allows the user to move through songs:

        $app->add_event_handler(
        sub {
                my ($event, $app) = @_;

                if( $event->type == SDL_KEYDOWN && $event->key_sym == SDLK_DOWN)
                {       
                        #Indicate that we are done playing the music_finished_playing
                        music_finished_playing();
                }

        }
        );

From there, the application is ready to run:

        $app->run();

... and the final code gracefully stops SDL::Mixer:

        SDL::Mixer::Effects::unregister( MIX_CHANNEL_POST, $music_data_effect_id );
        SDL::Mixer::Music::hook_music_finished();
        SDL::Mixer::Music::halt_music();
        SDL::Mixer::close_audio();

The result? Several dozen lines of code to glue together the SDL mixer and display a real-time visualization of the music.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 1:

Unknown directive: =head0

Jump to Line
Something went wrong with that request. Please try again.