#!/usr/bin/env perl # # $Id: mt-daapd-ssc.pl 954 2006-04-15 04:45:25Z rpedde $ # # -*- perl -*- # # ---------------------------------------------------------------------- # Server side media format conversion script for mt-daapd. # ---------------------------------------------------------------------- # Copyright & 2005 # Timo J. Rinne # ---------------------------------------------------------------------- # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # # This is quite rudimentary but handles most cases quite well. # # TODO-list: # - Make the guessing of the wav length reliable. # - Possibly implement some kind of caching. # # I'll probably never have time for above things, but now it should # be easy for someone to take this over, since basically all you have # to do is to make the filter program and tweak the configuration. # # //tri # require 'sys/syscall.ph'; use POSIX qw(:sys_wait_h fork); use IO::Handle; use IO::File; use IPC::Open2; use IPC::Open3; $SIG{PIPE} = 'IGNORE'; # To debug or not debug, that is the question... my $DEBUG = 1; # Temporary file handler and PID of encoder needs to be # global to be availible both in function and END {}... my ($tf, $pid); { umask(0077); if ($#ARGV < 1) { usage(); } my ($fn) = $ARGV[0]; my ($off) = 0 + $ARGV[1]; my ($forgelen) = undef; if ($#ARGV > 1) { $forgelen = 0.0 + $ARGV[2]; if ($forgelen < 0.01) { $forgelen = 0.0; } } else { $forgelen = 0.0; } if ($off < 0) { usage(); } $0 = 'mt-daapd-ssc.pl'; binmode(STDOUT); print STDERR "DEBUG: $0: Starting\n" if($DEBUG); if ($fn =~ m/^..*\.wav$/i) { passthru_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.wave$/i) { passthru_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.flac$/i) { flac_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.fla$/i) { flac_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.mp4$/i) { mpeg4_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.m4a$/i) { mpeg4_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.m4p$/i) { mpeg4_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.aac$/i) { mpeg4_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.shn$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.mp3$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.mpg$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.ogg$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.au$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.m4v$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.avi$/i) { mplayer_proc($fn, $off, $forgelen, 'avi'); } elsif ($DEBUG && ($fn =~ m/^..*\.mov$/i)) { mplayer_proc($fn, $off, $forgelen, 'mov'); } else { mplayer_proc($fn, $off, $forgelen, 0); } # Unknown extension exit; } sub usage { print STDERR "usage: ssc-script file offset\n"; exit -1; } sub tmp_file { return(sprintf "/tmp/t-%d", $$); } sub wav_loop { my ($f) = shift; my ($off) = shift; my ($forgelen) = shift; my ($exten) = shift; my ($buf); my ($buflen); my ($buftry) = 4096; my ($hdr); binmode($f); $hdr = read_wav_hdr($f); if (! defined $hdr) { print STDERR "ERROR: wav_loop(): no wav hdr.\n" if($DEBUG); return undef; } my ($chunk_data_length, $format_data_length, $compression_code, $channel_count, $sample_rate, $sample_bit_length, $data_length) = parse_wav_hdr($hdr); if (! defined $chunk_data_length) { print STDERR "ERROR: Can't parse WAV header.\n" if($DEBUG); return undef; } if (defined $forgelen) { if ($forgelen > 0.00001) { my ($sec) = int($forgelen); my ($msec) = ($forgelen - (0.0 + int($forgelen))) * 1000; my ($bps) = ($sample_rate * $channel_count * int(($sample_bit_length + 7) / 8)); my ($len) = int(($sec * $bps) + (($msec * $bps) / 1000)); my ($blocklen); my ($residual); if (($sample_rate == 44100) && ($channel_count == 2) && ($sample_bit_length == 16)) { # It's probably a CD track so let's round it to next cd block limit (2352 bytes). $blocklen = 2352; } else { # Pad it at least to next valid sample border. $blocklen = ($channel_count * int(($sample_bit_length + 7) / 8)) } $residual = $len % $blocklen; if ($residual != 0) { my ($pad) = $blocklen - $residual; $len += $pad; } $hdr = forge_wav_hdr($hdr, $len); } else { $hdr = forge_continuous_wav_hdr($hdr); } } if (($off > 0) && ($off < length($hdr))) { $hdr = substr($hdr, $off, (length($hdr) - $off)); } elsif ($off == length($hdr)) { $hdr = undef; } elsif ($off > length($hdr)) { $hdr = undef; $off -= length($hdr); if (! sysseek($f, $off, 1)) { # It's not seekable. Then just read until desired offset. while ($off > 0) { my ($rl) = $off > $buftry ? $buftry : $off; $buflen = sysread($f, $buf, $rl); if ($buflen < 1) { return undef; } $off -= $buflen; } } } if (defined $hdr) { if (syswrite (STDOUT, $hdr) != length($hdr)) { print STDERR "ERROR: Header write failed ($!).\n" if($DEBUG); return undef; } } stream_loop($f); } sub stream_loop { my ($f) = shift; my ($buf); my ($buftry) = 4096; my ($buflen); my ($err) = 0; my($read) = undef; if ($DEBUG) { open(DUP, "> /tmp/filename-manual-script.mov"); select(DUP); $| = 1; } print STDERR "DEBUG: " if($DEBUG); my ($i, $j) = (1, 0); while (1) { $buflen = sysread($f, $buf, $buftry); print STDERR "$i($buflen) " if($DEBUG); $i++; if ($buflen < 1) { sleep(1); # Try ten times - it's possible we just need to catch up with the transcoder... if($j > 9) { print STDERR "DEBUG: Read less than 1 byte - exit (done?).\n" if($DEBUG); close($f); return 0; } $i--; # So we don't count the same block twice... } elsif(!$buflen) { print STDERR "ERROR: Error reading from file (buflen=$buflen).\n" if($DEBUG); return -1; } if ($buflen) { print DUP $buf if($DEBUG); if (($read = syswrite (STDOUT, $buf)) != $buflen) { print STDERR "ERROR: Buffer write failed ('$read' != '$buflen'): $!\n" if($DEBUG); return -1; } $j = 0; # Restart counter... } else { $j++; } } } sub read_wav_hdr { my ($f) = shift; my ($hdr) = undef; my ($format_data_length); if (sysread($f, $hdr, 44) != 44) { print STDERR "ERROR: Can't read WAV header.\n" if($DEBUG); return undef; } return undef if ((substr($hdr, 0, 4) ne "RIFF")); return undef if ((substr($hdr, 8, 4) ne "WAVE")); return undef if ((substr($hdr, 12, 4) ne "fmt ")); $format_data_length = (((((((ord(substr($hdr, 19, 1))) * 256) + (ord(substr($hdr, 18, 1)))) * 256) + (ord(substr($hdr, 17, 1)))) * 256) + (ord(substr($hdr, 16, 1)))); return undef if (($format_data_length < 16) || ($format_data_length > 256)); if ($format_data_length > 16) { my ($hdr_cont); my ($cont_len) = $format_data_length - 16; if (sysread($f, $hdr_cont, $cont_len) != $cont_len) { print STDERR "ERROR: Can't read WAV header.\n" if($DEBUG); return undef; } $hdr = $hdr . $hdr_cont; } # FFMpeg creates sometimes WAVs with fmt chunk longer than 16 bytes. # This is weird, since at least iTunes can't play them. Let's chop # extra bytes away from the fmt chunk and make the WAV header look # like everything else in the world seems to like it. if ($format_data_length > 16) { my ($chunk_data_length, $format_data_length, $compression_code, $channel_count, $sample_rate, $sample_bit_length, $data_length) = parse_wav_hdr($hdr); $chunk_data_length = $chunk_data_length - ($format_data_length - 16); $hdr = (substr($hdr, 0, 4) . chr($chunk_data_len % 256) . chr((int($chunk_data_len / 256)) % 256) . chr((int($chunk_data_len / (256 * 256))) % 256) . chr((int($chunk_data_len / (256 * 256 * 256))) % 256) . substr($hdr, 8, 8) . chr(16) . chr(0) . chr(0) . chr(0) . substr($hdr, 20, 16) . substr($hdr, 20 + $format_data_length, length($hdr) - (20 + $format_data_length))); } return $hdr; } sub parse_wav_hdr { my ($hdr) = shift; if (length($hdr) < 44) { return undef; } my ($chunk_data_length); my ($format_data_length); my ($compression_code); my ($channel_count); my ($sample_rate); my ($sample_bit_length); my ($data_length); return undef if ((substr($hdr, 0, 4) ne "RIFF")); $chunk_data_length = (((((((ord(substr($hdr, 7, 1))) * 256) + (ord(substr($hdr, 6, 1)))) * 256) + (ord(substr($hdr, 5, 1)))) * 256) + (ord(substr($hdr, 4, 1)))); return undef if ((substr($hdr, 8, 4) ne "WAVE")); return undef if ((substr($hdr, 12, 4) ne "fmt ")); $format_data_length = (((((((ord(substr($hdr, 19, 1))) * 256) + (ord(substr($hdr, 18, 1)))) * 256) + (ord(substr($hdr, 17, 1)))) * 256) + (ord(substr($hdr, 16, 1)))); return undef if ($format_data_length < 16); $compression_code = (((ord(substr($hdr, 21, 1))) * 256) + (ord(substr($hdr, 20, 1)))); return undef if ($compression_code != 1); $channel_count = (((ord(substr($hdr, 23, 1))) * 256) + (ord(substr($hdr, 22, 1)))); return undef if ($channel_count < 1); $sample_rate = (((((((ord(substr($hdr, 27, 1))) * 256) + (ord(substr($hdr, 26, 1)))) * 256) + (ord(substr($hdr, 25, 1)))) * 256) + (ord(substr($hdr, 24, 1)))); $sample_bit_length = (((ord(substr($hdr, 35, 1))) * 256) + (ord(substr($hdr, 34, 1)))); return undef if ((substr($hdr, $format_data_length + 20, 4) ne "data")); $data_length = (((((((ord(substr($hdr, $format_data_length + 24 + 3, 1))) * 256) + (ord(substr($hdr, $format_data_length + 24 + 2, 1)))) * 256) + (ord(substr($hdr, $format_data_length + 24 + 1, 1)))) * 256) + (ord(substr($hdr, $format_data_length + 24 + 0, 1)))); return ($chunk_data_length, $format_data_length, $compression_code, $channel_count, $sample_rate, $sample_bit_length, $data_length); } sub forge_wav_hdr { my ($hdr) = shift; my ($data_len) = shift; my ($chunk_data_len) = $data_len + 36; return (substr($hdr, 0, 4) . chr($chunk_data_len % 256) . chr((int($chunk_data_len / 256)) % 256) . chr((int($chunk_data_len / (256 * 256))) % 256) . chr((int($chunk_data_len / (256 * 256 * 256))) % 256) . substr($hdr, 8, length($hdr) - 8 - 4) . chr($data_len % 256) . chr((int($data_len / 256)) % 256) . chr((int($data_len / (256 * 256))) % 256) . chr((int($data_len / (256 * 256 * 256))) % 256)); } sub forge_continuous_wav_hdr { my ($hdr) = shift; return (substr($hdr, 0, 4) . chr(0xff) . chr(0xff) . chr(0xff) . chr(0xff) . substr($hdr, 8, length($hdr) - 8 - 4) . chr(0xff - length($hdr) - 8) . chr(0xff) . chr(0xff) . chr(0xff)); } sub passthru_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($f) = undef; if (! open($f, "< $fn")) { return undef; } wav_loop($f, $off, undef); # Ignore $forgelen close($f); } sub flac_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($r) = undef; my ($w) = undef; $pid = open2($r, $w, 'flac', '--silent', '--decode', '--stdout', "$fn"); wav_loop($r, $off, undef); # Ignore $forgelen close($r); close($w); } sub ffmpeg_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($r) = undef; my ($w) = undef; $pid = open2($r, $w, 'ffmpeg', '-vn', '-i', "$fn", '-f', 'wav', '-'); wav_loop($r, $off, $forgelen); close($r); close($w); } sub mplayer_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($exten) = shift; # extension my ($tf) = tmp_file(); my ($w) = undef; my ($f) = undef; my ($i); my ($started) = undef; my ($waited) = undef; my ($prebuf) = 1 * 1024 * 1024; if($exten) { # Plays directly (without transcoding): # * MOV/.mov (w/o transcoding) # * MOV/.m4v (w/o transcoding) # * MP4/.mp4 (w/ DB hack works w/o transcoding) # => update songs set has_video=1,type='m4v',codectype='mp4v' where path like '%.mp4') # + MPG/.mp4 (only sound - Even w/ DB hack!) # - WAV/.m4v (loads, no error but don't play) # - M4V/.m4v (loads, no error but don't play) # --------------------------------------------------------------------- # Apperently iTunes supports '.m4a', '.mp3', '.mov', '.mp4', '.m4v'. # # Force to 'wav': Works - kind'a (sound, no video!) # Force to 'asf': Streams 119x4096 bytes (= Broken Pipe) # '-acodec', 'libmp3lame', '-vcodec', 'msmpeg4' # Force to 'mov': Streams 119x4096 bytes (= Broken Pipe) # Force to 'mov': Streams 69x4096 bytes (= Broken Pipe) # '-acodec', 'libmp3lame', '-vcodec', 'mpeg4' # Force to 'mov': Streams 69x4096 bytes (= Broken Pipe) # '-acodec', 'libmp3lame' # Force to 'mpeg': Streams 48x4096 bytes (= Broken Pipe) # Force to 'h264': Plays (w/o video, crappy sound) # '-vcodec', 'mpeg4', '-acodec', 'libmp3lame' # # --------------------------------------------------------------------- # These three seems more or less the same... # Force to 'mp4': Streams 6x4096 bytes (= Broken Pipe) # Force to 'm4v': (format mp4) # Force to 'm4a': (format mp4) # # Creating files with ffmpeg: # .mp4 is not recognized as a iTunes playable file # => Plays in QuickTime though. # .m4v seems to be an audio format only (at least ffmpeg only outputs # the audio stream, unless using -vcodec option). # => Does not play in either iTunes nor QuickTime. # .m4a seems to be the same as .m4v (but with video - ?). # => iTunes will play this... # # --------------------------------------------------------------------- # Works - if firefly is configured to ssc transcode .mov files! my ($STREAM_SYMLINK, $symlink) = (1, "/share/Movies/tmp/Filename_x264aac-mov_fixed-mp4creator.mov"); # # --------------------------------------------------------------------- my ($format) = 'mp4'; my (@common_options) = ('-bufsize', '4096'); my (@additional_options) = ('-vcodec', 'libxvid', '-maxrate', '1000', '-b', '400', '-qmin', '3', '-qmax', '5', '-bufsize', '4096', '-g', '300', '-acodec', 'libfaac', '-ab', '128', '-s', '320x240', '-async', '1'); # my (@additional_options) = ('-b', '300', '-maxrate', '300', '-r', '25', '-s', '384x256', # '-vcodec', 'copy', '-vtag', 'mp4v', '-acodec', 'libfaac', '-ab', '128'); # --------------------------------------------------------------------- # Something happened. Short burst of some kind... # my ($format) = 'mp4'; # my (@additional_options) = ('-vcodec', 'libx264', '-acodec', 'libfaac'); # my (@additional_options) = ('-vcodec', 'libx264', '-acodec', 'libfaac', '-ab', '128k', '-s', '640x480'); # my (@additional_options) = ('-acodec', 'libfaac', '-ab', '128k', '-s', '640x480', '-vcodec', 'mpeg4', # '-b', '2500', '-flags', '+aic+cbp+mv0+mv4+trell', '-mbd', '2', '-cmp', '2', # '-subcmp', '2', '-g', '250', '-maxrate', '2.5M', '-bufsize', '2M'); # my (@additional_options) = ('-vcodec', 'libx264', '-me', 'full', '-refs', '3', '-subq', '5', # '-b', '700k', '-s', '320x240', '-r', '23.976023976', '-ac', '1', # '-bf', '0', '-level', '13'); # my (@additional_options) = ('-vcodec', 'libx264', '-level', '13', '-s', '320x240', '-b', '768k', # '-bt', '768k', '-bufsize', '2000k', '-maxrate', '768k', '-g', '250', # '-coder', '0', '-acodec', 'libfaac', '-ac', '2', '-ab', '128k'); # # This plays! Somewhat - no video, broken sound. # my ($format) = 'h264'; # my (@additional_options) = ('-vcodec', 'mpeg4', '-acodec', 'libmp3lame'); # my (@additional_options) = ('-vcodec', 'libx264', '-acodec', 'libmp3lame'); # # --------------------------------------------------------------------- if($STREAM_SYMLINK) { print STDERR "DEBUG: Streaming symlink '$symlink'\n" if($DEBUG); symlink($symlink, $tf); sleep(3); } else { print STDERR "DEBUG: Starting transcoding of file '$fn' as '$format'.\n" if($DEBUG); print STDERR "DEBUG: ffmpeg -i '$fn' "; for(my $i=0; defined($additional_options[$i]); $i++) { print STDERR $additional_options[$i]." "; } print STDERR "-f $format $tf\n"; open(NULL, "/dev/null"); my (@command) = ('/usr/bin/ffmpeg', '-i', $fn, @common_options, @additional_options, '-f', $format, $tf); $pid = open3('<&NULL', '>&NULL', '>&NULL', @command); } } else { $pid = open3('>&STDERR', $w, '>&STDERR', 'mplayer', '-quiet', '-really-quiet', '-nomouseinput', '-nojoystick', '-nolirc', '-noconsolecontrols', '-nortc', '-vo', 'null', '-ao', "pcm:file=$tf", "$fn"); } print STDERR "DEBUG: Transcoding started (pid=$pid)...\n" if($DEBUG); # ------------- print STDERR "DEBUG: Waiting for stream...\n" if($DEBUG); # Wait just to see, if the decoding starts. # If the resulting wav is less than 512 bytes, # this will fail, but who cares. # # Wait until we have $prebuf (1Mb) in the buffer # untill we start streaming... my ($sleep) = 0.25; while (! $waited) { my ($s); my ($wp); $s = (stat("$tf"))[7]; if (defined $s && ($s > 512) && ($s > ($off + $prebuf))) { $started = 1; } if ($wp > 0) { $waited = 1; } if ($started || $waited) { last; } else { vec($stdout, fileno(STDOUT), 1) = 1; select($stdout, $stdout, $stdout, $sleep); } } if (! $started) { print STDERR "ERROR: Transcoding not started.\n" if($DEBUG); close($w); return; } # Stream. if (sysopen($f, $tf, O_RDONLY|O_NONBLOCK|O_RSYNC)) { my ($buftry) = 4096; my ($buf); my ($buflen); print STDERR "DEBUG: Starting to stream from '$tf'.\n" if($DEBUG); if($exten) { sysseek($f, 0, 0); binmode($f); stream_loop($f); close($f); close($w); } else { wav_loop($f, $off, $forgelen, $exten); close($f); } } else { print STDERR "ERROR: Can't open temp file\n" if($DEBUG); return undef; } return; } sub read_wav_hdr { my ($f) = shift; my ($hdr) = undef; my ($format_data_length); if (sysread($f, $hdr, 44) != 44) { print STDERR "ERROR: Can't read WAV header.\n" if($DEBUG); return undef; } return undef if ((substr($hdr, 0, 4) ne "RIFF")); return undef if ((substr($hdr, 8, 4) ne "WAVE")); return undef if ((substr($hdr, 12, 4) ne "fmt ")); $format_data_length = (((((((ord(substr($hdr, 19, 1))) * 256) + (ord(substr($hdr, 18, 1)))) * 256) + (ord(substr($hdr, 17, 1)))) * 256) + (ord(substr($hdr, 16, 1)))); return undef if (($format_data_length < 16) || ($format_data_length > 256)); if ($format_data_length > 16) { my ($hdr_cont); my ($cont_len) = $format_data_length - 16; if (sysread($f, $hdr_cont, $cont_len) != $cont_len) { print STDERR "ERROR: Can't read WAV header.\n" if($DEBUG); return undef; } $hdr = $hdr . $hdr_cont; } # FFMpeg creates sometimes WAVs with fmt chunk longer than 16 bytes. # This is weird, since at least iTunes can't play them. Let's chop # extra bytes away from the fmt chunk and make the WAV header look # like everything else in the world seems to like it. if ($format_data_length > 16) { my ($chunk_data_length, $format_data_length, $compression_code, $channel_count, $sample_rate, $sample_bit_length, $data_length) = parse_wav_hdr($hdr); $chunk_data_length = $chunk_data_length - ($format_data_length - 16); $hdr = (substr($hdr, 0, 4) . chr($chunk_data_len % 256) . chr((int($chunk_data_len / 256)) % 256) . chr((int($chunk_data_len / (256 * 256))) % 256) . chr((int($chunk_data_len / (256 * 256 * 256))) % 256) . substr($hdr, 8, 8) . chr(16) . chr(0) . chr(0) . chr(0) . substr($hdr, 20, 16) . substr($hdr, 20 + $format_data_length, length($hdr) - (20 + $format_data_length))); } return $hdr; } sub parse_wav_hdr { my ($hdr) = shift; if (length($hdr) < 44) { return undef; } my ($chunk_data_length); my ($format_data_length); my ($compression_code); my ($channel_count); my ($sample_rate); my ($sample_bit_length); my ($data_length); return undef if ((substr($hdr, 0, 4) ne "RIFF")); $chunk_data_length = (((((((ord(substr($hdr, 7, 1))) * 256) + (ord(substr($hdr, 6, 1)))) * 256) + (ord(substr($hdr, 5, 1)))) * 256) + (ord(substr($hdr, 4, 1)))); return undef if ((substr($hdr, 8, 4) ne "WAVE")); return undef if ((substr($hdr, 12, 4) ne "fmt ")); $format_data_length = (((((((ord(substr($hdr, 19, 1))) * 256) + (ord(substr($hdr, 18, 1)))) * 256) + (ord(substr($hdr, 17, 1)))) * 256) + (ord(substr($hdr, 16, 1)))); return undef if ($format_data_length < 16); $compression_code = (((ord(substr($hdr, 21, 1))) * 256) + (ord(substr($hdr, 20, 1)))); return undef if ($compression_code != 1); $channel_count = (((ord(substr($hdr, 23, 1))) * 256) + (ord(substr($hdr, 22, 1)))); return undef if ($channel_count < 1); $sample_rate = (((((((ord(substr($hdr, 27, 1))) * 256) + (ord(substr($hdr, 26, 1)))) * 256) + (ord(substr($hdr, 25, 1)))) * 256) + (ord(substr($hdr, 24, 1)))); $sample_bit_length = (((ord(substr($hdr, 35, 1))) * 256) + (ord(substr($hdr, 34, 1)))); return undef if ((substr($hdr, $format_data_length + 20, 4) ne "data")); $data_length = (((((((ord(substr($hdr, $format_data_length + 24 + 3, 1))) * 256) + (ord(substr($hdr, $format_data_length + 24 + 2, 1)))) * 256) + (ord(substr($hdr, $format_data_length + 24 + 1, 1)))) * 256) + (ord(substr($hdr, $format_data_length + 24 + 0, 1)))); return ($chunk_data_length, $format_data_length, $compression_code, $channel_count, $sample_rate, $sample_bit_length, $data_length); } sub forge_wav_hdr { my ($hdr) = shift; my ($data_len) = shift; my ($chunk_data_len) = $data_len + 36; return (substr($hdr, 0, 4) . chr($chunk_data_len % 256) . chr((int($chunk_data_len / 256)) % 256) . chr((int($chunk_data_len / (256 * 256))) % 256) . chr((int($chunk_data_len / (256 * 256 * 256))) % 256) . substr($hdr, 8, length($hdr) - 8 - 4) . chr($data_len % 256) . chr((int($data_len / 256)) % 256) . chr((int($data_len / (256 * 256))) % 256) . chr((int($data_len / (256 * 256 * 256))) % 256)); } sub forge_continuous_wav_hdr { my ($hdr) = shift; return (substr($hdr, 0, 4) . chr(0xff) . chr(0xff) . chr(0xff) . chr(0xff) . substr($hdr, 8, length($hdr) - 8 - 4) . chr(0xff - length($hdr) - 8) . chr(0xff) . chr(0xff) . chr(0xff)); } sub passthru_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($tf) = tmp_file(); my ($f) = undef; if (! open($f, "< $fn")) { return undef; } wav_loop($f, $off, undef); # Ignore $forgelen close($f); } sub flac_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($r) = undef; my ($w) = undef; $pid = open2($r, $w, 'flac', '--silent', '--decode', '--stdout', "$fn"); wav_loop($r, $off, undef); # Ignore $forgelen close($r); close($w); } sub mpeg4_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($f) = undef; my ($hdr) = undef; my ($r) = undef; if (open($f, "< $fn")) { binmode($f); my ($rl) = sysread($f, $hdr, 512); close($f); if ($rl != 512) { return undef; } } else { return undef; } # # This detection is really rudimentary, but seems to # do the job for now. # if (index($hdr, "Halac") >= 0) { $r = alac_proc($fn, $off, $forgelen); } else { $r = ffmpeg_proc($fn, $off, $forgelen); } return $r; } sub alac_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($r) = undef; my ($w) = undef; $pid = open2($r, $w, 'alac', "$fn"); wav_loop($r, $off, undef); # Ignore $forgelen close($r); close($w); } BEGIN { $tf = tmp_file(); unlink($tf); } END { if ($pid) { if (waitpid($pid, WNOHANG) <= 0) { kill(SIGKILL, $pid); waitpid($pid, 0); } } unlink($tf); }