[Slim-Checkins] r12808 - in /trunk/server: CPAN/MPEG/Audio/ CPAN/Math/ Firmware/ Slim/Networking/ Slim/Networking/SliMP3/ Slim/Player/ Slim/Player/Protocols/ lib/MPEG/ lib/MPEG/Audio/

andy at svn.slimdevices.com andy at svn.slimdevices.com
Thu Aug 30 21:08:55 PDT 2007


Author: andy
Date: Thu Aug 30 21:08:54 2007
New Revision: 12808

URL: http://svn.slimdevices.com?rev=12808&view=rev
Log:
Improved sync patch from Alan Young.

The server is now able to track how synced players are playing in relation to each other and adjust the audio if they drift apart.

Added:
    trunk/server/CPAN/Math/
    trunk/server/CPAN/Math/VecStat.pm
    trunk/server/Firmware/squeezebox2_82.bin   (with props)
    trunk/server/Firmware/transporter_32.bin   (with props)
    trunk/server/lib/MPEG/
    trunk/server/lib/MPEG/Audio/
    trunk/server/lib/MPEG/Audio/Frame.pm
Removed:
    trunk/server/CPAN/MPEG/Audio/Frame.pm
    trunk/server/Firmware/squeezebox2_81.bin
    trunk/server/Firmware/transporter_31.bin
Modified:
    trunk/server/Firmware/squeezebox2.version
    trunk/server/Firmware/transporter.version
    trunk/server/Slim/Networking/SliMP3/Protocol.pm
    trunk/server/Slim/Networking/SliMP3/Stream.pm
    trunk/server/Slim/Networking/Slimproto.pm
    trunk/server/Slim/Networking/UDP.pm
    trunk/server/Slim/Player/Client.pm
    trunk/server/Slim/Player/Player.pm
    trunk/server/Slim/Player/Protocols/HTTP.pm
    trunk/server/Slim/Player/SLIMP3.pm
    trunk/server/Slim/Player/Source.pm
    trunk/server/Slim/Player/Squeezebox.pm
    trunk/server/Slim/Player/Squeezebox2.pm
    trunk/server/Slim/Player/Sync.pm

