Audio-StreamGenerator

 view release on metacpan or  search on metacpan

lib/Audio/StreamGenerator.pm  view on Meta::CPAN


sub _pack_sample {
    my ($self, $sample) = @_;

    return map { pack 's', $_ } @$sample
        if ref $sample eq 'ARRAY';
    return $sample;
}

sub _unpack_sample {
    my ($self, $sample) = @_;

    return $sample unless defined $sample;
    return $sample if ref $sample eq 'ARRAY';
    return [unpack 's*', $sample];
}

sub _get_samples {
    my ($self, $count, $dest) = @_;
    return 1 unless $count;

    my $data;
    my $bytes_div = $self->{channels_amount} * 2;
    my $bytes = $bytes_div * $count;
    my $len   = read( $self->{source}, $data, $bytes );

    if ( my $rest = $len % $bytes_div ) {
        my $add_bytes = $bytes_div - $rest;
        $data .= "\x00" x $add_bytes;
        $len += $add_bytes;
    }

    $self->{elapsed} += ( $len / $bytes_div );

    while ($data) {
        push @$dest, substr $data, 0, $bytes_div, '';
    }

    return $len;
}

sub _do_get_new_source {
    my $self = shift;
    $self->{elapsed} = 0;
    return $self->{get_new_source}();
}

sub skip {
    my $self = shift;
    $self->{skip} = 1;
}

1;

__END__

=pod

=head1 NAME

Audio::StreamGenerator - create a 'radio' stream by mixing ('cross fading') multiple audio sources (files or anything that can be converted to PCM audio) and sending it to a streaming server (like Icecast)

=head1 SYNOPSIS

    use strict;
    use warnings;
    use Audio::StreamGenerator;

    my $out_command = q~
            ffmpeg -re -f s16le -acodec pcm_s16le -ac 2 -ar 44100 -i -  \
            -acodec libopus -ac 2 -b:a 160k -content_type application/ogg -format ogg icecast://source:hackme@localhost:8000/our_radio.opus \
            -acodec libmp3lame -ac 2 -b:a 192k -content_type audio/mpeg icecast://source:hackme@localhost:8000/our_radio.mp3 \
            -acodec aac -b:a 192k -ac 2 -content_type audio/aac icecast://source:hackme@localhost:8000/our_radio.aac
    ~;

    my $out_fh;
    open ($out_fh, '|-', $out_command);

    sub get_new_source {
        my $fullpath = '/path/to/some/audiofile.flac';
        my @ffmpeg_cmd = (
                '/usr/bin/ffmpeg',
                '-i',
                $fullpath,
                '-loglevel', 'quiet',
                '-f', 's16le',
                '-acodec', 'pcm_s16le',
                '-ac', '2',
                '-ar', '44100',
                '-'
        );
        open(my $source, '-|', @ffmpeg_cmd);
        return $source;
    }

    sub run_every_second {
        my $streamert = shift;
        my $position = $streamert->get_elapsed_seconds();
        print STDERR "now at position $position\r";
        if ([-some external event happened-]) {  # skip to the next song requested
            $streamert->skip()
        }
    }

    my $streamer = Audio::StreamGenerator->new(
        out_fh => $out_fh,
        get_new_source => \&get_new_source,
        run_every_second => \&run_every_second,
    );

    $streamer->stream();

=head1 DESCRIPTION

This module creates a 'live' audio stream that can be broadcast using streaming technologies like Icecast or HTTP Live Streaming.

It creates one ongoing audio stream by mixing or 'crossfading' multiple sources (normally audio files).

Although there is nothing stopping you from using this to generate a file that can be played back later, its intended use is to create a 'radio' stream that can be streamed or 'broadcast' live on the internet.

The module takes raw PCM audio from a file handle as input, and outputs raw PCM audio to another file handle. This means that an external program is necessary to decode (mp3/flac/etc) source files, and to encode & stream the actual output. For both p...

=head1 CONSTRUCTOR METHOD

    my $streamer = Audio::StreamGenerator->new( %options );

Creates a new StreamGenerator object and returns it.

=head1 OPTIONS

The following options can be specified:

    KEY                             DEFAULT     MANDATORY
    -----------                     -------     ---------
    out_fh                          -           yes
    get_new_source                  -           yes
    run_every_second                -           no
    normal_fade_seconds             5           no
    buffer_length_seconds           10          no
    skip_fade_seconds               3           no
    sample_rate                     44100       no
    channels_amount                 2           no
    max_vol_before_mix_fraction     0.75        no
    skip_silence                    1           no
    min_audible_vol_fraction        0.005       no
    debug                           0           no

=head2 out_fh

The outgoing file handle - this is where the generated signed 16-bit little-endian PCM audio stream is sent to.

Note that StreamGenerator has no notion of time - if you don't slow it down, it will process data as fast as it can - which is faster than your listeners are able to play the stream.
On Icecast, this will cause listeners to be disconnected because they are "too far behind".

