#!/usr/bin/env perl # # $Id$ # # -*- 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 ":sys_wait_h"; use IO::Handle; use IO::File; use IPC::Open2; use IPC::Open3; $SIG{PIPE} = 'IGNORE'; { 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(); } 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/^..*\.avi$/i) { ffmpeg_proc($fn, $off, $forgelen); } elsif ($fn =~ m/^..*\.au$/i) { ffmpeg_proc($fn, $off, $forgelen); } else { mplayer_proc($fn, $off, $forgelen); } exit; } sub usage { print STDERR "usage: ssc-script file offset\n"; exit -1; } sub tmp_file { my ($r) = sprintf "/tmp/t-%d.wav", $$; return $r; } sub wav_loop { my ($f) = shift; my ($off) = shift; my ($forgelen) = shift; my ($buf); my ($buflen); my ($buftry) = 4096; my ($hdr); $hdr = read_wav_hdr($f); if (! defined $hdr) { 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 "Can't parse WAV header.\n"; 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 "Write failed.\n"; return undef; } } while (1) { $buflen = sysread($f, $buf, $buftry); if ($buflen < 1) { return 1; } if (syswrite (STDOUT, $buf) != $buflen) { print STDERR "Write failed.\n"; return undef; } } } sub read_wav_hdr { my ($f) = shift; my ($hdr) = undef; my ($format_data_length); if (sysread($f, $hdr, 44) != 44) { print STDERR "Can't read WAV header.\n"; 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 "Can't read WAV header.\n"; 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; my ($pid); $pid = open2($r, $w, 'flac', '--silent', '--decode', '--stdout', "$fn"); wav_loop($r, $off, undef); # Ignore $forgelen close($r); close($w); if (waitpid($pid, WNOHANG) <= 0) { kill(SIGKILL, $pid); waitpid($pid, 0); } } sub ffmpeg_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($r) = undef; my ($w) = undef; my ($pid); $pid = open2($r, $w, 'ffmpeg', '-vn', '-i', "$fn", '-f', 'wav', '-'); wav_loop($r, $off, $forgelen); close($r); close($w); if (waitpid($pid, WNOHANG) <= 0) { kill(SIGKILL, $pid); waitpid($pid, 0); } } sub mplayer_proc { my ($fn) = shift; my ($off) = shift; my ($forgelen) = shift; my ($tf) = tmp_file(); my ($w) = undef; my ($f) = undef; my ($pid); my ($i); my ($started) = undef; my ($waited) = undef; my ($ready) = undef; my ($prebuf) = 2 * 1024 * 1024; unlink($tf); $pid = open3('>&STDERR', $w, '>&STDERR', 'mplayer', '-quiet', '-really-quiet', '-vo', 'null', '-nomouseinput', '-nojoystick', '-nolirc', '-noconsolecontrols', '-nortc', '-ao', "pcm:file=$tf", "$fn"); # Wait just to see, if the decoding starts. # If the resulting wav is less than 512 bytes, # This will fail, but who cares. foreach $i (0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2) { my ($s); my ($wp); select(undef, undef, undef, $i); $wp = waitpid($pid, WNOHANG); ($_, $_, $_, $_, $_, $_, $_, $s, $_, $_, $_, $_, $_) = stat("$tf"); if (defined $s && ($s > 512)) { $started = 1; } if ($wp > 0) { $waited = 1; } if ($started || $waited) { last; } } if (! $started) { kill(SIGKILL, $pid); unlink($tf); } # Loop here until we got data beyond the offset point # or the process terminates. my ($sleep) = 0.025; while (! $waited) { my ($s); my ($wp); if ($sleep < 0.5) { $sleep *= 2.0; } select(undef, undef, undef, $sleep); $wp = waitpid($pid, WNOHANG); ($_, $_, $_, $_, $_, $_, $_, $s, $_, $_, $_, $_, $_) = stat("$tf"); if (defined $s && ($s > ($off + $prebuf))) { $ready = 1; } if ($wp > 0) { $waited = 1; } if ($ready || $waited) { last; } } # Stream. if (open($f, "< $tf")) { wav_loop($f, $off, $forgelen); close($f); } if (! $waited) { waitpid($pid, 0); } close($w); unlink($tf); } 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")) { 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; my ($pid); $pid = open2($r, $w, 'alac', "$fn"); wav_loop($r, $off, undef); # Ignore $forgelen close($r); close($w); if (waitpid($pid, WNOHANG) <= 0) { kill(SIGKILL, $pid); waitpid($pid, 0); } }