Added: trunk/server/CPAN/Math/VecStat.pm
URL: http://svn.slimdevices.com/trunk/server/CPAN/Math/VecStat.pm?rev=12808&view=auto
==============================================================================
--- trunk/server/CPAN/Math/VecStat.pm (added)
+++ trunk/server/CPAN/Math/VecStat.pm Thu Aug 30 21:08:54 2007
@@ -1,0 +1,347 @@
+
+require Exporter;
+package Math::VecStat;
+ at Math::VecStat::ISA=qw(Exporter);
+ at EXPORT_OK=qw(max min maxabs minabs sum average
+	vecprod ordered convolute
+	sumbyelement diffbyelement
+	allequal median);
+$Math::VecStat::VERSION = '0.08';
+
+use strict;
+
+sub max {
+  my $v=ref($_[0]) ? $_[0] : \@_;
+  my $i=$#{$v};
+  my $j=$i;
+  my $m=$v->[$i];
+  while (--$i >= 0) { if ($v->[$i] > $m) { $m=$v->[$i]; $j=$i; }}
+  return wantarray ? ($m,$j): $m;
+}
+
+sub min {
+  my $v=ref($_[0]) ? $_[0] : \@_;
+  my $i=$#{$v};
+  my $j=$i;
+  my $m=$v->[$i];
+  while (--$i >= 0) { if ($v->[$i] < $m) { $m=$v->[$i]; $j=$i; }}
+  return wantarray ? ($m,$j): $m;
+}
+
+sub maxabs {
+  my $v=ref($_[0]) ? $_[0] : \@_;
+  my $i=$#{$v};
+  my $j=$i;
+  my $m=abs($v->[$i]);
+  while (--$i >= 0) { if (abs($v->[$i]) > $m) { $m=abs($v->[$i]); $j=$i}}
+  return (wantarray ? ($m,$j) : $m);
+}
+
+sub minabs {
+   my $v=ref($_[0]) ? $_[0] : \@_;
+   my $i=$#{$v};
+   my $j=$i;
+   my $m=abs($v->[$i]);
+   while (--$i >= 0) { if (abs($v->[$i]) < $m) { $m=abs($v->[$i]); $j=$i}}
+   return (wantarray ? ($m,$j) : $m);
+}
+
+sub sum {
+  my $v=ref($_[0]) ? $_[0] : \@_;
+  my $s=0;
+  foreach(@{$v}) { $s+=$_; }
+  return $s;
+}
+
+# spinellia at acm.org, handle the empty array case
+sub average {
+  my $v=ref($_[0]) ? $_[0] : \@_;
+  return undef unless $#{$v} >= 0;
+  return $#{$v}==-1 ? 0 : sum($v)/(1+$#{$v});
+}
+
+sub vecprod {
+  my $c = shift;
+  my $v=ref($_[0]) ? $_[0] : \@_;
+  return undef unless $#{$v} >= 0;
+  my @result = map( $_ * $c, @{$v} );
+  return \@result;
+}
+
+sub ordered
+{
+	my $v=ref($_[0]) ? $_[0] : \@_;
+	if( scalar( @{$v} ) < 2 ){ return 1; }
+	for(my $i=0; $i<$#{$v}; $i++ ){
+		return 0 if $v->[$i] > $v->[$i+1];
+	}
+	return 1;
+}
+
+sub allequal
+{
+	my($v,$u) = @_;
+	return undef unless (defined($v) and defined($u)); # this is controversial
+	return undef unless ($#{$v} == $#{$u});
+	my $i= @{$v};
+	while (--$i >= 0) { return 0 unless( $v->[$i] == $u->[$i]); }
+	return 1;
+}
+
+sub sumbyelement
+{
+	my($v,$u) = @_;
+
+	return undef unless ($#{$v} == $#{$u});
+	my @summed;
+	my $i= @{$v};
+	while (--$i >= 0) { $summed[$i] = $v->[$i] + $u->[$i]; }
+	return \@summed;
+}
+
+sub diffbyelement
+{
+	my($v,$u) = @_;
+
+	return undef unless ($#{$v} == $#{$u});
+	my @summed;
+	my $i= @{$v};
+	while (--$i >= 0) { $summed[$i] = $v->[$i] - $u->[$i]; }
+	return \@summed;
+}
+
+sub convolute
+{
+	my($v,$u) = @_;
+
+	return undef unless ($#{$v} == $#{$u});
+	my @conv;
+	my $i= @{$v};
+	while (--$i >= 0) { $conv[$i] = $v->[$i]*$u->[$i]; }
+	return \@conv;
+}
+
+sub _justToAvoidWarnings
+{
+	my $a = $Math::VecStat::VERSION;
+}
+
+sub median
+{
+	my $v=ref($_[0]) ? $_[0] : \@_;
+	my $n = scalar @{$v};
+
+# generate a list of [value,index] pairs
+	my @tras =	map( [$v->[$_],$_], 0..$#{$v} );
+# sort by ascending value, then by original position
+# suggested by david at jamesgang.com
+	my @sorted = sort { ($a->[0] <=> $b->[0])
+		or ($a->[1] <=> $b->[1]) } @tras;
+# find the middle ordinal
+	my $med = int( $n / 2 );
+
+# when there are several identical median values
+# we arbitrarily (but consistently) choose the first one
+# in the original array
+
+	while( ($med >= 1) && ($sorted[$med]->[0] == $sorted[$med-1]->[0]) ){
+		$med--;
+	}
+
+	return $sorted[$med];
+}
+
+
+1;
+
+__END__
+
+# $Id: VecStat.pm,v 1.5 1997/02/26 17:20:37 willijar Exp $
+
+=head1 NAME
+
+    Math::VecStat - Some basic numeric stats on vectors
+
+=head1 SYNOPSIS
+
+    use Math::VecStat qw(max min maxabs minabs sum average);
+    $max=max(@vector);
+    $max=max(\@vector);
+    ($max,$imax)=max(@vector);
+    ($max,$imax)=max(\@vector);
+    $min=min(@vector);
+    $min=min(\@vector);
+    ($max,$imin)=min(@vector);
+    ($max,$imin)=min(\@vector);
+    $max=maxabs(@vector);
+    $max=maxabs(\@vector);
+    ($max,$imax)=maxabs(@vector);
+    ($max,$imax)=maxabs(\@vector);
+    $min=minabs(@vector);
+    $min=minabs(\@vector);
+    ($max,$imin)=minabs(@vector);
+    ($max,$imin)=minabs(\@vector);
+    $sum=sum($v1,$v2,...);
+    $sum=sum(@vector);
+    $sum=sum(\@vector);
+    $average=average($v1,$v2,...);
+    $av=average(@vector);
+    $av=average(\@vector);
+    $ref=vecprod($scalar,\@vector);
+    $ok=ordered(@vector);
+    $ok=ordered(\@vector);
+    $ref=sumbyelement(\@vector1,\@vector2);
+    $ref=diffbyelement(\@vector1,\@vector2);
+    $ok=allequal(\@vector1,\@vector2);
+    $ref=convolute(\@vector1,\@vector2);
+
+=head1 DESCRIPTION
+
+This package provides some basic statistics on numerical
+vectors. All the subroutines can take
+a reference to the vector to be operated
+on. In some cases a copy of the vector is acceptable,
+but is not recommended for efficiency.
+
+=over 5
+
+=item  max(@vector), max(\@vector)
+
+return the maximum value of given values or vector. In an array
+context returns the value and the index in the array where it
+occurs.
+
+=item min(@vector), min(\@vector)
+
+return the minimum value of given values or vector, In an array
+context returns the value and the index in the array where it
+occurs.
+
+
+=item maxabs(@vector), maxabs(\@vector)
+
+return the maximum value of absolute of the given values or vector. In
+an array context returns the value and the index in the array where it
+occurs.
+
+=item minabs(@vector), minabs(\@vector)
+
+return the minimum value of the absolute of the given values or
+vector. In an array context returns the value and the index in the
+array where it occurs.
+
+=item sum($v1,$v2,...), sum(@vector), sum(\@vector)
+
+return the sum of the given values or vector
+
+=item average($v1,$v2,..), average(@vector), average(\@vector)
+
+return the average of the given values or vector
+
+=item vecprod($a,$v1,$v2,..), vecprod($a, at vector), vecprod( $a, \@vector )
+
+return a vector built by multiplying the scalar $a by each element of the
+ at vector.
+
+=item ordered($v1,$v2,..), ordered(@vector), ordered(\@vector)
+
+return nonzero iff the vector is nondecreasing with respect to its index.
+To be used like
+
+  if( ordered( $lowBound, $value, $highBound ) ){
+
+instead of the (slightly) more clumsy
+
+  if( ($lowBound <= $value) && ($value <= $highBound) ) {
+
+=item sumbyelement( \@array1, \@array2 ), diffbyelement(\@array1,\@array2)
+
+return the element-by-element sum or difference of two
+identically-sized vectors. Given
+
+  $s = sumbyelement( [10,20,30], [1,2,3] );
+  $d = diffbyelement( [10,20,30], [1,2,3] );
+
+C<$s> will be C<[11,22,33]>, C<$d> will be C<[9,18,27]>.
+
+=item allequal( \@array1, \@array2 )
+
+returns true if and only if the two arrays are numerically identical.
+
+=item convolute( \@array1, \@array2 )
+
+return a reference to an array containing the element-by-element
+product of the two input arrays. I.e.,
+
+  $r = convolute( [1,2,3], [-1,2,1] );
+
+returns a reference to
+
+  [-1,4,3]
+
+=item median
+
+evaluates the median, i.e. an element which separates the population
+in two halves.  It returns a reference to a list whose first element
+is the median value and the second element is the index of the
+median element in the original vector.
+
+  $a = Math::VecStat::median( [9,8,7,6,5,4,3,2,1] );
+
+returns the list reference
+
+  [ 5, 4 ]
+
+i.e. the median value is 5 and it is found at position 4 of the
+original array.
+
+If there are several elements of the array
+having the median value, e.g. [1,3,3,3,5].  In this case
+we choose always the first element in the original vector
+which is a median. In the example, we return [3,1].
+
+=head1 HISTORY
+
+ $Log: VecStat.pm,v $
+ Revision 1.9  2003/04/20 00:49:00 spinellia at acm.org
+ Perl 5.8 broke test 36, exposing inconsistency in C<median>.  Fixed, thanks to david at jamesgang.com.
+
+ Revision 1.8  2001/01/26 11:10:00 spinellia at acm.org
+ Added function median.
+ Fixed test, thanks to Andreas Marcel Riechert <riechert at pobox.com>
+
+ Revision 1.7  2000/10/24 15:28:00  spinellia at acm.org
+ Added functions allequal diffbyelement
+ Created a reasonable test suite.
+
+ Revision 1.6  2000/06/29 16:06:37  spinellia at acm.org
+ Added functions vecprod, convolute, sumbyelement
+
+ Revision 1.5  1997/02/26 17:20:37  willijar
+ Added line before pod header so pod2man installs man page correctly
+
+ Revision 1.4  1996/02/20 07:53:10  willijar
+ Added ability to return index in array contex to max and min
+ functions. Added minabs and maxabs functions.
+ Thanks to Mark Borges <mdb at cdc.noaa.gov> for these suggestions.
+
+ Revision 1.3  1996/01/06 11:03:30  willijar
+ Fixed stupid bug that crept into looping in min and max functions
+
+ Revision 1.2  1995/12/26 09:56:38  willijar
+ Oops - removed xy data functions.
+
+ Revision 1.1  1995/12/26 09:39:07  willijar
+ Initial revision
+
+=head1 BUGS
+
+Let me know. I welcome any appropriate additions for this package.
+
+=head1 AUTHORS
+
+John A.R. Williams <J.A.R.Williams at aston.ac.uk>
+Andrea Spinelli <spinellia at acm.org>
+
+=cut
+

Modified: trunk/server/Firmware/squeezebox2.version
URL: http://svn.slimdevices.com/trunk/server/Firmware/squeezebox2.version?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Firmware/squeezebox2.version (original)
+++ trunk/server/Firmware/squeezebox2.version Thu Aug 30 21:08:54 2007
@@ -3,8 +3,8 @@
 # from        to
 # from1..from2 to
 
-1..81 81
+1..82 82
 
 # future versions may be downgraded to this one
-* 81
+* 82
 

Added: trunk/server/Firmware/squeezebox2_82.bin
URL: http://svn.slimdevices.com/trunk/server/Firmware/squeezebox2_82.bin?rev=12808&view=auto
==============================================================================
Binary file - no diff available.

Propchange: trunk/server/Firmware/squeezebox2_82.bin
------------------------------------------------------------------------------
    svn:executable = *

Propchange: trunk/server/Firmware/squeezebox2_82.bin
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Modified: trunk/server/Firmware/transporter.version
URL: http://svn.slimdevices.com/trunk/server/Firmware/transporter.version?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Firmware/transporter.version (original)
+++ trunk/server/Firmware/transporter.version Thu Aug 30 21:08:54 2007
@@ -3,8 +3,8 @@
 # from        to
 # from1..from2 to
 
-1..31 31
+1..32 32
 
 # future versions may be downgraded to this one
-* 31
+* 32
 

Added: trunk/server/Firmware/transporter_32.bin
URL: http://svn.slimdevices.com/trunk/server/Firmware/transporter_32.bin?rev=12808&view=auto
==============================================================================
Binary file - no diff available.

Propchange: trunk/server/Firmware/transporter_32.bin
------------------------------------------------------------------------------
    svn:executable = *

Propchange: trunk/server/Firmware/transporter_32.bin
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Modified: trunk/server/Slim/Networking/SliMP3/Protocol.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Networking/SliMP3/Protocol.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Networking/SliMP3/Protocol.pm (original)
+++ trunk/server/Slim/Networking/SliMP3/Protocol.pm Thu Aug 30 21:08:54 2007
@@ -25,7 +25,7 @@
 my $prefs = preferences('server');
 
 sub processMessage {
-	my ($client, $msg) = @_;
+	my ($client, $msg, $msgTimeStamp) = @_;
 
 	my $type   = unpack('a',$msg);
 
@@ -42,7 +42,9 @@
 	if ($type eq 'i') {
 
 		# extract the IR code and the timestamp for the IR message
-		my ($irTime, $irCodeBytes) = unpack 'xxNxxH8', $msg;	
+		my ($irTime, $irCodeBytes) = unpack 'xxNxxH8', $msg;
+		
+		$client->trackJiffiesEpoch($irTime, $msgTimeStamp);	
 		
 		Slim::Hardware::IR::enqueue($client, $irCodeBytes, $irTime);
 
@@ -55,8 +57,8 @@
 	} elsif ($type eq 'a') {
 
 		my ($wptr, $rptr, $seq) = unpack 'xxxxxxnnn', $msg;
-
-		Slim::Networking::SliMP3::Stream::gotAck($client, $wptr, $rptr, $seq);
+		
+		Slim::Networking::SliMP3::Stream::gotAck($client, $wptr, $rptr, $seq, $msgTimeStamp);
 
 		Slim::Player::Sync::checkSync($client);
 
@@ -105,7 +107,7 @@
 			$client->init;
 
 			# remember all slimp3 clients so we can say hello to them on next server startup
-			my %slimp3 = map { $_ => 1 } @{ $prefs->get('slimp3clients') };
+			my %slimp3 = map { $_ => 1 } @{ $prefs->get('slimp3clients') || [] };
 
 			if (!$slimp3{$id}) {
 				$slimp3{$id} = 1;

Modified: trunk/server/Slim/Networking/SliMP3/Stream.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Networking/SliMP3/Stream.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Networking/SliMP3/Stream.pm (original)
+++ trunk/server/Slim/Networking/SliMP3/Stream.pm Thu Aug 30 21:08:54 2007
@@ -10,7 +10,7 @@
 use strict;
 use bytes;
 
-use Slim::Player::SLIMP3;
+use Slim::Player::Source;
 use Slim::Utils::Log;
 use Slim::Utils::Misc;
 use Slim::Utils::Timers;
@@ -19,26 +19,39 @@
 ###
 ### lots o' knobs:
 ###
-my $TIMEOUT      		= 0.05;  # timeout
-my $ACK_TIMEOUT			= 30.0; # in seconds
-
-my $MAX_PACKET_SIZE		= 1400;
-my $BUFFER_SIZE			= 131072; # in bytes
-my $PAUSE_THRESHOLD		= $MAX_PACKET_SIZE; # pause until we refill the buffer
-my $UNPAUSE_THRESHOLD 		= $BUFFER_SIZE / 2; # fraction of buffer needed full in order to start playing
-my $BUFFER_FULL_THRESHOLD 	= $BUFFER_SIZE - $MAX_PACKET_SIZE * 2; # fraction of buffer to consider full up
-
-my $BUFFER_FULL_DELAY		= 0.05; # seconds to wait until trying to resend packet when the buffer is full
+my $TIMEOUT               = 0.05;  # timeout
+my $ACK_TIMEOUT           = 30.0; # in seconds
+
+my $MAX_PACKET_SIZE       = 1400;
+my $BUFFER_SIZE           = 131072; # in bytes
+my $PAUSE_THRESHOLD       = $MAX_PACKET_SIZE; # pause until we refill the buffer
+my $UNPAUSE_THRESHOLD     = $BUFFER_SIZE / 2; # fraction of buffer needed full in order to start playing
+my $BUFFER_FULL_THRESHOLD = $BUFFER_SIZE - $MAX_PACKET_SIZE * 2; # fraction of buffer to consider full up
+
+my $BUFFER_FULL_DELAY     = 0.05; # seconds to wait until trying to resend packet when the buffer is full
+my $WPTR_LIMIT            = $BUFFER_SIZE / 2; # point at which player decode buffer wraps around
+my $SEQ_LIMIT             = $WPTR_LIMIT; # pretty arbitray, not really related to wptr or anything else
 
 # for each client:
 our %streamState;		# the state of the stream
 our %curWptr;			# the highest outstanding wptr we've sent to the client
 our %bytesSent;			# bytes sent in this stream
 our %seq;				# the next sequence number to send
-our %packetInFlight;		# hash of references of  the packet in flight to this client
+our %dataPktInFlight;	# hash of references of the data packet in flight to this client
+our %emptyPktInFlight;	# hash of references of the empty packet in flight to this client
 our %fullness;			# number of bytes in the buffer as of the last packet
 our %lastAck;			# timeout in the case that the player disappears completely.
 our %lastByte;			# if we get an odd number of bytes from the upper level, hold on to the last one.
+
+# the following are used to track and correct synchronization with other players
+
+our %latencyList;		# array of most-recent packet round-trip-time measures
+use constant LATENCY_LIST_SIZELIMIT		=> 20;		# max number of samples to keep
+use constant LATENCY_LIST_MINSIZE		=> 7;		# min number of sample for valid measure
+our %samplePlayPointAfter;	# next time to sample the play-point
+use constant PLAY_POINT_SAMPLE_INTERVAL	=> 0.500;	# how often to check the play-point
+our %pauseUntil;		# send 'stop' instead of 'go' until past this time
+use constant MIN_DEVIATION_ADJUST		=> 0.030;	# minimum deviation to try and adjust;
 
 my $empty = '';
 
@@ -91,8 +104,16 @@
 	}
 
 	$fullness{$client} = 0;
-	$packetInFlight{$client} = undef;
+	$dataPktInFlight{$client} = undef;
+	$emptyPktInFlight{$client} = undef;
 	$lastAck{$client} = Time::HiRes::time();
+	
+	if (!defined($latencyList{$client})) {
+		$latencyList{$client} = [];
+	}
+	
+	$client->playPoint(undef);
+	$pauseUntil{$client} = 0;
 
 	Slim::Utils::Timers::killOneTimer($client, \&sendNextChunk);
 	
@@ -112,9 +133,30 @@
 =cut
 
 sub pause {
-	my ($client) = @_;
-
-	$log->info($client->id, " pause");
+	my ($client, $interval) = @_;
+
+	$log->info( $client->id, " pause" . ($interval ? " for $interval" : '') );
+	
+	if ($interval) {
+		if ($streamState{$client} ne 'play' && $streamState{$client} ne 'eof') {
+			$::d_stream && msg("Attempted to pause a " . $streamState{$client} .  " stream.\n");
+			return 0;
+		}
+		if ($interval > MIN_DEVIATION_ADJUST) {
+			$interval -= 0.005;	# safety
+			# need to force drain of internal buffer
+			my $bitrate = Slim::Player::Source::streamBitrate($client) || 128000;
+			$interval += 1000 * 8 / $bitrate;
+			$::d_stream && msg($client->id() ." actual interval: $interval \n");
+			$pauseUntil{$client} = Time::HiRes::time() + $interval;
+			sendEmptyChunk($client);
+			$samplePlayPointAfter{$client} = $pauseUntil{$client} + PLAY_POINT_SAMPLE_INTERVAL;
+			$client->playPoint(undef);
+			return 1;
+		} else {
+			return 0;
+		}
+	}
 
 	if ($streamState{$client} ne 'play' && $streamState{$client} ne 'buffering') {
 
@@ -130,6 +172,7 @@
 	} else {
 		sendNextChunk($client);
 	}
+	$client->playPoint(undef);
 	
 	return 1;
 }
@@ -154,9 +197,12 @@
 
 	$streamState{$client} = 'stop';
 
-	sendNextChunk($client);	
+	sendEmptyChunk($client);	
 
 	$client->bytesReceived(0);
+	
+	$fullness{$client} = 0;
+	$client->playPoint(undef);
 
 	return 1;
 }
@@ -177,7 +223,7 @@
 =cut
 
 sub unpause {
-	my ($client) = @_;
+	my ($client, $at) = @_;
 
 	$log->info($client->id, " unpause");
 
@@ -197,7 +243,12 @@
 	} elsif  ($streamState{$client} eq 'paused') {
 
 		$streamState{$client} = 'play';
-		sendNextChunk($client);
+		
+		if ($at) {
+			$pauseUntil{$client} = $at - 0.010;
+		}
+		
+		sendEmptyChunk($client);
 		return 1;	
 
 	} else {
@@ -247,14 +298,9 @@
 	my $len  = $pkt->{'len'};
 	my $wptr = $pkt->{'wptr'};
 
-	$log->debug(
-		$client->id, 
-		" sending stream, seq = $seq len = $len wptr = $wptr state = $streamState{$client}",
-		" inflight = " . defined($packetInFlight{$client}),
-	);
-
 	my $control;
 	my $streamState = $streamState{$client};
+	my $now         = Time::HiRes::time();
 	
 	if (($streamState eq 'stop') || ($bytesSent{$client} == 0)) {
 
@@ -265,10 +311,12 @@
 	} elsif ($streamState eq 'buffering') {
 
 		$control = $streamControlCodes{'reset'};
+		$pauseUntil{$client} = 0;
 
 	} elsif ($streamState eq 'paused') {
 
 		$control = $streamControlCodes{'stop'};
+		$pauseUntil{$client} = 0;
 
 	} elsif ($streamState eq 'play') {
 
@@ -282,20 +330,37 @@
 
 		$log->logBacktrace("Bogus streamstate $streamState");
 	}
+	
+	if ($control == $streamControlCodes{'go'} && $pauseUntil{$client} > $now) {
+		$control = $streamControlCodes{'stop'};
+	}
+	
+	if ( $log->is_debug ) {
+		$log->debug(
+			$client->id() . 
+			" sending stream: seq:$seq, len:$len, wptr:$wptr, state:". 
+			$streamState{$client}.
+			", control:$control, inflight:" . (0 + defined($dataPktInFlight{$client}) + defined($emptyPktInFlight{$client}))
+		);
+	}
 
 	my $measuredlen = length(${$pkt->{'chunkref'}});
 
 	if ($len == $measuredlen && $len < 4097 ) {
 
 		$client->udpstream($control, $wptr, $seq, ${$pkt->{'chunkref'}});
-	
-		if ($log->warn && $packetInFlight{$client}) {
-
-			$log->logBacktrace("Sending packet when we have one in queue!!!!!!"); 
+		$pkt->{'sendTimeStamp'} = $now;
+	
+		if ($log->is_warn && $len && $dataPktInFlight{$client}) {
+			$log->logBacktrace("Sending data packet when we have one in queue!!!!!!"); 
 		};
 		
-		$packetInFlight{$client} = $pkt;
-		$bytesSent{$client} += $len;
+		if ($len) {
+			$dataPktInFlight{$client}  = $pkt;
+			$bytesSent{$client}       += $len;
+		} else {
+			$emptyPktInFlight{$client} = $pkt;
+		}
 
 	} else {
 
@@ -303,41 +368,52 @@
 	}
 
 	# restart the timeout
-	Slim::Utils::Timers::setTimer($client, Time::HiRes::time()+$TIMEOUT, \&timeout, ($seq));
+	Slim::Utils::Timers::setTimer(
+		$client,
+		make_timeout($client, $TIMEOUT, $now),
+		\&timeout,
+		($seq)
+	);
 }
 
 # Retransmit timed out packet
 sub timeout {
-	my $client = shift;
-	my $seq = shift;
+	my ($client, $seq) = @_;
 		
 	Slim::Utils::Timers::killOneTimer($client, \&timeout);
 
-	return unless $packetInFlight{$client};
-
-	my $packet = $packetInFlight{$client};
-
-	$log->warn($client->id, " Timeout on seq: $packet->{'seq'}");
-
-	$packetInFlight{$client} = undef;
-
+	return unless ($dataPktInFlight{$client} || $emptyPktInFlight{$client});
+
+	$log->debug($client->id, " Timeout on seq: $seq");
+	
 	if (($lastAck{$client} + $ACK_TIMEOUT) < Time::HiRes::time()) {
 
 		# we haven't gotten an ack in a long time.  shut it down and don't bother resending.
-		Slim::Player::Sync::unsync($client);
+		Slim::Player::Sync::unsync($client, 1);
+		$dataPktInFlight{$client}  = undef;
+		$emptyPktInFlight{$client} = undef;
 		$client->execute(["stop"]);
 
 	} else {
-		sendStreamPkt($client, $packet);
+		# Resend the packet
+		my $packet;
+		if ($dataPktInFlight{$client}) {
+			$packet = $dataPktInFlight{$client};
+			$dataPktInFlight{$client}  = undef;
+			$emptyPktInFlight{$client} = undef;	# forget about retrying it
+			sendStreamPkt($client, $packet);
+		}
+		else {
+			$packet = $emptyPktInFlight{$client};
+			$emptyPktInFlight{$client} = undef;
+			sendStreamPkt($client, $packet);
+		}
 	}
 }
 
 # receive an ack, then send one or two more packets
 sub gotAck {
-	my ($client, $wptr, $rptr, $seq) = @_;
-	my $pkt;
-	my $pkt2;
-	my $eachpkt;
+	my ($client, $wptr, $rptr, $seq, $msgTimeStamp) = @_;
 
 	if (!defined($streamState{$client})) {
 
@@ -345,90 +421,142 @@
 
 		return;
 	}
-
-	$log->debug($client->id, " gotAck for seq: $seq ack: wptr:$wptr, rptr:$rptr, seq:$seq");
 
 	# calculate buffer usage
 	# todo: optimize usage calculations
-	my $bytesInFlight = 0;
-
-	if ($packetInFlight{$client}) {
-
-		$bytesInFlight += $packetInFlight{$client}->{'len'};
-	}
-
-	my $fullness = $curWptr{$client} - $rptr;  
-
-	if ($fullness < 0) {
-		$fullness += $UNPAUSE_THRESHOLD;
-	} 
-
-	$fullness = $fullness * 2 + $bytesInFlight;
-
+	my $bytesInFlight = $dataPktInFlight{$client} ? $dataPktInFlight{$client}->{'len'} : 0;
+
+	# is this an expected packet?
+	my $packet;
+	if ($dataPktInFlight{$client} && $dataPktInFlight{$client}->{'seq'} == $seq) {
+		$packet = $dataPktInFlight{$client};
+		$dataPktInFlight{$client} = undef;
+	}
+	elsif ($emptyPktInFlight{$client} && $emptyPktInFlight{$client}->{'seq'} == $seq) {
+		$packet = $emptyPktInFlight{$client};
+		$emptyPktInFlight{$client} = undef;
+	}
+
+	my $fullness = (($curWptr{$client} - $rptr + $WPTR_LIMIT) % $WPTR_LIMIT) * 2;
 	$fullness{$client} = $fullness;
 
-	$log->debug("bytesinflight:$bytesInFlight fullness:$fullness{$client}");
-
-	if (!$packetInFlight{$client}) {
-
-		$log->warn("Warning: Missing packet acked: $seq");
-
-	} elsif ($packetInFlight{$client}->{'seq'} != $seq) { 
-
-		$log->warn("Warning: Unexpected packet acked: $seq, was expecting " . $packetInFlight{$client}->{'seq'});
-
+	my $pktLatency = $packet
+		? int(($msgTimeStamp - $packet->{'sendTimeStamp'})*1000000/2) : -1;
+
+	if ( $log->is_debug ) {
+		$log->debug(
+			$client->id() . " gotAck: wptr:$wptr rptr:$rptr seq:$seq " .
+			"inflight:$bytesInFlight fullness:$fullness{$client} latency:$pktLatency us"
+		);
+	}
+
+	if ( !$packet ) {
+		if ( $log->is_debug ) {
+			if ( ($seq{$client} - $seq) % $SEQ_LIMIT > 2) {
+				$log->debug($client->id() . " ***Missing or unexpected packet acked: $seq");
+			}
+		}
+		
 	} else {
-
-		$client->bytesReceived($client->bytesReceived + $packetInFlight{$client}->{'len'});
-
-		$packetInFlight{$client} = undef;
+		# Keep track of effective network delay to this client;
+		# assume packet latency is half round-trip-time; stored in microseconds.
+		# Keep set of recent entries and assume that only values below the median are representative
+		my $latencyList = $latencyList{$client};
+		push(@{$latencyList}, $pktLatency); shift @{$latencyList} if (@{$latencyList} > LATENCY_LIST_SIZELIMIT);
 
 		Slim::Utils::Timers::killOneTimer($client, \&timeout);
 
-		$lastAck{$client} = Time::HiRes::time();
-	}
+		$client->bytesReceived($client->bytesReceived + $packet->{'len'});
+		$lastAck{$client} = $msgTimeStamp;
+
+		# Calculate and publish playPoint
+		#
+		# The following calculations are costly, so only do when necessary, and not too frequently.
+		my $medianLatency;
+		if (   Slim::Player::Sync::isSynced($client)
+			&& ($streamState{$client} eq 'play' || $streamState{$client} eq 'eof')
+			&& $msgTimeStamp > $samplePlayPointAfter{$client}
+			&& defined($medianLatency = getMedianLatencyMicroSeconds($client))
+		) {
+			if ($pktLatency <= $medianLatency) {
+				my $statusTime = $msgTimeStamp - $pktLatency / 1000000;
+				my $apparentStreamStartTime = $client->apparentStreamStartTime($statusTime);
+
+				$client->publishPlayPoint($statusTime, $apparentStreamStartTime, $pauseUntil{$client});
+
+				# only do this again after a short interval
+				$samplePlayPointAfter{$client} = $msgTimeStamp + PLAY_POINT_SAMPLE_INTERVAL;
+			}
+		}
+	}
+	
+	my $state = $streamState{$client};
 
 	if ($fullness <= 512) { 
 
-		$log->warn("Warning: Stream underrun: $fullness");
-
-		Slim::Player::Source::underrun($client);
-
-		if ($streamState{$client} eq 'eof') { 
-
-			$streamState{$client} = 'stop'; 
-		}
-	}
-
-	my $state = $streamState{$client};
-
-	if ($state eq 'stop') {
-
-		# don't bother sending anything.
-
-	} else {
-
-		sendNextChunk($client);
+		if ( $log->is_debug ) {
+			if ( $state eq 'play' || $state eq 'eof' ) {
+				$log->debug("***Stream underrun: $fullness");
+			}
+		}
+		
+		if ( $state eq 'play' ) {
+			Slim::Player::Source::outputUnderrun($client);
+		}
+		elsif ( $state eq 'eof' ) { 
+			Slim::Player::Source::underrun($client);
+			$state = $streamState{$client} = 'stop';
+		}
+	}
+
+	sendNextChunk($client) unless ($state eq 'stop');
+}
+
+sub make_timeout {
+	my ($client, $delta, $now) = @_;
+
+	$now = Time::HiRes::time() unless defined($now);
+
+	my $pauseUntil = $pauseUntil{$client};
+
+	if ($pauseUntil > $now) {
+		my $timeout = $now + $delta;
+		return $timeout > $pauseUntil ? $pauseUntil : $timeout;
+	}
+	else {
+		return $now + $delta;
 	}
 }
 
 # sends the next packet of data in the stream
 sub sendNextChunk {
-	my $client   = shift;
+	my $client   = $_[0];
 
 	my $fullness = $fullness{$client};
 	my $curWptr  = $curWptr{$client};
 
 	Slim::Utils::Timers::killOneTimer($client, \&sendNextChunk);
+	
+	my $streamState = $streamState{$client};
 
 	# if there's a packet in flight, come back later and try again...
-	if ($packetInFlight{$client}) {
-
-		Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + $BUFFER_FULL_DELAY, \&sendNextChunk);
+	if ($dataPktInFlight{$client} || $emptyPktInFlight{$client}) {
+		if ( $log->is_debug ) {
+			$log->debug(
+				$client->id() . "- $streamState - " .
+				($dataPktInFlight{$client} ? "data" : "empty") 
+				. " packet already in flight"
+			);
+		}
+		
+		Slim::Utils::Timers::setTimer(
+			$client, 
+			make_timeout($client, $BUFFER_FULL_DELAY),
+			\&sendNextChunk
+		);
+		
 		return 0;
 	}
-
-	my $streamState = $streamState{$client};
 	
 	if (($streamState eq 'stop')) { 
 
@@ -442,12 +570,13 @@
 
 		$log->debug($client->id, "- $streamState - Buffer full, need to poll to see if there is space");
 
-		# if client's buffer is full, poll it every 50ms until there's room if we're playing
-		# otherwise, we can't send a chunk.
-		if ($streamState eq 'play' || $streamState eq 'eof') {
-
-			Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + $BUFFER_FULL_DELAY, \&sendEmptyChunk);
-		} 
+		# if client's buffer is full, poll it every 50ms until there's room 
+		# Note: already dealt with 'stop' case above; previous test for 'play' || 'eof' may have missed certain race conditions
+		Slim::Utils::Timers::setTimer(
+			$client, 
+			make_timeout($client, $BUFFER_FULL_DELAY),
+			\&sendEmptyChunk
+		);
 
 		return 0;
 	}
@@ -465,23 +594,26 @@
 
 		$requestedChunkSize--;
 	}
+	
+	## TODO - if we are just about to unpause, then send an empty packet rather than waste time
+	# getting another chunk.
 
 	my $chunkRef = Slim::Player::Source::nextChunk($client, $requestedChunkSize);
 	
 	if (!defined($chunkRef)) {
 
-		$log->warn("Stream not readable");
+		0 && $log->warn("Stream not readable");
 
 		if ($streamState eq 'eof') {
 
 			$log->warn("Sending empty chunk...");
 
 			# we're going to poll after BUFFER_FULL_DELAY with an empty chunk so we can know when the player runs out.
-			Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + $BUFFER_FULL_DELAY, \&sendEmptyChunk);
+			Slim::Utils::Timers::setTimer($client, make_timeout($client, $BUFFER_FULL_DELAY), \&sendEmptyChunk);
 
 		} else {
 
-			Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + $TIMEOUT, \&sendNextChunk);
+			Slim::Utils::Timers::setTimer($client, make_timeout($client, $TIMEOUT), \&sendNextChunk);
 		}
 
 		return 0;
@@ -515,9 +647,10 @@
 
 	} elsif (($fullness < $PAUSE_THRESHOLD) && ($streamState eq 'play')) {
 
-		$log->info($client->id, "Buffer drained, pausing playback");
-
-		$streamState{$client}='buffering';
+		$log->info($client->id, " Buffer drained, pausing playback");
+
+		$streamState{$client} = 'buffering';
+		Slim::Player::Source::outputUnderrun($client);
 	}
 
 	my $pkt = {
@@ -526,14 +659,7 @@
 		'chunkref' => $chunkRef,
 	};
 
-	$curWptr = $curWptr + $len/2;
-
-	if ($curWptr >= $UNPAUSE_THRESHOLD) {
-
-		$curWptr -= $UNPAUSE_THRESHOLD;
-	}
-	
-	$curWptr{$client} = $curWptr;
+	$curWptr{$client} = ($curWptr + $len/2) % $WPTR_LIMIT;
 	
 	sendPkt($client, $pkt);
 
@@ -545,7 +671,9 @@
 sub sendEmptyChunk {
 	my $client = shift;
 
-	$log->debug($client->id);
+	$log->debug($client->id, ' sendEmptyChunk');
+	
+	Slim::Utils::Timers::killOneTimer($client, \&sendEmptyChunk);
 
 	my $pkt = {
 		'wptr'     => $curWptr{$client},
@@ -557,8 +685,7 @@
 }
 
 sub sendPkt {
-	my $client = shift;
-	my $pkt    = shift;
+	my ($client, $pkt) = @_;
 
 	my $seq = $seq{$client};
 
@@ -566,16 +693,19 @@
 
 	sendStreamPkt($client, $pkt);
 
-	$seq++;
-
-	if ($seq >= $UNPAUSE_THRESHOLD) {
-
-		$seq -= $UNPAUSE_THRESHOLD;
-	}
-
-	$seq{$client} = $seq;
-}
-
-1;
-
-__END__
+	$seq{$client} = ($seq + 1) % $SEQ_LIMIT;
+}
+
+sub getMedianLatencyMicroSeconds {
+	my $client      = $_[0];
+	my $latencyList = $latencyList{$client};
+	
+	if ( @{$latencyList} > LATENCY_LIST_MINSIZE ) {
+		return (sort {$a <=> $b} @{$latencyList})[ int(@{$latencyList} / 2) ];
+	}
+	else {
+		return;
+	}
+}
+
+1;

Modified: trunk/server/Slim/Networking/Slimproto.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Networking/Slimproto.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Networking/Slimproto.pm (original)
+++ trunk/server/Slim/Networking/Slimproto.pm Thu Aug 30 21:08:54 2007
@@ -16,6 +16,7 @@
 use FileHandle;
 use Sys::Hostname;
 use File::Spec::Functions qw(:ALL);
+use Math::VecStat;
 use Scalar::Util qw(blessed);
 
 use Slim::Networking::Select;
@@ -29,7 +30,9 @@
 use Slim::Utils::Strings qw(string);
 use Slim::Utils::Prefs;
 
-use constant SLIMPROTO_PORT => 3483;
+use constant SLIMPROTO_PORT   => 3483;
+use constant LATENCY_LIST_MAX => 10;
+use constant LATENCY_LIST_MIN => 6;
 
 my @deviceids = (undef, undef, 'squeezebox', 'softsqueeze','squeezebox2','transporter', 'softsqueeze3');
 my $log       = logger('network.protocol.slimproto');
@@ -49,6 +52,8 @@
 our %sock2client;	     # reference to client for each sonnected sock
 our %heartbeat;          # the last time we heard from a client
 our %status;
+our %latencyList;        # last few latencies
+our %latency;            # current published latency
 
 our %callbacks;
 our %callbacksRAWI;
@@ -211,11 +216,9 @@
 			slimproto_close( $client->tcpsock );
 			next;
 		}
-
-		# force a status request if we haven't heard from the player in a short while
-		if ( $last_heard >= $check_all_clients_time / 2 ) {
-			$client->requestStatus();
-		}
+		
+		# Always ask for status requests so we can use the result for latency tracking
+		$client->requestStatus();
 	}
 
 	$check_time = $now + $check_all_clients_time;
@@ -498,6 +501,8 @@
 	}
 
 	my ($irTime, $irCode) = unpack('NxxH8', $$data_ref);
+	
+	$client->trackJiffiesEpoch($irTime, Time::HiRes::time());
 
 	Slim::Hardware::IR::enqueue($client, $irCode, $irTime);
 
@@ -576,8 +581,10 @@
 	my $client = shift;
 	my $data_ref = shift;
 	
+	my $now = Time::HiRes::time();
+	
 	# update the heartbeat value for this player
-	$heartbeat{ $client->id } = time();
+	$heartbeat{ $client->id } = $now;
 
 	#struct status_struct {
 	#        u32_t event;
@@ -593,6 +600,8 @@
 	#        u32_t output_buffer_fullness;
 	#        u32_t elapsed_seconds;
 	#        u16_t voltage;
+	#        u32_t elapsed_milliseconds;
+	#        u32_t server_timestamp;
 	#
 	
 	# event types:
@@ -628,8 +637,29 @@
 		$status{$client}->{'output_buffer_fullness'},
 		$status{$client}->{'elapsed_seconds'},
 		$status{$client}->{'voltage'},
-
-	) = unpack ('a4CCCNNNNnNNNNn', $$data_ref);
+		$status{$client}->{'elapsed_milliseconds'},
+		$status{$client}->{'server_timestamp'},
+
+	) = unpack ('a4CCCNNNNnNNNNnNN', $$data_ref);
+	
+	# Track latency if we have a server timestamp
+	if ( $status{$client}->{'server_timestamp'} ) {
+		my $latency = (int($now * 1000 % 0xffffffff) - $status{$client}->{'server_timestamp'}) / 2;
+	
+		push (@{$latencyList{$client}}, $latency) if ($latency >= 0 && $latency < 1000);
+		shift(@{$latencyList{$client}}) if (@{$latencyList{$client}} > LATENCY_LIST_MAX);
+
+		$latency{$client} = Math::VecStat::min($latencyList{$client}) if (@{$latencyList{$client}} >= LATENCY_LIST_MIN);
+		
+		if ( $log->is_debug ) {
+			$log->debug(
+				$client->id() . " latency=$latency{$client}, from ("
+				. join(', ', @{$latencyList{$client}}) . ')'
+			);
+		}
+	}	
+		
+	$client->trackJiffiesEpoch($status{$client}->{'jiffies'}, $now);
 
 	$status{$client}->{'bytes_received'} = $status{$client}->{'bytes_received_H'} * 2**32 + $status{$client}->{'bytes_received_L'}; 
 
@@ -687,7 +717,8 @@
 
 		if (defined($status{$client}->{'output_buffer_size'})) {
 
-			my $msg = join("\n", 
+			my $msg = join("\n",
+				"",
 				"\toutput size:     $status{$client}->{'output_buffer_size'}",
 				"\toutput fullness: $status{$client}->{'output_buffer_fullness'}",
 				"\telapsed seconds: $status{$client}->{'elapsed_seconds'}",
@@ -696,6 +727,28 @@
 
 			$log->debug($msg);
 		}
+		
+		if (defined($status{$client}->{'elapsed_milliseconds'})) {
+			
+			my $msg = join("\n",
+				"",
+				"\telapsed milliseconds: $status{$client}->{'elapsed_milliseconds'}",
+				"\tserver timestamp:     $status{$client}->{'server_timestamp'}",
+				"",
+			);
+			
+			$log->debug($msg);
+		}
+	}
+	
+	if (   $client->model() eq 'squeezebox'
+		&& Slim::Player::Sync::isSynced($client)
+		&& Slim::Player::Source::playmode($client) eq 'play')
+	{
+		my $statusTime = $client->jiffiesToTimestamp( $status{$client}->{'jiffies'} );
+		if ( my $apparentStreamStartTime = $client->apparentStreamStartTime($statusTime) ) {
+			$client->publishPlayPoint( $statusTime, $apparentStreamStartTime, undef );
+		}
 	}
 
 	Slim::Player::Sync::checkSync($client);
@@ -703,6 +756,15 @@
 	my $callback = $callbacks{$status{$client}->{'event_code'}};
 
 	&$callback($client) if $callback;
+}
+
+sub getLatency {
+	return $latency{shift};
+}
+
+sub getPlayPointData {
+	my $client = shift;
+	return ($status{$client}->{'jiffies'}, $status{$client}->{'elapsed_milliseconds'});
 }
 	
 sub _update_request_handler {
@@ -936,6 +998,8 @@
 	}
 
 	$sock2client{$s} = $client;
+	
+	$latencyList{$client} = [];
 
 	if ($client->needsUpgrade()) {
 
@@ -975,7 +1039,7 @@
 		$client->volume($client->volume(), defined($client->tempVolume()));
 			
 		# add the player to the list of clients we're watching for signs of life
-		$heartbeat{ $client->id } = time();
+		$heartbeat{ $client->id } = Time::HiRes::time();
 	}
 }
 
