reds has asked for the wisdom of the Perl Monks concerning the following question:
Greetings fellow monks.
I am writing a small little ditty that grabs an audio stream from one machine (using Audio Hijack Pro, if you're curious), pipes it over the network to a listening daemon which then plays said audio over a stereo. I'm working over an 802.11b network (higher latency than Ethernet).
My question arises from poor results I've been seeing. I can't figure out how to buffer a couple seconds of audio so that the bursty, lagged nature of the 802.11b transmission doesn't lead to buffer underruns in aplay. Right now I'm seeing an underrun every second or so. Nasty.
On the client side (sender), I've got a basic:
my $buf;
while ( read(STDIN, $buf, 128) ) {
print SOCKET, $buf;
}
On the server side (receiver), I've currently got this:
# open up our pipe and turn off the default buffering
open AUDIO_OUT, "|/usr/bin/aplay -f S32_BE -r22050 -c2 --buffer-time=5
+00000 -" or die "aplay pipe broken\n";
select(AUDIO_OUT);
$| = 1;
select(STDOUT);
my $stream;
# these 2 lines are an attempt to fill up aplay's buffer
# it doesn't appear to work at all
read(CLIENT, $stream, 512);
print AUDIO_OUT $stream;
# read the stream and dump it out to aplay
while ( defined( $stream = <CLIENT> ) ) {
print AUDIO_OUT $stream;
}
I've seen some disucssion here about sysread() vs. read(), but I can't seem to figure out how I would dump the audio to AUDIO_OUT without print() (you're not supposed to mix buffered functions with unbuffered ones).
Any help would be greatly appreciated!
Chris H.
Re: Audio Stream Buffer
by Zaxo (Archbishop) on Jul 01, 2004 at 01:36 UTC
|
I think that you will get better results from four-arg select or the higher level IO::Select.
The select loop would allow several input packets to accumulate (with the .= operator) while a stalled write to *AUDIO_OUT is cooking. Once the delay in output is resolved, you'd have all the received data to pass along.
That will be most effective if you have an intermediate child process which can quickly read and buffer what you write to it and handle writes to the audio player in its own good time.
Update: Here's a fairly dumb example of a select loop. It reads from STDIN and writes to a child of open.
#!/usr/bin/perl
open my $ofh, '-|', '/usr/bin/perl', 'lazyread.pl' or die $!;
my ($inv, $ouv, $erv);
$inv = $ouv = $erv = '';
vec($inv, fileno(*STDIN), 1) = 1;
vec($ouv, fileno($fh), 1) = 1;
$erv = $inv | $ouv;
my $string = '';
{
my $num = select
my $in = $inv,
my $out = $ouv,
my $err = $erv,
undef;
sysread *STDIN, $string, -1, 10, length($string)
or last if $in ne $inv;
$string = substr($string, syswrite( $ofh, $string, length($string)
+))
if $string && $out ne $ouv;
redo;
}
__END__
#!/usr/bin/perl # lazyread.pl
open my $fh, '>', 'lazy' or die $!;
while (<>) {
print $fh $_;
sleep 5;
}
Sorry I couldn't write closer to the problem, but my setup doesn't have alsasound installed.
reds, your fork example looks pretty good, but I think I see the problem. The child process will be unable to see changes the parent makes in its copy of $buffer. You'd need to set up IPC between the two, like a pipe or socket, to pass data.
| [reply] [d/l] |
|
| [reply] |
|
I, too, am a little fuzzy on how the select loop system works. Select just chooses the current FH, so how can I use that to buffer the input stream?
Also, once I have my stream buffered, could you or someone please provide a small bit of sample code for how to set up a child process? To date, my strength has been processing text files - not fancy pants networking or multiple threads!
I just tried the code below, but am beginning to understand that because the reading takes more time than it does to play 512 bytes worth of data, I get the interrupted playback. In fact, this code has also somehow corrupted the audio stream so that it plays a tone of some sort, rather like cartoon car horn sound. Learning how to set up a child process to smoothly feed aplay would really help me out!
# build up a buffer
sysread(CLIENT, $read_buffer, 2048);
syswrite(AUDIO_OUT, $read_buffer);
# with buffer established, stream as usual
while ( sysread(CLIENT, $read_buffer, 512) ) {
$buffer .= $read_buffer;
syswrite( AUDIO_OUT, $buffer, 512);
Thanks for your help so far. Cheers!
--------------------------
Some while later:
I've been reading and learned that threads are different from child processes. It would seem as if a thread would serve me best, since I don't want copies of all the filehandles and stuff. Is this correct? The code I have doesn't seem to execute the child code...
my $child_pid;
if ( not defined($child_pid = fork()) ) {
die "cannot fork: $!";
} elsif ($child_pid) {
# I'm the parent
# keep reading the stream
while ( sysread(CLIENT, $read_buffer, 512) ) {
$buffer .= $read_buffer;
print ".";
}
} else {
# I'm the child
# with buffer established, stream as usual
while ( $buffer ) {
syswrite( AUDIO_OUT, $buffer, 512);
print "-";
}
exit;
}
waitpid($child_pid, 0);
| [reply] [d/l] [select] |
Re: Audio Stream Buffer
by tachyon (Chancellor) on Jul 01, 2004 at 02:02 UTC
|
sysread and syswrite (rather than print) let you avoid bufferring by stdio. I suggest you should be using it on both sides as you don't want stdio to be involved buffering. It would seem logical to read/write the same number of bytes rather than sending 128 and waiting on 512. You could do:
while( sysread(CLIENT,$stream,512) ){
syswrite( AUDIO_OUT, $stream )
}
These simple steps may or may not resolve you issues.
Buffering only helps if you 1) get ahead of the game and 2) stay ahead of the game (unless you got far enough ahead to start) of course. Depending on the behaviour of aplay you could possibly just buffer X bytes to start, send that to aplay to fill its buffer, and then loop as above. This would only work if aplay accepts the entire initial stream without blocking so that the following loop is effectively just running in top up mode. Failing that you may need a dedicated local reader that can buffer and supply a consistent stream to your writer as suggested by Zaxo
| [reply] [d/l] |
|
I'll change the read/write bytes to be the same. At one point, they were the same, but I changed it in the hopes that reading more initially would fill up aplay's buffer and provide me with the padding I needed to cover for the bursty 802.11b stream.
I believe I will need the full-blown local reader that Zaxo suggested because aplay's maximum buffer size is very small. I think I need something several seconds long.
As far as the capacity goes, the the audio format is a stereo signed 32 bit, big endian stream at 22050 Hz. I believe that this translates into 1.3Mb of data per second, which should be well within the capacity of an unladen, full strength, 802.11b connection.
| [reply] |
Re: Audio Stream Buffer
by monkey_boy (Priest) on Jul 01, 2004 at 08:27 UTC
|
| [reply] |
Re: Audio Stream Buffer
by aquarium (Curate) on Jul 01, 2004 at 06:07 UTC
|
here's my suggestions:
- make sure that indeed you're in non-buffered read/write over the sockets.
- on the receiving side, write the received data to a local file in non-buffered mode; i.e. NOT pipe the received socket data straight into aplay.
- hope for the best....as current 802.11G implementations still seem rather bursty at the best of times, as you noted. | [reply] |
|
|