Calibrate prepared audio from silent callback timestamps

This commit is contained in:
firestar5683
2026-09-20 11:29:31 -07:00
parent 2bbee419f2
commit 2ff7ed501c
2 changed files with 27 additions and 3 deletions
+14 -2
View File
@@ -222,6 +222,7 @@ def audio_worker(a):
done = False
errors = []
clock_errors = []
clock_observations = []
bridge = None
flags = 0
max_drift = 0.
@@ -231,7 +232,11 @@ def audio_worker(a):
def callback(out, n, ti, status):
nonlocal position,rendered,done,flags,max_drift,first_frame,callback_revision
out.fill(0)
if anchor is None or bridge is None or done:return
if bridge is None:
if len(clock_observations)<24:
clock_observations.append((float(time.monotonic()),float(ti.currentTime),float(ti.outputBufferDacTime)))
return
if anchor is None or done:return
now=time.monotonic()
active_anchor=anchor
expected=None;dac=None;start=position
@@ -266,7 +271,14 @@ def audio_worker(a):
server = control_server(a.project_root,a.out,shared,a.port,forwarder,follow_peer=forwarder is not None)
write_json(a.out/'controls.json',{'url':f'http://127.0.0.1:{server.server_port}/'})
with sd.OutputStream(device=a.audio_device,samplerate=rate,channels=2,blocksize=960,dtype='float32',callback=callback) as stream:
bridge=StreamClockBridge.measure(lambda:stream.time)
calibration_deadline=time.monotonic()+2.
while len(clock_observations)<20:
if not stream.active:raise RuntimeError('Output stream stopped during silent clock calibration')
if time.monotonic()>=calibration_deadline:raise TimeoutError('Output clock did not provide 20 silent callbacks')
time.sleep(.01)
observations=list(clock_observations[:20])
write_json(a.out/'stream_clock_calibration.json',dict(silent_callback_samples=observations))
bridge=StreamClockBridge.from_callbacks(observations)
(a.out/'prepared_ready').write_text('ready')
while not done:
sm.update(50);now=time.monotonic();controls.poll()
+13 -1
View File
@@ -120,9 +120,21 @@ class ReplaySyncTests(unittest.TestCase):
playback_clock=PreparedClock(),audio=np.zeros((480000,2),np.float32),
processor=SimpleNamespace(process=lambda chunk,*args:(chunk,{})),state={},
controls=SimpleNamespace(selection=('recorded','recorded')),a=SimpleNamespace(muted=True),
errors=[],clock_errors=[])
errors=[],clock_errors=[],clock_observations=[])
exec(compile(ast.Module(body=[callback],type_ignores=[]),str(path),'exec'),context)
out=np.ones((960,2),np.float32)
# Startup gathers only a bounded set of silent observations. No stream.time
# getter, model anchor, source-frame processing or output is required.
saved_bridge=context['bridge'];context['bridge']=None
for i in range(30):
context['time'].monotonic=lambda i=i:99.+i*.02
context['callback'](out,960,SimpleNamespace(currentTime=499.+i*.02,outputBufferDacTime=499.04+i*.02),False)
self.assertFalse(out.any())
self.assertEqual(len(context['clock_observations']),24)
self.assertIsNone(context['position'])
calibrated=StreamClockBridge.from_callbacks(context['clock_observations'][:20])
self.assertAlmostEqual(calibrated.offset,-400.)
context['bridge']=saved_bridge;context['time'].monotonic=lambda:100.08
context['callback'](out,960,SimpleNamespace(currentTime=500.,outputBufferDacTime=500.056),False)
context['time'].monotonic=lambda:100.04
context['callback'](out,960,SimpleNamespace(currentTime=500.02,outputBufferDacTime=500.076),False)