@@ -985,6 +1049,8 @@
 
 	# handle hard buttons
 	my ($time, $button) = unpack( 'NH8', $$data_ref);
+	
+	$client->trackJiffiesEpoch($time, Time::HiRes::time());
 
 	Slim::Hardware::IR::enqueue($client, $button, $time);
 
@@ -997,6 +1063,8 @@
 
 	# handle knob movement
 	my ($time, $position, $sync) = unpack('NNC', $$data_ref);
+	
+	$client->trackJiffiesEpoch($time, Time::HiRes::time());
 
 	# Perl doesn't have an unsigned network long format.
 	if ($position & 1<<31) {

Modified: trunk/server/Slim/Networking/UDP.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Networking/UDP.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Networking/UDP.pm (original)
+++ trunk/server/Slim/Networking/UDP.pm Thu Aug 30 21:08:54 2007
@@ -71,6 +71,8 @@
 	my $msg = '';
 
 	do {
+		my $ts = Time::HiRes::time();
+		
 		$clientpaddr = recv($sock, $msg, 1500, 0);
 		
 		if ($clientpaddr) {
@@ -85,8 +87,8 @@
 				}
 
 				my $client = Slim::Networking::SliMP3::Protocol::getUdpClient($clientpaddr, $sock, $msg) || return;
-	
-				Slim::Networking::SliMP3::Protocol::processMessage($client, $msg);
+				
+				Slim::Networking::SliMP3::Protocol::processMessage($client, $msg, $ts);
 	
 			} elsif ($msg =~/^d/) {
 

Modified: trunk/server/Slim/Player/Client.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Client.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Client.pm (original)
+++ trunk/server/Slim/Player/Client.pm Thu Aug 30 21:08:54 2007
@@ -133,7 +133,7 @@
 #	$client->[33]
 #	$client->[34]
 
-	$client->[35] = 0; # outputBufferFullness
+	$client->[35] = undef; # outputBufferFullness
 	$client->[36] = undef; # irRefTime
 	$client->[37] = 0; # bytesReceived
 	$client->[38] = ''; # currentplayingsong
@@ -225,6 +225,15 @@
 	$client->[120] = {}; # pluginData, plugin-specific state data
 	$client->[121] = 1; # readNextChunkOk (flag used when we are waiting for an async response in readNextChunk)
 
+	# Sync data
+	$client->[122] = undef; # initialStreamBuffer, cache of initially-streamed data to calculate rate
+	$client->[123] = undef; # playPoint, (timeStamp, apparentStartTime) tuple;
+	$client->[124] = undef; # playPoints, set of (timeStamp, apparentStartTime) tuples to determine consistency;
+
+	$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
+
 	$clientHash{$id} = $client;
 
 	Slim::Control::Request::notifyFromArray($client, ['client', 'new']);
@@ -1603,6 +1612,8 @@
 sub pluginData {
 	my ( $client, $key, $value ) = @_;
 	
+	$client = Slim::Player::Sync::masterOrSelf($client);
+	
 	my $namespace;
 	
 	# if called from a plugin, we automatically use the plugin's namespace for keys
@@ -1638,4 +1649,40 @@
 	@_ ? ($r->[121] = shift) : $r->[121];
 }
 
+sub initialStreamBuffer {
+	my $r = shift;
+	@_ ? ($r->[122] = shift) : $r->[122];
+}
+
+sub playPoint {
+	my $r = shift;
+	if (@_) {
+		$r->[123] = my $new = shift;
+		playPoints($r, undef) if (!defined($new));
+		return $new;
+	} else {
+		return $r->[123];
+	}
+}
+
+sub playPoints {
+	my $r = shift;
+	@_ ? ($r->[124] = shift) : $r->[124];
+}
+
+sub jiffiesEpoch {
+	my $r = shift;
+	@_ ? ($r->[125] = shift) : $r->[125];
+}
+
+sub jiffiesOffsetList {
+	my $r = shift;
+	@_ ? ($r->[126] = shift) : $r->[126];
+}
+
+sub frameData {
+	my $r = shift;
+	@_ ? ($r->[127] = shift) : $r->[127];
+}
+
 1;

Modified: trunk/server/Slim/Player/Player.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Player.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Player.pm (original)
+++ trunk/server/Slim/Player/Player.pm Thu Aug 30 21:08:54 2007
@@ -15,6 +15,7 @@
 
 use strict;
 
+use Math::VecStat;
 use Scalar::Util qw(blessed);
 
 use base qw(Slim::Player::Client);
@@ -65,6 +66,9 @@
 	'syncBufferThreshold'  => 128,
 	'bufferThreshold'      => 255,
 	'powerOnResume'        => 'PauseOff-NoneOn',
+	'maintainSync'         => 1,
+	'minSyncAdjust'        => 0.030,
+	'packetLatency'        => 0.002,	
 };
 
 sub new {
@@ -180,7 +184,9 @@
 
 		if (defined $sync && $sync == 0) {
 
-			logger('player.sync')->info("Temporary Unsync " . $client->id);
+			if ( Slim::Player::Sync::isSynced($client) ) {
+				logger('player.sync')->info("Temporary Unsync " . $client->id);
+			}
 
 			Slim::Player::Sync::unsync($client, 1);
   		}
@@ -698,6 +704,150 @@
 	}
 }
 