This can be addressed by making sure that the L</"out_fh"> process consumes the audio no faster than realtime.

If you are using ffmpeg, you can achieve this with its C<-re> option.

Another possibility is to first pipe the data to a command like C<pv> to rate limit the data. An additional advantage of C<pv> is that it can also add a buffer between the StreamGenerator and the encoder, which can absorb any short delays that may oc...

Example:

    pv -q -L 176400 -B 3528000 | ffmpeg ...

This will tell C<pv> to be quiet (no output to STDERR), to allow a maximum throughput of 44100 samples per second * 2 bytes per sample * 2 channels = 176400 bytes per second, and keep a buffer of 176400 Bps * 20 seconds = 3528000 bytes

The L</"out_fh"> command is also the place where you could insert a sound processing solution like the command line version of L<Stereo tool|https://www.stereotool.com/> - just pipe the audio first to that tool, and from there to your encoder.

=head2 get_new_source

Reference to a sub that will be called every time that a new source (audio file) is needed. Needs to return a readable filehandle that will output signed 16-bit little-endian PCM audio. Optionally, an amount of seconds can be returned as a second ret...

=head2 run_every_second

This sub will be run after each second of playback, with the StreamGenerator object as an argument. This can be used to do things like updating a UI with the current playing position - or to call the L</"skip"> method if we need to skip to the next s...

=head2 normal_fade_seconds

Amount of seconds that we want tracks to overlap. This is only the initial/max value - the mixing algorithm may decide to mix less seconds if the old track ends with loud samples. It can be overriden for individual sources by returning an amount of s...

=head2 buffer_length_seconds

Amount of seconds of the current track to keep in the buffer. Having this set to a higher value than L</"normal_fade_seconds"> will ensure that there will be enough audio left to mix after removing silence at the end of the old track.

=head2 skip_fade_seconds

When 'skipping' to the next song using the L</"skip"> method (for example, after a user clicked a "next song" button on some web interface), we mix less seconds than normally, simply because mixing 5+ seconds in the middle of the old track sounds pre...

=head2 sample_rate

The amount of samples per second (both incoming & outgoing), normally this is 44100 for standard CD-quality audio.

=head2 channels_amount

Amount of audio channels, this is normally 2 (stereo).

=head2 max_vol_before_mix_fraction

This tells StreamGenerator what the minimum volume of a 'loud' sample is. It is expressed as a fraction of the maximum volume.
When mixing 2 tracks, StreamGenerator needs to find out what the last loud sample of the old track is so that it can start the next song immediately after that.

=head2 skip_silence

If enabled, audio softer than L</"min_audible_vol_fraction"> at the beginning of a track, and at the end of tracks (but within the last L</"buffer_length_seconds"> seconds) will be skipped.

=head2 min_audible_vol_fraction

Audio softer than this volume fraction at the end of a track (and within the buffer) or at the beginning of a track will be skipped, if skip_silence is enabled.

=head2 debug

Log debugging information. If the value is a code reference, the logs will be passed to that sub. Otherwise the value will be treated as a boolean. If true, logs will be printed to C<STDERR> .

=head1 METHODS

=head2 stream

    $streamer->stream();

Start the actual audio stream.

=head2 get_streamer

    my $streamer_sub = $streamer->get_streamer($sec_per_call);

    while (1) {
        $streamer_sub->();
    }

Get an anonymous subroutine that will produce C<$sec_per_call> seconds of a stream when called.

C<$sec_per_call> is optional, and is by default C<1>.

Use this method instead of L</"stream"> if you want to have more control over the streaming process, for example, running the streamer inside an event loop:

    use Mojo::IOLoop;

    my $loop = Mojo::IOLoop->singleton;
    my $streamer_sub = $streamer->get_streamer(0.5);

    $loop->recurring(0.05 => $streamer_sub);
    $loop->start;

Note: event loop will be blocked for up to 0.5 seconds every time the timer is done. The timer/streamer ratio should be sizeable enough for the loop to run smoothly, including the mixing process. This may vary depending on the speed of the machine wh...

=head2 skip

    $streamer->skip();

Skip to the next track without finishing the current one. This can be called from the L</"run_every_second"> sub, for example after checking whether a 'skip' flag was set in a database, or whether a file exists.

=head2 get_elapsed_samples

    my $elapsed_samples = $streamer->get_elapsed_samples();
    print "$elapsed_samples played so far\r";

Get the amount of played samples in the current track - this can be called from the L</"run_every_second"> sub.

=head2 get_elapsed_seconds

    my $elapsed_seconds = $streamer->get_elapsed_seconds();
    print "now at position $elapsed_seconds of the current track\r";

Get the amount of elapsed seconds in the current track - in other words the current position in the track. This equals to C<get_elapsed_samples / sample_rate > .



( run in 2.540 seconds using v1.01-cache-2.11-cpan-140bd7fdf52 )