mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-10 00:03:50 +08:00
Crossfade bounded Bluetooth DAC estimate reversals in saved playback
This commit is contained in:
@@ -12,21 +12,24 @@ class PreparedClock:
|
||||
"""One audio-callback owner. Read snapshot outside the callback for evidence.
|
||||
|
||||
expected_frame comes from the existing original-model/DAC mapping. Small
|
||||
scheduling jitter keeps contiguous PCM. A bounded forward discontinuity skips
|
||||
the material whose presentation time has passed, blending old and new source
|
||||
for 40 ms without inserting samples. Explicit seeks may move either direction;
|
||||
scheduling jitter keeps contiguous PCM. Bounded DAC estimate changes blend
|
||||
old and new source for 40 ms without inserting samples. A short backwards
|
||||
output-delay correction is distinct from rewinding the route/model clock,
|
||||
which the caller separately guards. Explicit seeks may move either direction;
|
||||
the caller must reset presentation state outside the callback when seeking.
|
||||
"""
|
||||
def __init__(self, rate=48000, *, threshold_ms=50., crossfade_ms=40., maximum_forward_seconds=5.):
|
||||
def __init__(self, rate=48000, *, threshold_ms=50., crossfade_ms=40., maximum_forward_seconds=5., maximum_backward_seconds=.25):
|
||||
if type(rate) is not int or rate <= 0:raise ValueError('Invalid sample rate')
|
||||
values=(threshold_ms,crossfade_ms,maximum_forward_seconds)
|
||||
values=(threshold_ms,crossfade_ms,maximum_forward_seconds,maximum_backward_seconds)
|
||||
if any(type(v) not in (int,float) or not math.isfinite(v) for v in values):raise ValueError('Invalid clock recovery limits')
|
||||
if not 0<threshold_ms<=100 or not 1<=crossfade_ms<=40 or maximum_forward_seconds<=threshold_ms/1000:
|
||||
if (not 0<threshold_ms<=100 or not 1<=crossfade_ms<=40 or maximum_forward_seconds<=threshold_ms/1000
|
||||
or not threshold_ms/1000<maximum_backward_seconds<=.25):
|
||||
raise ValueError('Clock recovery limits are out of bounds')
|
||||
self.rate=rate
|
||||
self.threshold=round(rate*threshold_ms/1000)
|
||||
self.fade_frames=max(1,round(rate*crossfade_ms/1000))
|
||||
self.maximum_forward=round(rate*maximum_forward_seconds)
|
||||
self.maximum_backward=round(rate*maximum_backward_seconds)
|
||||
self.position=None
|
||||
self.fade_from=None
|
||||
self.fade_done=0
|
||||
@@ -58,10 +61,10 @@ class PreparedClock:
|
||||
correction=None
|
||||
if not initial and (explicit_seek or abs(delta)>self.threshold):
|
||||
if not explicit_seek:
|
||||
if delta<0:raise ClockDiscontinuity('Prepared audio clock moved backwards without an explicit seek')
|
||||
if delta < -self.maximum_backward:raise ClockDiscontinuity('Prepared audio clock moved backwards beyond the output-delay recovery bound')
|
||||
if delta>self.maximum_forward:raise ClockDiscontinuity('Prepared audio clock discontinuity exceeds the recovery bound')
|
||||
if self.fade_from is not None:raise ClockDiscontinuity('Prepared audio clock changed again during recovery')
|
||||
kind='explicit-seek' if explicit_seek else 'forward-clock-recovery'
|
||||
kind='explicit-seek' if explicit_seek else 'backward-dac-recovery' if delta<0 else 'forward-clock-recovery'
|
||||
correction={'kind':kind,'from_frame':previous,'to_frame':expected_frame,'delta_frames':delta,'crossfade_frames':self.fade_frames}
|
||||
self.events.append(correction)
|
||||
self.corrections+=not explicit_seek
|
||||
@@ -92,4 +95,5 @@ class PreparedClock:
|
||||
return {'next_source_frame':self.position,'corrections':self.corrections,'explicit_seeks':self.explicit_seeks,
|
||||
'max_pre_error_seconds':self.max_pre_error/self.rate,'max_post_error_seconds':self.max_post_error/self.rate,
|
||||
'crossfade_ms':1000*self.fade_frames/self.rate,'recovery_in_progress':self.fade_from is not None,
|
||||
'maximum_backward_recovery_seconds':self.maximum_backward/self.rate,
|
||||
'events':list(self.events),'added_delay_samples':0,'resampling':False}
|
||||
|
||||
@@ -39,6 +39,29 @@ class PreparedClockTests(unittest.TestCase):
|
||||
for target in (0,position+6*48000):
|
||||
with self.assertRaises(ClockDiscontinuity):clock.render(self.core,target,960)
|
||||
self.assertEqual(clock.position,position)
|
||||
|
||||
def test_measured_65ms_bluetooth_delay_correction_crossfades(self):
|
||||
# Saved0217 failed with next8757465 vs expected8754316: 3149 frames,
|
||||
# or65.60ms backwards in the estimated DAC clock, not in model messages.
|
||||
clock=PreparedClock();clock.render(self.core,96000,960)
|
||||
previous=clock.position;target=previous-3149
|
||||
first,info=clock.render(self.core,target,960)
|
||||
second,last=clock.render(self.core,target+960,960)
|
||||
self.assertEqual(info['correction']['kind'],'backward-dac-recovery')
|
||||
self.assertEqual(info['correction']['delta_frames'],-3149)
|
||||
self.assertEqual(first.shape,(960,2));self.assertEqual(second.shape,(960,2))
|
||||
self.assertLess(np.max(abs(first[0]-self.core[previous])),.001)
|
||||
np.testing.assert_allclose(second[-1],self.core[target+1919],atol=3e-8)
|
||||
self.assertEqual(last['post_error_frames'],0)
|
||||
self.assertEqual(clock.explicit_seeks,0);self.assertEqual(clock.corrections,1)
|
||||
self.assertIsNone(clock.fade_from)
|
||||
|
||||
def test_reverse_clock_recovery_has_a_separate_small_bound(self):
|
||||
clock=PreparedClock();clock.render(self.core,96000,960)
|
||||
before=clock.position
|
||||
with self.assertRaises(ClockDiscontinuity):clock.render(self.core,before-12001,960)
|
||||
self.assertEqual(clock.position,before)
|
||||
with self.assertRaises(ValueError):PreparedClock(maximum_backward_seconds=1.)
|
||||
def test_explicit_seek_can_rebase_backwards(self):
|
||||
clock=PreparedClock();clock.render(self.core,96000,960)
|
||||
_,info=clock.render(self.core,4800,960,explicit_seek=True)
|
||||
|
||||
Reference in New Issue
Block a user