+# Intended to be overridden by sub-classes who know better
+sub packetLatency {
+	return $prefs->client(shift)->get('packetLatency');
+}
+
+use constant JIFFIES_OFFSET_TRACKING_LIST_SIZE => 10;
+use constant JIFFIES_EPOCH_MIN_ADJUST          => 0.001;
+use constant JIFFIES_EPOCH_MAX_ADJUST          => 0.005;
+
+sub trackJiffiesEpoch {
+	my ($client, $jiffies, $timestamp) = @_;
+
+	# Note: we do not take the packet latency into account here;
+	# see jiffiesToTimestamp
+
+	my $jiffiesTime = $jiffies / $client->ticspersec;
+	my $offset      = $timestamp - $jiffiesTime;
+	my $epoch       = $client->jiffiesEpoch || 0;
+
+	logger('network.protocol')->debug($client->id() . " trackJiffiesEpoch: epoch=$epoch, offset=$offset");
+
+	if (   $offset < $epoch			# simply a better estimate, or
+		|| $offset - $epoch > 50	# we have had wrap-around (or first time)
+	) {
+		if ( logger('player.sync')->is_debug ) {
+			if ( abs($offset - $epoch) > 0.001 ) {
+				logger('player.sync')->debug( sprintf("%s adjust jiffies epoch %+.3fs", $client->id(), $offset - $epoch) );
+			}
+		}
+		
+		$client->jiffiesEpoch($epoch = $offset);	
+	}
+
+	my $diff = $offset - $epoch;
+	my $jiffiesOffsetList = $client->jiffiesOffsetList();
+
+	unshift @{$jiffiesOffsetList}, $diff;
+	pop @{$jiffiesOffsetList}
+		if (@{$jiffiesOffsetList} > JIFFIES_OFFSET_TRACKING_LIST_SIZE);
+
+	if (   $diff > 0.001
+		&& (@{$jiffiesOffsetList} == JIFFIES_OFFSET_TRACKING_LIST_SIZE)
+	) {
+		my $min_diff = Math::VecStat::min($jiffiesOffsetList);
+		if ( $min_diff > JIFFIES_EPOCH_MIN_ADJUST ) {
+			if ( $min_diff > JIFFIES_EPOCH_MAX_ADJUST ) {
+				$min_diff = JIFFIES_EPOCH_MAX_ADJUST;
+			}
+			logger('player.sync')->debug( sprintf("%s adjust jiffies epoch +%.3fs", $client->id(), $min_diff) );
+			$client->jiffiesEpoch($epoch += $min_diff);
+			$diff -= $min_diff;
+			@{$jiffiesOffsetList} = ();	# start tracking again
+		}
+	}
+	return $diff;
+}
+
+sub jiffiesToTimestamp {
+	my ($client, $jiffies) = @_;
+
+	# Note: we only take the packet latency into account here,
+	# rather than in trackJiffiesEpoch(), so that a bad calculated latency
+	# (which presumably would be transient) does not permanently effect
+	# our idea of the jiffies-epoch.
+	
+	return $client->jiffiesEpoch + $jiffies / $client->ticspersec - $client->packetLatency();
+}
+	
+# Only works for SliMP3s and (maybe) SB1s
+sub apparentStreamStartTime {
+	my ($client, $statusTime) = @_;
+
+	my $bytesPlayed = $client->bytesReceived()
+						- $client->bufferFullness()
+						- ($client->model() eq 'slimp3' ? 2000 : 2048);
+
+	my $format = Slim::Player::Sync::masterOrSelf($client)->streamformat();
+
+	my $timePlayed;
+
+	if ( $format eq 'mp3' ) {
+		$timePlayed = Slim::Player::Source::findTimeForOffset($client, $bytesPlayed) or return;
+	}
+	elsif ( $format eq 'wav' ) {
+		$timePlayed = $bytesPlayed * 8 / (streamBitrate($client) or return);
+	}
+	else {
+		return;
+	}
+
+	my $apparentStreamStartTime = $statusTime - $timePlayed;
+
+	if ( logger('player.sync')->is_debug ) {
+		logger('player.sync')->debug(
+			$client->id()
+			. " apparentStreamStartTime: $apparentStreamStartTime @ $statusTime \n"
+			. "timePlayed:$timePlayed (bytesReceived:" . $client->bytesReceived()
+			. " bufferFullness:" . $client->bufferFullness()
+			.")"
+		);
+	}
+
+	return $apparentStreamStartTime;
+}
+
+use constant PLAY_POINT_LIST_SIZE		=> 8;		# how many to keep
+use constant MAX_STARTTIME_VARIATION	=> 0.015;	# latest apparent-stream-start-time estimate
+													# must be this close to the average
+sub publishPlayPoint {
+	my ( $client, $statusTime, $apparentStreamStartTime, $cutoffTime ) = @_;
+
+	my $playPoints = $client->playPoints();
+	$client->playPoints($playPoints = []) if (!defined($playPoints));
+	
+	unshift(@{$playPoints}, [$statusTime, $apparentStreamStartTime]);
+
+	# remove all old and excessive play-points
+	pop @{$playPoints} if ( @{$playPoints} > PLAY_POINT_LIST_SIZE );
+	while( @{$playPoints} && $playPoints->[-1][0] < $cutoffTime ) {
+		pop @{$playPoints};
+	}
+
+	# Do we have a consistent set of playPoints so that we can publish one?
+	if ( @{$playPoints} == PLAY_POINT_LIST_SIZE ) {
+		my $meanStartTime = 0;
+		foreach my $point ( @{$playPoints} ) {
+			$meanStartTime += $point->[1];
+		}
+		$meanStartTime /= @{$playPoints};
+
+		if ( abs($apparentStreamStartTime - $meanStartTime) < MAX_STARTTIME_VARIATION ) {
+			# Ok, good enough, publish it!
+			$client->playPoint( [$statusTime, $meanStartTime] );
+			
+			if ( 0 && logger('player.sync')->is_debug ) {
+				logger('player.sync')->debug(
+					$client->id()
+					. " publishPlayPoint: $meanStartTime @ $statusTime"
+				);
+			}
+		}
+	}
+}
+
 1;
 
 __END__

