Index: Slim/Player/Client.pm =================================================================== --- Slim/Player/Client.pm (revision 17943) +++ Slim/Player/Client.pm (working copy) @@ -235,6 +235,8 @@ $client->[125] = undef; # jiffiesEpoch $client->[126] = []; # jiffiesOffsetList; array tracking the relative deviations relative to our clock $client->[127] = undef; # frameData; array of (stream-byte-offset, stream-time-offset) tuples + + $client->[128] = 0; # initialAudioBlockRemaining $clientHash{$id} = $client; @@ -1712,4 +1714,9 @@ @_ ? ($r->[127] = shift) : $r->[127]; } +sub initialAudioBlockRemaining { + my $r = shift; + @_ ? ($r->[128] = shift) : $r->[128]; +} + 1; Index: Slim/Player/Source.pm =================================================================== --- Slim/Player/Source.pm (revision 17943) +++ Slim/Player/Source.pm (working copy) @@ -95,7 +95,7 @@ } } -sub time2offset { +sub _time2offset { my $client = shift; my $time = shift; @@ -114,14 +114,25 @@ my $byterate = $duration ? ($size / $duration) : 0; my $offset = int($byterate * $time); - if (my $streamClass = streamClassForFormat($client)) { + my $streamClass = _streamClassForFormat($client); - $offset = $streamClass->findFrameBoundaries($client->audioFilehandle, $offset); + if (!defined($song->{'initialAudioBlock'}) && + $streamClass && $streamClass->can('getInitialAudioBlock')) + { + $song->{'initialAudioBlock'} = $streamClass->getInitialAudioBlock($client->audioFilehandle); + my $length = length($song->{'initialAudioBlock'}); + $log->debug("Got initial audio block of size $length"); + if ($offset < $length) { + $offset = $length; + } + } + if ($streamClass && $streamClass->can('findFrameBoundaries')) { + $offset = $streamClass->findFrameBoundaries($client->audioFilehandle, $offset); } else { - $offset -= $offset % $align; } + $log->info("$time to $offset (align: $align size: $size duration: $duration)"); @@ -1042,7 +1053,7 @@ $newtime += $oldtime; } - my $newoffset = time2offset($client, $newtime); + my $newoffset = _time2offset($client, $newtime); if ($rangecheck) { @@ -1125,6 +1136,8 @@ $client->bytesReceivedOffset(0); $client->trickSegmentRemaining(0); resetFrameData($client); + $client->initialAudioBlockRemaining(defined($song->{'initialAudioBlock'}) ? + length($song->{'initialAudioBlock'}) : 0); $client->audioFilehandle()->sysseek($newoffset + $dataoffset, 0); @@ -1628,6 +1641,7 @@ $client->songBytes(0); $client->bytesReceivedOffset($client->bytesReceived()); $client->trickSegmentRemaining(0); + $client->initialAudioBlockRemaining(0); resetFrameData($client); } @@ -1965,65 +1979,6 @@ $offset -= $seekoffset; } - if ( $format eq 'mp3' ) { - - # report whether the track should play back gapless or not - my $streamClass = streamClassForFormat($client, 'mp3'); - my $frame = $streamClass->getFrame( $client->audioFilehandle ); - - # Look for the LAME header and delay data in the frame - if ( $frame ) { - my $io = IO::String->new( \$frame->asbin ); - - if ( my $info = MP3::Info::get_mp3info($io) ) { - - if ( $log->is_debug ) { - if ( $info->{LAME} ) { - - $log->info("MP3 file was encoded with $info->{'LAME'}->{'encoder_version'}"); - - if ( $info->{LAME}->{start_delay} ) { - - $log->info(sprintf("MP3 contains encoder delay information (%d/%d), will be played gapless", - $info->{LAME}->{start_delay}, - $info->{LAME}->{end_padding}, - )); - } - else { - $log->info("MP3 doesn't contain encoder delay information, won't play back gapless"); - } - } - else { - $log->info("MP3 wasn't encoded with LAME, won't play back gapless"); - } - } - - if ( $info->{BITRATE} ) { - if ($log->is_debug && $bitrate && $info->{BITRATE}*1000 != $bitrate) { - $log->debug( - "Track bitrate $bitrate differs from MP3::Info rate ". - ($info->{BITRATE}*1000) - ); - } - $bitrate ||= $info->{BITRATE}*1000; - } - - if ( $info->{FREQUENCY} ) { - my $frequency = int($info->{FREQUENCY} * 1000); - if ($log->is_debug && $samplerate && $frequency != $samplerate) { - $log->debug("Track samplerate $samplerate differs from MP3::Info rate $frequency"); - } - $samplerate ||= $frequency; - } - - $channels ||= $info->{STEREO} ? 2 : 1; - } - } - else { - $log->warn('Unable to find MP3 frame in file!'); - } - } - # pipe is a socket if (-p $filepath) { $client->audioFilehandleIsSocket(1); @@ -2099,6 +2054,17 @@ gototime($client, $duration, 1); return 1; } + + # Deal with the case where we are fast-forwarding and get to + # this song. In this case, we should jump to the start of + # the newly opened song using gototime so that we fully + # include any initial header. + elsif (rate($client) > 1 && !$client->audioFilehandleIsSocket()) { + # Clear out the song queue to just include this song + streamingSongIndex($client, streamingSongIndex($client), 1, $song); + gototime($client, 0, 1); + return 1; + } } else { @@ -2207,6 +2173,28 @@ my $song = streamingSong($client); + if (my $length = $client->initialAudioBlockRemaining()) { + + my $chunkLength = $length; + my $chunkref; + + $log->debug("getting initial audio block of size $length"); + + if ($length > $givenChunkSize) { + $chunkLength = $givenChunkSize; + $chunk = substr($song->{'initialAudioBlock'}, -$length, $chunkLength); + $client->initialAudioBlockRemaining($length - $chunkLength); + $chunkref = \$chunk; + } else { + $client->initialAudioBlockRemaining(0); + $chunkref = \$song->{'initialAudioBlock'}; + } + + $client->streamBytes($client->streamBytes() + $chunkLength); + + return $chunkref; + } + if ($client->audioFilehandle()) { if (!$client->audioFilehandleIsSocket) { @@ -2262,7 +2250,9 @@ $tricksegmentbytes -= $tricksegmentbytes % $song->{blockalign}; # Find the frame boundaries for the streaming format, and seek to them. - if (my $streamClass = streamClassForFormat($client)) { + my $streamClass; + if (($streamClass = _streamClassForFormat($client)) && + $streamClass->can('findFrameBoundaries')) { my ($start, $end) = $streamClass->findFrameBoundaries( $client->audioFilehandle, $seekpos, $tricksegmentbytes @@ -2460,19 +2450,13 @@ return \$chunk; } -sub streamClassForFormat { +sub _streamClassForFormat { my ( $client, $streamFormat ) = @_; $streamFormat ||= $client->streamformat; if (Slim::Formats->loadTagFormatForType($streamFormat)) { - - my $streamClass = Slim::Formats->classForFormat($streamFormat); - - if ($streamClass && $streamClass->can('findFrameBoundaries')) { - - return $streamClass; - } + return Slim::Formats->classForFormat($streamFormat); } } Index: Slim/Formats/Ogg.pm =================================================================== --- Slim/Formats/Ogg.pm (revision 17943) +++ Slim/Formats/Ogg.pm (working copy) @@ -26,6 +26,7 @@ use strict; use base qw(Slim::Formats); +use Fcntl qw(:seek); use Slim::Utils::Log; use Slim::Utils::Unicode; @@ -202,4 +203,143 @@ return (-1, undef); } +sub getInitialAudioBlock { + my ($class, $fh) = @_; + + open(my $localFh, '<&=', $fh); + + seek($localFh, 0, 0); + my $ogg = Ogg::Vorbis::Header::PurePerl->new($localFh); + + seek($localFh, 0, 0); + logger('player.source')->debug('Reading initial audio blcok: length ' . ($ogg->info('headers_length'))); + read ($localFh, my $buffer, $ogg->info('headers_length')); + + close($localFh); + + return $buffer; +} + +=head2 findFrameBoundaries( $fh, $offset, $seek ) + +Starts seeking from $offset (bytes relative to beginning of file) until it +finds the next valid frame header. Returns the offset of the first and last +bytes of the frame if any is found, otherwise (0, 0). + +If the caller does not request an array context, only the first (start) position is returned. + +The only caller is L at this time. + +=cut + +sub findFrameBoundaries { + my ($class, $fh, $offset, $seek) = @_; + + if (!defined $fh || !defined $offset) { + logError("Invalid arguments!"); + return wantarray ? (0, 0) : 0; + } + + my $start = $class->_seekNextFrame($fh, $offset, 1); + my $end = 0; + + if (defined $seek) { + $end = $class->_seekNextFrame($fh, $offset + $seek, 1); + return ($start, $end); + } + + return wantarray ? ($start, $end) : $start; +} + +my $HEADERLEN = 28; # minumum +my $MAXDISTANCE = 255 * 255 + 26 + 256; + +# seekNextFrame: +# +# when scanning forward ($direction=1), simply detects the next frame header. +# +# when scanning backwards ($direction=-1), returns the next frame header whose +# frame length is within the distance scanned (so that when scanning backwards +# from EOF, it skips any truncated frame at the end of block. + +sub _seekNextFrame { + my ($class, $fh, $startoffset, $direction) = @_; + + use bytes; + + if (!defined $fh || !defined $startoffset || !defined $direction) { + logError("Invalid arguments!"); + return 0; + } + + my $filelen = -s $fh; + if ($startoffset > $filelen) { + $startoffset = $filelen; + } + + # TODO: MAXDISTANCE is far too far to seek backwards in most cases, so we don't + # use negative direction for the moment. + my $seekto = ($direction == 1) ? $startoffset : $startoffset - $MAXDISTANCE; + my $log = logger('player.source'); + + $log->debug("Reading $MAXDISTANCE bytes at: $seekto (to scan direction: $direction)"); + + sysseek($fh, $seekto, SEEK_SET); + sysread($fh, my $buf, $MAXDISTANCE, 0); + + my $len = length($buf); + + if ($len < $HEADERLEN) { + $log->warn("Got less than $HEADERLEN bytes"); + return 0; + } + + my ($start, $end) = (0, 0); + + if ($direction == 1) { + $start = 0; + $end = $len - $HEADERLEN; + } else { + $start = $len - $HEADERLEN; + $end = 0; + } + + $log->debug("Scanning: len = $len, start = $start, end = $end"); + + for (my $pos = $start; $pos != $end; $pos += $direction) { + + my $head = substr($buf, $pos, $HEADERLEN); + + if (!_isOggPageHeader($head)) { + next; + } + + my $found_at_offset = $seekto + $pos; + + $log->debug("Found frame header at $found_at_offset"); + + return $found_at_offset; + } + + $log->warn("Couldn't find any frame header"); + + return 0; +} + +# This is a pretty minimal test, liable to false positives +# but it does not really matter as the player decoder will +# resync properly if need be (as it has to anyway because +# of the non-coincidence or page and packet boundaries). +sub _isOggPageHeader { + my $buffer = shift; + + if (substr($buffer, 0, 4) ne 'OggS') {return 0;} + + if (ord(substr($buffer, 4, 1)) != 0) {return 0;} + + if (ord(substr($buffer, 5, 1)) & ~5) {return 0;} + + return 1; +} + 1; Index: lib/Ogg/Vorbis/Header/PurePerl.pm =================================================================== --- lib/Ogg/Vorbis/Header/PurePerl.pm (revision 17943) +++ lib/Ogg/Vorbis/Header/PurePerl.pm (working copy) @@ -14,21 +14,36 @@ my $class = shift; my $file = shift; - # open up the file - open FILE, $file or do { - warn "$class: File $file does not exist or cannot be read: $!"; - return undef; - }; + my %data; + + if (ref $file) { + binmode $file; + + %data = ( + 'filesize' => -s $file, + 'fileHandle' => $file, + ); + + } + + else { + + # open up the file + open FILE, $file or do { + warn "$class: File $file does not exist or cannot be read: $!"; + return undef; + }; + + # make sure dos-type systems can handle it... + binmode FILE; + + %data = ( + 'filename' => $file, + 'filesize' => -s $file, + 'fileHandle' => \*FILE, + ); + } - # make sure dos-type systems can handle it... - binmode FILE; - - my %data = ( - 'filename' => $file, - 'filesize' => -s $file, - 'fileHandle' => \*FILE, - ); - _init(\%data); _loadInfo(\%data); _loadComments(\%data); @@ -263,8 +278,18 @@ $byteCount += 5; # then skip on past it - substr($buffer, 0, $page_segments, ''); + my $segmentSizes = substr($buffer, 0, $page_segments, ''); $byteCount += $page_segments; + + my $contentSize = 0; + for (my $i = 0; $i < $page_segments; $i++) { + my $segmentSize = ord(substr($segmentSizes, $i, 1)); + $contentSize += $segmentSize; + } + + # This assumes the normal case that the three mandatory Vorbis header packets are + # completely contained in the first 2 Ogg pages. + $data->{'INFO'}{'headers_length'} = $contentSize + $byteCount; # check the header type (should be 0x03) if (ord(substr($buffer, 0, 1, '')) != 0x03) {