Modified: trunk/server/Slim/Player/Protocols/HTTP.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Protocols/HTTP.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Protocols/HTTP.pm (original)
+++ trunk/server/Slim/Player/Protocols/HTTP.pm Thu Aug 30 21:08:54 2007
@@ -60,12 +60,12 @@
 
 			if ($! ne "Unknown error" && $! != EWOULDBLOCK) {
 
-			 	$log->warn("Warning: Metadata byte not read! $!");
+			 	#$log->warn("Warning: Metadata byte not read! $!");
 			 	return;
 
 			 } else {
 
-				$log->debug("Metadata byte not read, trying again: $!");  
+				#$log->debug("Metadata byte not read, trying again: $!");  
 			 }
 		}
 
@@ -87,12 +87,12 @@
 			if ($!) {
 				if ($! ne "Unknown error" && $! != EWOULDBLOCK) {
 
-					$log->info("Metadata bytes not read! $!");
+					#$log->info("Metadata bytes not read! $!");
 					return;
 
 				} else {
 
-					$log->info("Metadata bytes not read, trying again: $!");
+					#$log->info("Metadata bytes not read, trying again: $!");
 				}
 			}
 
@@ -216,7 +216,7 @@
 	# stream for all players
 	if ( Slim::Player::Sync::isSynced($client) ) {
 
-		logger('player.streaming')->info(sprintf(
+		logger('player.streaming.direct')->info(sprintf(
 			"[%s] Not direct streaming because player is synced", $client->id
 		));
 
@@ -226,7 +226,7 @@
 	# Allow user pref to select the method for streaming
 	if ( my $method = preferences('server')->client($client)->get('mp3StreamingMethod') ) {
 		if ( $method == 1 ) {
-			logger('player.streaming')->debug("Not direct streaming because of mp3StreamingMethod pref");
+			logger('player.streaming.direct')->debug("Not direct streaming because of mp3StreamingMethod pref");
 			return 0;
 		}
 	}

Modified: trunk/server/Slim/Player/SLIMP3.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/SLIMP3.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/SLIMP3.pm (original)
+++ trunk/server/Slim/Player/SLIMP3.pm Thu Aug 30 21:08:54 2007
@@ -14,10 +14,17 @@
 use Slim::Player::Player;
 use Slim::Utils::Log;
 use Slim::Utils::Misc;
+use Slim::Utils::Prefs;
 
 use base qw(Slim::Player::Player);
 
+my $prefs = preferences('server');
+
 our $SLIMP3Connected = 0;
+
+our $defaultPrefs = {
+	syncBufferThreshold => 15000,
+};
 
 sub new {
 	my $class    = shift;
@@ -56,6 +63,8 @@
 sub init {
 	my $client = shift;
 
+	$prefs->client($client)->init($defaultPrefs);
+
 	$client->SUPER::init();
 
 	$client->periodicScreenRefresh(); 
@@ -128,6 +137,25 @@
 	return 1;
 }
 
+sub startAt {
+	my ($client, $at) = @_;
+
+	# make sure volume is set, without changing temp setting
+	$client->volume($client->volume(), defined($client->tempVolume()));
+
+	Slim::Networking::SliMP3::Stream::unpause($client, $at - $client->packetLatency());
+	return 1;
+}
+
+sub packetLatency {
+	my $client = shift;
+	return (
+		Slim::Networking::SliMP3::Stream::getMedianLatencyMicroSeconds($client) / 1000000
+		||
+		$client->SUPER::packetLatency()
+	);
+}
+
 #
 # pause
 #
@@ -139,6 +167,16 @@
 	$client->SUPER::pause();
 
 	return 1;
+}
+
+#
+# pauseForInterval
+#
+sub pauseForInterval {
+	my $client   = shift;
+	my $interval = shift;
+
+	return Slim::Networking::SliMP3::Stream::pause($client, $interval);
 }
 
 #
@@ -218,7 +256,9 @@
 		# I'm sure there's something optimal, but this is better.
 	
 		my $level = sprintf('%05X', 0x80000 * (($volume / $client->maxVolume) ** 2));
-	
+		
+		logger('network.protocol.slimp3')->debug($client->id() . " volume: newvolume=$newvolume volume=$volume level=$level");
+		
 		$client->i2c(
 			 Slim::Hardware::mas3507d::masWrite('ll', $level)
 			.Slim::Hardware::mas3507d::masWrite('rr', $level)

Modified: trunk/server/Slim/Player/Source.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Source.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Source.pm (original)
+++ trunk/server/Slim/Player/Source.pm Thu Aug 30 21:08:54 2007
@@ -16,6 +16,8 @@
 use FileHandle;
 use FindBin qw($Bin);
 use IO::Socket qw(:DEFAULT :crlf);
+use MP3::Info;
+use MPEG::Audio::Frame;
 use Scalar::Util qw(blessed);
 use Time::HiRes;
 
@@ -194,9 +196,10 @@
 
 		my $outputBufferFullness = $client->outputBufferFullness();
 		if (defined($outputBufferFullness)) {
-			# Assume 44.1KHz output sample rate. This will be slightly
+			# Default 44.1KHz output sample rate. This will be slightly
 			# off for anything that's 48Khz, but it's a guesstimate anyway.
-			$outputBufferSeconds = (($outputBufferFullness / (44100 * 8)) * $rate);
+			$outputBufferSeconds = $outputBufferFullness /
+				(($song->{'samplerate'} || 44100) * 8) * $rate;
 		}
 	}
 	# If we're moving forward and have started streaming the next
@@ -210,7 +213,7 @@
 
 	if ($realpos < 0) {
 
-		$log->info("Negative position calculated, we are still playing out the previous song.");
+		$log->info($client->id, " Negative position calculated, we are still playing out the previous song.");
 		$log->info("Realpos $realpos calcuated from bytes received: " . 
 			$client->bytesReceived .  " minus buffer fullness: " . $client->bufferFullness);
 
@@ -609,8 +612,8 @@
 			pop @{$queue};
 		}
 		
-		# If the track that failed was the final one, stop
-		if ( noMoreValidTracks($client) ) {
+		# If the track that failed was the final one, stop, unless we're in repeat mode
+		if ( noMoreValidTracks($client) && !Slim::Player::Playlist::repeat($client) ) {
 			playmode( $client, 'stop' );
 		}
 	}
@@ -898,9 +901,9 @@
 	} else {
 
 		#otherwise, read a new chunk
-		my $readfrom = Slim::Player::Sync::masterOrSelf($client);
-
-		$chunk = readNextChunk($readfrom, $maxChunkSize);
+		my $master = Slim::Player::Sync::masterOrSelf($client);
+
+		$chunk = readNextChunk($master, $maxChunkSize);
 
 		if (defined($chunk)) {
 
@@ -912,6 +915,35 @@
 				foreach my $buddy (Slim::Player::Sync::syncedWith($client)) {
 
 					push @{$buddy->chunks}, $chunk;
+				}
+				
+				# And save the data for analysis, if we are synced.
+				# Only really need to do this if we have any SliMP3s or SB1s in the
+				# sync group.
+				if (Slim::Player::Sync::isMaster($master)) {
+					if (my $buf = $master->initialStreamBuffer()) {
+						$$buf .= $$chunk;
+
+						# Safety check - just make sure that we are not in the process
+						# of slurping up a perhaps-infinite stream without using it.
+						# XXX: we should give up way before we have 30MB in memory!! -andy
+						if (length($$buf) > 30000000 ||
+							defined($master->frameData) && @{$master->frameData} > 150000)
+						{
+							resetFrameData($master);
+						}
+					} elsif ($master->streamformat() eq 'mp3' && $master->streamBytes() <= $len) {
+						# do we need to save frame data?
+						my $needFrameData = 0;
+						foreach ($master, Slim::Player::Sync::slaves($master)) {
+							my $model = $_->model();
+							last if $needFrameData = ($model eq 'slimp3' || $model eq 'squeezebox');
+						}
+						if ($needFrameData) {		
+							my $savedChunk = $$chunk; 	# copy
+							$master->initialStreamBuffer(\$savedChunk);
+						}
+					}
 				}
 			}
 		}
@@ -1049,6 +1081,7 @@
 	$client->songStartStreamTime($newtime);
 	$client->bytesReceivedOffset(0);
 	$client->trickSegmentRemaining(0);
+	resetFrameData($client);
 
 	$client->audioFilehandle()->sysseek($newoffset + $dataoffset, 0);
 
@@ -1803,8 +1836,9 @@
 	
 		my $filepath = $track->path;
 
-		my ($size, $duration, $offset, $samplerate, $blockalign, $endian, $drm) = (0, 0, 0, 0, 0, undef, undef);
-		
+		my ($size, $duration, $offset, $bitrate, $samplerate, $samplesize, $channels, $blockalign, $endian, $drm) =
+			(0, 0, 0, undef, 0, 0, 0, 0, undef, undef);
+			
 		# don't try and read this if we're a pipe
 		if (!-p $filepath) {
 
@@ -1812,7 +1846,9 @@
 			$size       = $track->audio_size() || -s $filepath;
 			$duration   = $track->durationSeconds();
 			$offset     = $track->audio_offset() || 0 + $seekoffset;
-			$samplerate = $track->samplerate();
+			$samplerate = $track->samplerate() || 0;
+			$samplesize = $track->samplesize() || 0;
+			$channels   = $track->channels() || 0;
 			$blockalign = $track->block_alignment() || 1;
 			$endian     = $track->endian() || '';
 			$drm        = $track->drm();
@@ -1859,6 +1895,7 @@
 
 		# this case is when we play the file through as-is
 		if ($command eq '-') {
+			$bitrate = $track->bitrate();
 
 			# hack for little-endian aiff.
 			if ($format eq 'aif' && defined($endian) && !$endian) {
@@ -1884,33 +1921,58 @@
 					$offset -= $seekoffset;
 				}
 				
-				if ($format eq 'mp3' && $log->is_debug) {
+				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
-					my $io = IO::String->new( \$frame->asbin );
+					if ( $frame ) {
+						my $io = IO::String->new( \$frame->asbin );
 					
-					if ( my $info = MP3::Info::get_mp3info($io) ) {
-						if ( $info->{LAME} ) {
-
-							$log->info("MP3 file was encoded with $info->{'LAME'}->{'encoder_version'}");
+						if ( my $info = MP3::Info::get_mp3info($io) ) {
 							
-							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},
-								));
+							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");
+								}
 							}
-							else {
-								$log->info("MP3 doesn't contain encoder delay information, 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;
 							}
-						}
-						else {
-							$log->info("MP3 wasn't encoded with LAME, won't play back gapless");
+							
+							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;
 						}
 					}
 				}
@@ -1958,9 +2020,19 @@
 			
 			# XXX: This will reset size and thus $song->{totalbytes} to 0
 			# if not using bitrate limiting, is this what we want?? -andy
-			$size   = $duration * ($maxRate * 1000) / 8;
-			$offset = 0;
-		}
+			$bitrate = $maxRate * 1000;
+			$size    = $duration * $bitrate / 8;
+			$offset  = 0;
+		}
+		
+		if ( $bitrate && $duration && $size && abs($bitrate / 8 * $duration - $size) / $size > 0.05 ) {
+			warn "openSong: bitrate $bitrate does not concur with duration $duration and size $size";
+		}
+
+		$song->{'bitrate'}    = $bitrate if ($bitrate);
+		$song->{'samplerate'} = $samplerate if ($samplerate);
+		$song->{'samplesize'} = $samplesize if ($samplesize);
+		$song->{'channels'}   = $channels if ($channels);
 
 		$song->{'totalbytes'} = $size;
 		$song->{'duration'}   = $duration;
@@ -2076,7 +2148,7 @@
 			}
 		}
 		
-		$log->debug("We need to send $silence seconds of silence...");
+		0 && $log->debug("We need to send $silence seconds of silence...");
 		
 		while ($silence > 0) {
 			$chunk .=  ${Slim::Web::HTTP::getStaticContent("html/lbrsilence.mp3")};
@@ -2212,7 +2284,7 @@
 
 				if ($! == EWOULDBLOCK) {
 
-					$log->debug("Would have blocked, will try again later.");
+					#$log->debug("Would have blocked, will try again later.");
 
 					return undef;	
 
@@ -2370,6 +2442,161 @@
 	}
 }
 
-1;
-
-__END__
+use constant FRAME_BYTE_OFFSET => 0;
+use constant FRAME_TIME_OFFSET => 1;
+
+sub streamBitrate {
+	my $client = Slim::Player::Sync::masterOrSelf($_[0]);
+
+	# Only do this for sync play, although there is no real reason not to do it otherwise
+	# Need to change this if we allow clients to join in mid song.
+	if( !Slim::Player::Sync::isSynced($client) ) {
+		return 0;
+	}
+
+	my $song = streamingSong($client);
+
+	# already know the answer
+	my $rate = $song->{'bitrate'};
+	if ( defined $rate ) {
+		return $rate;
+	}
+
+	my $format = $client->streamformat();
+	
+	if ( $format eq 'mp3' ) {
+		my $frames = $client->frameData();
+		if ( @{$frames} > 1 ) {
+			$rate = $frames->[-1][FRAME_BYTE_OFFSET] / $frames->[-1][FRAME_TIME_OFFSET] * 8;
+		}
+	}
+	elsif ( $format eq 'wav' ) {
+		# assume 44.1k, 16-bit, stereo
+		$rate = ($song->{'samplerate'} || 44100) * ($song->{'samplesize'} || 16) 
+				* ($song->{'channels'} || 2);
+		$song->{'bitrate'} = $rate; # save for later
+	}
+
+	return $rate;
+}
+
+
+sub resetFrameData {
+	my ($client) = @_;
+	return unless Slim::Player::Sync::isMaster($client);
+
+	$client->initialStreamBuffer(undef);
+	$client->frameData(undef);
+}
+	
+sub purgeOldFrames {
+	my $frames     = $_[0]->frameData() or return;
+	my $timeOffset = $_[1];
+
+	my ($i, $j, $k) = (0, @{$frames} - 1);
+
+	# sanity checks
+	return if $timeOffset < $frames->[$i][FRAME_TIME_OFFSET];
+	if ( $timeOffset > $frames->[$j][FRAME_TIME_OFFSET] ) {
+		$log->debug("purgeOldFrames: timeOffset $timeOffset beyond last entry: $frames->[$j][FRAME_TIME_OFFSET]");
+		return;
+	}
+
+	# weighted binary chop
+	while ( ($j - $i) > 1 ) {
+		$k = int ( ($i + $j) / 2 );
+		# $k = $i + (int(($timeOffset - $frames->[$i][FRAME_TIME_OFFSET]) / ($frames->[$j][FRAME_TIME_OFFSET] - $frames->[$i][FRAME_TIME_OFFSET]) * ($j - $i)) || 1);
+		if ( $timeOffset < $frames->[$k][FRAME_TIME_OFFSET] ) {
+			$j = $k;
+		}
+		else {
+			$i = $k;
+		}
+	}
+	
+	if ( $log->is_debug ) {
+		$log->debug(
+			"purgeOldFrames: timeOffset $timeOffset; removing "
+			. ($j+1) . " frames from total " . scalar(@{$frames}) 
+		);
+	}
+	
+	splice @{$frames}, 0, $j+1;	
+}
+
+sub findTimeForOffset {
+	my $client     = Slim::Player::Sync::masterOrSelf($_[0]);
+	my $byteOffset = $_[1];
+	my $buffer     = $client->initialStreamBuffer() or return;
+	my $frames     = $client->frameData();
+
+	return unless $byteOffset;
+
+	# check if there are any frames to analyse
+	if ( length($$buffer) > 400 ) { # make it worth our while
+	
+		my $pos = 0;
+
+		while ( my ($length, $nextPos, $seconds) = MPEG::Audio::Frame->read($buffer, $pos) ) {
+			last unless ($length);
+			# Note: $length may not equal ($nextPos - $pos) if tag data has been skipped
+			if ( !defined($frames) ) {
+				$client->frameData( $frames = [[$nextPos - $length, 0]] );
+				push @{$frames}, [$nextPos, $seconds];
+			}
+			else {
+				my $off = $frames->[-1][FRAME_BYTE_OFFSET] + $nextPos - $pos;
+				my $tim = $frames->[-1][FRAME_TIME_OFFSET] + $seconds;
+				push @{$frames}, [$off, $tim];
+			}
+			$pos = $nextPos;
+
+			if ( $log->is_debug ) {
+				$log->debug("recordFrameOffset: $frames->[-1][FRAME_BYTE_OFFSET] -> $frames->[-1][FRAME_TIME_OFFSET]");
+			}
+		}
+
+		if ($pos) {
+			my $newBuffer = substr $$buffer, $pos;
+			$client->initialStreamBuffer(\$newBuffer);
+		}
+	}
+
+	return unless ( defined @{$frames} && @{$frames} > 1 );
+
+	my ($i, $j, $k) = (0, @{$frames} - 1);
+
+	# sanity check
+	unless ($byteOffset - $frames->[$i][FRAME_BYTE_OFFSET] <= $byteOffset && $byteOffset <= $frames->[$j][FRAME_BYTE_OFFSET]) {
+		$log->debug("findTimeForOffset: byteOffset $byteOffset outside frame range: $frames->[$i][FRAME_BYTE_OFFSET] .. $frames->[$j][FRAME_BYTE_OFFSET]");
+		return;
+	}
+
+	# weighted binary chop
+	while ( ($j - $i) > 1 ) {
+		$k = int ( ($i + $j) / 2 );
+		use integer;
+		# $k = $i + (int(($j - $i) * ($byteOffset - $frames->[$i][FRAME_BYTE_OFFSET]) / ($frames->[$j][FRAME_BYTE_OFFSET] - $frames->[$i][FRAME_BYTE_OFFSET])) || 1);
+		if ( $byteOffset < $frames->[$k][FRAME_BYTE_OFFSET] ) {
+			$j = $k;
+		}
+		else {
+			$i = $k;
+		}
+	}
+	
+	my $frameByteOffset = $frames->[$i][FRAME_BYTE_OFFSET];
+	my $timeOffset = $frames->[$i][FRAME_TIME_OFFSET];
+	if ( $byteOffset > $frameByteOffset && @{$frames} - 1 > $i ) {
+		# interpolate within a frame
+		$timeOffset += ($byteOffset - $frameByteOffset) /
+			  ($frames->[$i+1][FRAME_BYTE_OFFSET] - $frameByteOffset)
+			* ($frames->[$i+1][FRAME_TIME_OFFSET] - $timeOffset);
+	}
+
+	$log->debug("findTimeForOffset: $byteOffset -> $timeOffset");
+
+	return $timeOffset;
+}
+
+1;

Modified: trunk/server/Slim/Player/Squeezebox.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Squeezebox.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Squeezebox.pm (original)
+++ trunk/server/Slim/Player/Squeezebox.pm Thu Aug 30 21:08:54 2007
@@ -205,6 +205,18 @@
 	return 1;
 }
 
+sub startAt {
+	my ($client, $at) = @_;
+
+	Slim::Utils::Timers::killTimers($client, \&buffering);
+	Slim::Utils::Timers::setHighTimer(
+			$client,
+			$at - $client->packetLatency(),
+			\&_unpauseAfterInterval
+		);
+	return 1;
+}
+
 #
 # pause
 #
@@ -214,16 +226,42 @@
 	Slim::Utils::Timers::killTimers($client, \&buffering);
 
 	$client->stream('p');
+	$client->playPoint(undef);
 	$client->SUPER::pause();
 	return 1;
 }
 
+sub pauseForInterval {
+	my $client   = shift;
+	my $interval = shift;
+
+	# TODO - show resyncing message briefly
+	# TODO - adjust interval for SB1 internal decode buffer
+	
+	$client->playPoint(undef);
+	$client->stream('p');
+	Slim::Utils::Timers::setHighTimer(
+				$client,
+				Time::HiRes::time() + $interval - 0.005,
+				\&_unpauseAfterInterval
+			);
+	return 1;
+}
+
+sub _unpauseAfterInterval {
+	my $client = shift;
+	$client->stream('u');
+	$client->playPoint(undef);
+	return 1;
+}
+
 sub stop {
 	my $client = shift;
 
 	Slim::Utils::Timers::killTimers($client, \&buffering);
 
 	$client->stream('q');
+	$client->playPoint(undef);
 	Slim::Networking::Slimproto::stop($client);
 	# disassociate the streaming socket to the client from the client.  HTTP.pm will close the socket on the next select.
 	$client->streamingsocket(undef);
@@ -315,7 +353,7 @@
 		if ( $stillBuffering ) {
 			Slim::Utils::Timers::setTimer(
 				$client,
-				Time::HiRes::time() + 0.125,
+				Time::HiRes::time() + 0.400, # was .125 but too fast sometimes in wireless settings
 				\&buffering,
 				$threshold,
 			);
@@ -1021,6 +1059,22 @@
 		$flags |= 0x40 if $params->{reconnect};
 		$flags |= 0x80 if $params->{loop};
 		$flags |= ($prefs->client($client)->get('polarityInversion') || 0);
+		
+		# ReplayGain field is also used for startAt, pauseAt, unpauseAt, timestamp
+		my $replayGain = 0;
+		my $interval = $params->{interval} || 0;
+		if ($command eq 'a' || $command eq 'p') {
+			$replayGain = int($interval * 1000);
+		}
+		elsif ($command eq 'u') {
+			 $replayGain = $interval;
+		}
+		elsif ($command eq 't') {
+			$replayGain = int(Time::HiRes::time() * 1000 % 0xffffffff);
+		}
+		else {
+			$replayGain = $client->canDoReplayGain($params->{replay_gain});
+		}
 
 		$log->info("flags: $flags");
 
@@ -1039,7 +1093,7 @@
 			$flags,		# flags	     
 			$outputThreshold,
 			0,		# reserved
-			$client->canDoReplayGain($params->{replay_gain}),		
+			$replayGain,	
 			$server_port || $prefs->get('httpport'),  # use slim server's IP
 			$server_ip || 0,
 		);

Modified: trunk/server/Slim/Player/Squeezebox2.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Squeezebox2.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Squeezebox2.pm (original)
+++ trunk/server/Slim/Player/Squeezebox2.pm Thu Aug 30 21:08:54 2007
@@ -39,6 +39,7 @@
 	'transitionDuration' => 0,
 	'replayGainMode'     => 0,
 	'disableDac'         => 0,
+	'minSyncAdjust'      => 0.010,
 	'snLastSyncUp'       => -1,
 	'snLastSyncDown'     => -1,
 	'snSyncInterval'     => 30,
@@ -241,7 +242,7 @@
 }
 
 sub requestStatus {
-	shift->sendFrame('stat');
+	shift->stream('t');
 }
 
 sub stop {
@@ -319,7 +320,7 @@
 	
 	my $response = shift @headers;
 	
-	if (!$response || $response !~ / (\d\d\d)/) {
+	if (!$response || $response !~ m/ (\d\d\d)/) {
 
 		$log->warn("Invalid response code ($response) from remote stream $url");
 
@@ -852,4 +853,57 @@
 	return defined $rate ? $rate : '3';
 }
 
+sub playPoint {
+	my $client = shift;
+
+	my ($jiffies, $elapsedMilliseconds) = Slim::Networking::Slimproto::getPlayPointData($client);
+
+	return unless $elapsedMilliseconds;
+
+	my $statusTime = $client->jiffiesToTimestamp($jiffies);
+	my $apparentStreamStartTime = $statusTime - ($elapsedMilliseconds / 1000);
+
+	0 && logger('player.sync')->debug($client->id() . " playPoint: jiffies=$jiffies, epoch="
+		. ($client->jiffiesEpoch) . ", statusTime=$statusTime, elapsedMilliseconds=$elapsedMilliseconds");
+
+	return [$statusTime, $apparentStreamStartTime];
+}
+
+sub startAt {
+	my ($client, $at) = @_;
+	
+	if ( logger('player.sync')->is_debug ) {
+		logger('player.sync')->debug( $client->id, ' startAt: ' . int(($at - $client->jiffiesEpoch()) * 1000) );
+	}
+
+	$client->stream( 'u', { 'interval' => int(($at - $client->jiffiesEpoch()) * 1000) } );
+	return 1;
+}
+
+sub pauseForInterval {
+	my $client   = shift;
+	my $interval = shift;
+
+	$client->stream( 'p', { 'interval' => $interval } );
+	return 1;
+}
+
+sub skipAhead {
+	my $client   = shift;
+	my $interval = shift;
+
+	$client->stream( 'a', { 'interval' => $interval } );
+	return 1;
+}
+
+sub packetLatency {
+	my $client = shift;
+	
+	return (
+		Slim::Networking::Slimproto::getLatency($client) / 1000
+		||
+		$client->SUPER::packetLatency()
+	);
+}
+
 1;

Modified: trunk/server/Slim/Player/Sync.pm
URL: http://svn.slimdevices.com/trunk/server/Slim/Player/Sync.pm?rev=12808&r1=12807&r2=12808&view=diff
==============================================================================
--- trunk/server/Slim/Player/Sync.pm (original)
+++ trunk/server/Slim/Player/Sync.pm Thu Aug 30 21:08:54 2007
@@ -17,6 +17,12 @@
 
 my $prefs = preferences('server');
 
+my %nextCheckSyncTime;	# kept for each sync-group master player
+use constant CHECK_SYNC_INTERVAL        => 0.950;
+use constant MIN_DEVIATION_ADJUST       => 0.010;
+use constant MAX_DEVIATION_ADJUST       => 10.000;
+use constant PLAYPOINT_RECENT_THRESHOLD => 3.0;
+
 # playlist synchronization routines
 sub syncname {
 	my $client = shift;
@@ -72,15 +78,16 @@
 	my $client = shift;
 	my $temp = shift;
 
-	$log->info($client->id . ": unsyncing");
-
 	# bail if we don't have sync state
 	if (!defined($client->syncgroupid)) {
 		return;
 	}
+	
+	$log->info($client->id . ": unsyncing");
 
 	my $syncgroupid = $client->syncgroupid;
 	my $lastInGroup;
+	my $master = $client->master;
 	
 	# if we're the master...
 	if (isMaster($client)) {
@@ -122,6 +129,20 @@
 		$newmaster->audioFilehandle($client->audioFilehandle);
 		$client->audioFilehandle(undef);	
 
+		$newmaster->audioFilehandleIsSocket($client->audioFilehandleIsSocket);
+		$client->audioFilehandleIsSocket(0);	
+
+		$newmaster->frameData($client->frameData);
+		$client->frameData(undef);	
+
+		$newmaster->initialStreamBuffer($client->initialStreamBuffer);
+		$client->initialStreamBuffer(undef);	
+
+		$newmaster->streamformat($client->streamformat);
+		$client->streamformat(undef);	
+
+		$master = $newmaster;
+
 	} elsif (isSlave($client)) {
 
 		# if we're a slave, remove us from the master's list
@@ -139,7 +160,6 @@
 		}	
 	
 		# and copy the playlist to the now freed slave
-		my $master = $client->master;
 		
 		$client->master(undef);
 		
@@ -150,6 +170,19 @@
 	} else {
 
 		$lastInGroup = $client;
+	}
+	
+	if ($lastInGroup) {
+		Slim::Player::Source::resetFrameData($lastInGroup);
+	}
+	else {
+	    # do we still need to save frame data?
+	    my $needFrameData = 0;
+	    foreach ( $master, Slim::Player::Sync::slaves($master) ) {
+		    my $model = $_->model();
+		    last if $needFrameData = ($model eq 'slimp3' || $model eq 'squeezebox');
+	    }
+	    Slim::Player::Source::resetFrameData($master) unless ($needFrameData);
 	}
 
 	# check for any players in group which are off and hence not synced
@@ -327,7 +360,7 @@
 
 				push @buddies, $otherclient;
 
-				$log->debug($client->id . ": is synced with other slave " . $otherclient->id);
+				# $log->debug($client->id . ": is synced with other slave " . $otherclient->id);
 			}
 		}
 	}
@@ -337,7 +370,7 @@
 
 		push @buddies, $otherclient;
 
-		$log->debug($client->id . ": is synced with it's slave " . $otherclient->id);
+		# $log->debug($client->id . ": is synced with it's slave " . $otherclient->id);
 	}
 
 	return @buddies;
@@ -407,15 +440,17 @@
 sub checkSync {
 	my $client = shift;
 
-	$log->debug(sprintf("Player %s has %d chunks and %d%% full buffer", 
-		$client->id, scalar(@{$client->chunks}), $client->usage
-	));
-
 	if (!isSynced($client) || $prefs->client($client)->get('silent')) {
 		return;
 	}
 
 	return if $client->playmode eq 'stop';
+
+	if ( 0 && $log->is_debug && isSynced($client) ) {
+		$log->debug(sprintf("Player %s has %d chunks and %d%% full buffer, readyToSync=%s", 
+			$client->id, scalar(@{$client->chunks}), $client->usage, $client->readytosync()
+		));
+	}
 
 	my @group = ($client, syncedWith($client));
 
@@ -451,30 +486,41 @@
 
 				$client->readytosync(1);
 		
-				$log->info($client->id . " is ready to sync " . Time::HiRes::time());
+				$log->info($client->id . " is ready to sync");
 
 				my $allReady = 1;
+				my $playerStartDelay = 0;
 
 				for my $everyclient (@group) {
 
-					if (!$everyclient->readytosync) {
+					if ( !$everyclient->readytosync ) {
 						$allReady = 0;
+					}
+					else {
+						my $delay;
+						if ( $delay = $prefs->client($everyclient)->get('startDelay') && $delay > $playerStartDelay ) {
+							$playerStartDelay = $delay;
+						}
 					}
 				}
 			
 				if ($allReady) {
 
 					$log->info("all clients ready to sync now. unpausing them.");
+					
+					my $startAt = Time::HiRes::time() + $playerStartDelay
+								+ ( $prefs->get('syncStartDelay') || 0.100 );
 
 					for my $everyclient (@group) {
-						$everyclient->resume;
+						$everyclient->startAt( $startAt - ( $prefs->client($everyclient)->get('startDelay') || 0) );
 					}
 				}
 			}
 		}
 
 	# now check to see if every player has run out of data...
-	} elsif ($client->readytosync == -1) {
+	}
+	elsif ($client->readytosync == -1) {
 
 		$log->info($client->id . " has run out of data, checking to see if we can push on...");
 
@@ -507,6 +553,92 @@
 				Slim::Player::Source::playmode($client,'stop');
 
 				$client->update;
+			}
+		}
+	}
+	elsif ( isMaster($client) ) {
+		# check to see if resynchronization is necessary
+
+		my $now = Time::HiRes::time();
+
+		return if $now < $nextCheckSyncTime{$client};
+
+		$nextCheckSyncTime{$client} = $now + CHECK_SYNC_INTERVAL;
+
+		# $log->debug("checksync: checking for resync");
+
+		# need a recent play-point from all players in the group, otherwise give up
+		my $recentThreshold = $now - PLAYPOINT_RECENT_THRESHOLD;
+		my @playerPlayPoints;
+		foreach my $player (@group) {
+			next unless $prefs->client($player)->get('maintainSync');
+			my $playPoint = $player->playPoint();
+			if ( !defined $playPoint ) {
+				$log->debug($player->id() ." bailing as no playPoint");
+				return;
+			}
+			if ($playPoint->[0] > $recentThreshold) {
+				push(@playerPlayPoints, [$player, $playPoint->[1]]);
+			}
+			else {
+				$log->debug(
+					$player->id() ." bailing as playPoint too old: ".
+					($now - $playPoint->[0]) . "s"
+				);
+				return;
+			}
+		}
+		return unless scalar(@playerPlayPoints);
+
+		if ( $log->is_debug ) {
+			my $first = $playerPlayPoints[0][1];
+			my $str = sprintf("%s: %.3f", $playerPlayPoints[0][0]->id(), $first);
+			foreach ( @playerPlayPoints[1 .. $#playerPlayPoints] ) {
+				$str .= sprintf(", %s: %+5d", $_->[0]->id(), ($_->[1] - $first) * 1000);
+			}
+			$log->debug("playPoints: $str");
+		}
+
+		# sort the play-points by decreasing apparent-start-time
+		@playerPlayPoints = sort {$b->[1] <=> $a->[1]} @playerPlayPoints;
+
+		# clean up the list of stored frame data
+		# (do this now, so that it does not delay critial timers when using pauseFor())
+		Slim::Player::Source::purgeOldFrames( $client, $recentThreshold - $playerPlayPoints[0][1] );
+
+		# find the reference player - the most-behind that does not support skipAhead
+		my $reference;
+		for ( $reference = 0; $reference < $#playerPlayPoints; $reference++ ) {
+			last unless $playerPlayPoints[$reference][0]->can('skipAhead');
+		}
+		my $referenceTime = $playerPlayPoints[$reference][1];
+		# my $referenceMinAdjust = $prefs->client( $playerPlayPoints[$reference][0] )->get('minSyncAdjust');
+
+		# tell each player that is out-of-sync with the reference to adjust
+		for ( my $i = 0; $i < @playerPlayPoints; $i++ ) {
+			next if ($i == $reference);
+			my $player = $playerPlayPoints[$i][0];
+			my $delta = abs($playerPlayPoints[$i][1] - $referenceTime);
+			next if ($delta > MAX_DEVIATION_ADJUST
+				|| $delta < MIN_DEVIATION_ADJUST
+				|| $delta < $prefs->client($player)->get('minSyncAdjust')
+				# || $delta < $referenceMinAdjust
+				);
+			if ($i < $reference) {
+				if ( $log->is_debug ) {
+					$log->debug( sprintf("%s resync: skipAhead %dms", $player->id(), $delta * 1000) );
+				}
+				
+				$player->skipAhead($delta);
+				$nextCheckSyncTime{$client} += 1;
+			}
+			else {
+				if ( $log->is_debug ) {
+					$log->debug( sprintf("%s resync: pauseFor %dms", $player->id(), $delta * 1000) );
+				}
+				
+				$player->pauseForInterval($delta);
+				$nextCheckSyncTime{$client} += $delta;
 			}
 		}
 	}

Added: trunk/server/lib/MPEG/Audio/Frame.pm
URL: http://svn.slimdevices.com/trunk/server/lib/MPEG/Audio/Frame.pm?rev=12808&view=auto
==============================================================================
--- trunk/server/lib/MPEG/Audio/Frame.pm (added)
+++ trunk/server/lib/MPEG/Audio/Frame.pm Thu Aug 30 21:08:54 2007
@@ -1,0 +1,269 @@
+#!/usr/bin/perl -w
+
+package MPEG::Audio::Frame;
+
+# BLECH! With 5.005_04 compatibility the pretty 0b000101001 notation went away,
+# and now we're stuck using hex. Phooey!
+
+use strict;
+#use warnings;
+use integer;
+
+# fields::new is not used because it is very costly in such a tight loop. about 1/4th of the time, according to DProf
+#use fields qw/
+#	headhash
+#	binhead
+#	header
+#	content
+#	length
+#	bitrate
+#	sample
+#	offset
+#	crc_sum
+#	calculated_sum
+#	broken
+#/;
+
+use overload '""' => \&asbin;
+
+use vars qw/$VERSION $free_bitrate $lax $mpeg25/;
+$VERSION = 0.09;
+
+$mpeg25 = 1; # normally support it
+
+# constants and tables
+
+
+
+my @version = (
+	1,		# 0b00 MPEG 2.5
+	undef,	# 0b01 is reserved
+	1,		# 0b10 MPEG 2
+	0,		# 0b11 MPEG 1
+);
+
+my @layer = (
+	undef,	# 0b00 is reserved
+	2,		# 0b01 Layer III
+	1,		# 0b10 Layer II
+	0,		# 0b11 Layer I
+);
+
+my @bitrates = (
+		# 0/free 1   10  11  100  101  110  111  1000 1001 1010 1011 1100 1101 1110 # bits
+	[	# mpeg 1
+		[ undef, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 ], # l1
+		[ undef, 32, 48, 56, 64,  80,  96,  112, 128, 160, 192, 224, 256, 320, 384 ], # l2
+		[ undef, 32, 40, 48, 56,  64,  80,  96,  112, 128, 160, 192, 224, 256, 320 ], # l3
+	],
+	[	# mpeg 2
+		[ undef, 32, 48, 56, 64,  80,  96,  112, 128, 144, 160, 176, 192, 224, 256 ], # l1
+		[ undef, 8,  16, 24, 32,  40,  48,  56,  64,  80,  96,  112, 128, 144, 160 ], # l3
+		[ undef, 8,  16, 24, 32,  40,  48,  56,  64,  80,  96,  112, 128, 144, 160 ], # l3
+	],
+);
+
+my @samples = (
+	[ # MPEG 2.5
+		11025, # 0b00
+		12000, # 0b01
+		8000,  # 0b10
+		undef, # 0b11 is reserved
+	],
+	undef, # version 0b01 is reserved
+	[ # MPEG 2
+		22050, # 0b00
+		24000, # 0b01
+		16000, # 0b10
+		undef, # 0b11 is reserved
+	],
+	[ # MPEG 1
+		44100, # 0b00
+		48000, # 0b01
+		32000, # 0b10
+		undef, # 0b11 is reserved
+	],
+);
+
+
+# stolen from libmad, bin.c
+my @crc_table = (
+	0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011,
+	0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022,
+	0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072,
+	0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041,
+	0x80c3, 0x00c6, 0x00cc, 0x80c9, 0x00d8, 0x80dd, 0x80d7, 0x00d2,
+	0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1,
+	0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1,
+	0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082,
+
+	0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192,
+	0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1,
+	0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1,
+	0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2,
+	0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151,
+	0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162,
+	0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132,
+	0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101,
+
+	0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312,
+	0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321,
+	0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371,
+	0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342,
+	0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1,
+	0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2,
+	0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2,
+	0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381,
+
+	0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291,
+	0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2,
+	0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2,
+	0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1,
+	0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252,
+	0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261,
+	0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231,
+	0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202
+);
+
+sub CRC_POLY () { 0x8005 }
+
+###
+
+my @protbits = (
+	[ 128, 256 ], # layer one
+	undef,
+	[ 136, 256 ], # layer three
+);
+
+
+my @consts;
+sub B ($) { $_[0] == 12 ? 3 : (1 + ($_[0] / 4)) }
+sub M ($) {
+	my $s = 0;
+	$s += $consts[$_][1] for (0 .. $_[0]-1);
+	$s%=8;
+	my $v = '';
+	vec($v,8-$_,1) = 1 for $s+1 .. $s+$consts[$_[0]][1];
+	"0x" . unpack("H*", $v);
+}
+sub R ($) { 
+	my $i = 0;
+	my $m = eval "M_$consts[$_[0]][0]()";
+	$i++ until (($m >> $i) & 1);
+	$i;
+}
+
+BEGIN {
+	@consts = (
+		# [ $name, $width ]
+		[ SYNC => 3 ],
+		[ VERSION => 2 ],
+		[ LAYER => 2 ],
+		[ CRC => 1 ],	
+		[ BITRATE => 4 ],
+		[ SAMPLE => 2 ],
+		[ PAD => 1 ],
+		[ PRIVATE => 1 ],
+		[ CHANMODE => 2 ],
+		[ MODEXT => 2 ],
+		[ COPY => 1 ],
+		[ HOME => 1 ],
+		[ EMPH => 2 ],
+	);
+	my $i = 0;
+	foreach my $c (@consts){
+		my $CONST = $c->[0];
+		eval "sub $CONST () { $i }"; # offset in $self->{header}
+		eval "sub M_$CONST () { " . M($i) ." }"; # bit mask
+		eval "sub B_$CONST () { " . B($i) . " }"; # offset in read()'s @hb
+		eval "sub R_$CONST () { " . R($i) . " }"; # amount to right shift
+		$i++;
+	}
+}
+
+
+# constructor and work horse
+sub read {
+	my $pkg = shift || return undef;
+	my $bufref = shift || return undef;
+	my $start = shift;
+
+	my $pos = $start;
+	my $len = length($$bufref);
+	
+	my $header; # the binary header data... what a fabulous pun.
+	my @hr; # an array of integer
+
+	OUTER: while ($pos+4 < $len) {
+		if (substr($$bufref, $pos, 1) eq "\xff") {
+
+			$header = substr($$bufref, $pos, 4); $pos += 4;
+
+			my @hb = unpack("CCCC",$header); # an array of 4 integers for convenient access, each representing a byte of the header
+			# I wish vec could take non powers of 2 for the bit width param... *sigh*
+			# make sure there are no illegal values in the header
+			($hr[SYNC]		= ($hb[B_SYNC] 		& M_SYNC)		>> R_SYNC)		!= 0x07 and next; # see if the sync remains
+			($hr[VERSION]	= ($hb[B_VERSION]	& M_VERSION)	>> R_VERSION)	== 0x00 and ($mpeg25 or next);
+			($hr[VERSION])														== 0x01 and next;
+			($hr[LAYER]		= ($hb[B_LAYER]		& M_LAYER)		>> R_LAYER)		== 0x00 and next;
+			($hr[BITRATE]	= ($hb[B_BITRATE]	& M_BITRATE)	>> R_BITRATE)	== 0x0f and next;
+			($hr[SAMPLE]	= ($hb[B_SAMPLE]	& M_SAMPLE) 	>> R_SAMPLE)	== 0x03 and next;
+			($hr[EMPH]		= ($hb[B_EMPH]		& M_EMPH) 		>> R_EMPH)		== 0x02 and ($lax or next);
+			# and drink up all that we don't bother verifying
+			$hr[CRC]		= ($hb[B_CRC] & M_CRC) >> R_CRC;
+			$hr[PAD]		= ($hb[B_PAD] & M_PAD) >> R_PAD;
+			#$hr[PRIVATE]	= ($hb[B_PRIVATE] & M_PRIVATE) >> R_PRIVATE;
+			#$hr[CHANMODE]	= ($hb[B_CHANMODE] & M_CHANMODE) >> R_CHANMODE;
+			#$hr[MODEXT]		= ($hb[B_MODEXT] & M_MODEXT) >> R_MODEXT;
+			#$hr[COPY]		= ($hb[B_COPY] & M_COPY) >> R_COPY;
+			#$hr[HOME]		= ($hb[B_HOME] & M_HOME) >> R_HOME;
+
+			last OUTER;
+		}
+		$pos++;
+	}
+
+	my $sum = '';
+	if (!$hr[CRC]) {
+		return undef if (($pos += 2) >= $len);
+	}
+
+	my $bitrate	= $bitrates[$version[$hr[VERSION]]][$layer[$hr[LAYER]]][$hr[BITRATE]] or return undef;
+	my $sample	= $samples[$hr[VERSION]][$hr[SAMPLE]];
+
+	my $use_smaller = $hr[VERSION] == 2 || $hr[VERSION] == 0; # FIXME VERSION == 2 means no support for MPEG2 multichannel
+	my $length = $layer[$hr[LAYER]]
+		?  (($use_smaller ? 72 : 144) * ($bitrate * 1000) / $sample + $hr[PAD])		# layers 2 & 3
+		: ((($use_smaller ? 6  : 12 ) * ($bitrate * 1000) / $sample + $hr[PAD]) * 4);	# layer 1
+	
+	my $clength = $length - 4 - ($hr[CRC] ? 0 : 2);
+	return undef if (($pos += $clength) > $len);
+
+	my $seconds;
+	{
+		no integer;
+		$seconds = $layer[$hr[LAYER]]
+			? (($version[$hr[VERSION]] == 0 ? 1152 : 576) / $sample)
+			: (($version[$hr[VERSION]] == 0 ? 384 : 192) / $sample);
+	}
+
+	return ($length, $pos, $seconds);
+}
+
+1; # keep your mother happy
+
+__END__
+
+=pod
+
+=head1 AUTHOR
+
+Yuval Kojman <nothingmuch at altern.org>
+
+=head1 COPYRIGHT
+
+	Copyright (c) 2003 Yuval Kojman. All rights reserved
+	This program is free software; you can redistribute
+	it and/or modify it under the same terms as Perl itself.
+
+=cut



More information about the checkins mailing list