mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-04 13:24:13 +08:00
Give prepared native Bluetooth playback more output headroom
This commit is contained in:
@@ -3,6 +3,7 @@ import argparse
|
||||
import importlib.util
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
@@ -181,7 +182,29 @@ def control_server(project, out, shared, port, forwarder=None, *, follow_peer=Fa
|
||||
return server
|
||||
|
||||
|
||||
def output_latency_seconds(value):
|
||||
try:seconds=float(value)
|
||||
except (TypeError,ValueError):raise argparse.ArgumentTypeError('Output latency must be 0.02–0.5 seconds') from None
|
||||
if isinstance(value,bool) or not math.isfinite(seconds) or not .02<=seconds<=.5:
|
||||
raise argparse.ArgumentTypeError('Output latency must be 0.02–0.5 seconds')
|
||||
return seconds
|
||||
|
||||
|
||||
def native_bluetooth_latency(a, environ=None):
|
||||
value=a.output_latency
|
||||
if value is None:return None
|
||||
env=os.environ if environ is None else environ
|
||||
from bluetooth_output import ADDRESS, PCM_NAME
|
||||
identity=a.output_identity or ''
|
||||
if (not a.audio_worker or not a.no_control_server or env.get('OPENPILOT_PREFIX')!='roadscore_replay'
|
||||
or env.get('ROADSCORE_PREPARED_SHOWCASE')!='1' or a.audio_device!=PCM_NAME
|
||||
or not identity.startswith('bluealsa:') or not ADDRESS.fullmatch(identity.removeprefix('bluealsa:'))):
|
||||
raise ValueError('Output latency override requires the prepared native Bluetooth worker')
|
||||
return output_latency_seconds(value)
|
||||
|
||||
|
||||
def audio_worker(a):
|
||||
requested_output_latency=native_bluetooth_latency(a)
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
from cereal import messaging
|
||||
@@ -287,7 +310,7 @@ def audio_worker(a):
|
||||
if not a.no_control_server:
|
||||
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:
|
||||
with sd.OutputStream(device=a.audio_device,samplerate=rate,channels=2,blocksize=960,dtype='float32',latency=requested_output_latency,callback=callback) as stream:
|
||||
output_latency=float(stream.latency)
|
||||
calibration_deadline=time.monotonic()+2.
|
||||
while len(clock_observations)<20:
|
||||
@@ -377,7 +400,7 @@ def audio_worker(a):
|
||||
finally:
|
||||
trace.close()
|
||||
if sync_trace is not None:sync_trace.close()
|
||||
write_json(a.out/'prepared_summary.json',dict(generation_invoked=False,source=str(a.score_archive),first_source_frame=first_frame,last_source_frame=position,sample_rate=rate,portaudio_flags=flags,max_clock_error_seconds=max_drift,prepared_clock=playback_clock.snapshot(),stream_clock_bridge=bridge.snapshot() if bridge else None,callback_errors=errors,clock_errors=clock_errors,callback_timing=callback_timing,output_latency_seconds=output_latency,source_clock=source_clock,failure=failure,archived_tail=meta['archived_tail'],muted=a.muted,session_id=session,manual_scope='isolated replay display and presentation only'))
|
||||
write_json(a.out/'prepared_summary.json',dict(generation_invoked=False,source=str(a.score_archive),first_source_frame=first_frame,last_source_frame=position,sample_rate=rate,portaudio_flags=flags,max_clock_error_seconds=max_drift,prepared_clock=playback_clock.snapshot(),stream_clock_bridge=bridge.snapshot() if bridge else None,callback_errors=errors,clock_errors=clock_errors,callback_timing=callback_timing,requested_output_latency_seconds=requested_output_latency,output_latency_seconds=output_latency,source_clock=source_clock,failure=failure,archived_tail=meta['archived_tail'],muted=a.muted,session_id=session,manual_scope='isolated replay display and presentation only'))
|
||||
if errors:raise RuntimeError(errors[0])
|
||||
if abs(playback_clock.snapshot()['current_post_error_seconds'])>.05:raise RuntimeError('Prepared audio clock drift remained above 50 ms after recovery')
|
||||
|
||||
@@ -412,6 +435,7 @@ def parser():
|
||||
p.add_argument('--no-control-server',action='store_true',help=argparse.SUPPRESS)
|
||||
p.add_argument('--presentation-root',type=Path,help=argparse.SUPPRESS)
|
||||
p.add_argument('--output-identity',help=argparse.SUPPRESS)
|
||||
p.add_argument('--output-latency',type=output_latency_seconds,help=argparse.SUPPRESS)
|
||||
p.add_argument('--hold-start',action='store_true',help=argparse.SUPPRESS)
|
||||
p.add_argument('--fullscreen',action='store_true',help='Show the native Mac UI fullscreen; F/F11 toggles, Escape returns to its window')
|
||||
p.add_argument('--follow-playhead',action='store_true',help=argparse.SUPPRESS)
|
||||
@@ -430,6 +454,8 @@ def parser():
|
||||
|
||||
def main():
|
||||
a=parser().parse_args()
|
||||
if a.output_latency is not None and not a.audio_worker:
|
||||
raise ValueError('Output latency override is private to the prepared native Bluetooth worker')
|
||||
if a.audio_worker:
|
||||
if a.no_control_server and a.paired_comma:raise SystemExit('Paired controls require the local control server')
|
||||
if a.port is None:a.port=0
|
||||
|
||||
@@ -95,7 +95,7 @@ def worker_arguments(project, root, out, archive, route, muted, curve_plan=None,
|
||||
if muted:args.append('--muted')
|
||||
if curve_plan is not None:args += ['--curve-plan',str(curve_plan)]
|
||||
if output.get('bluetooth_selected'):
|
||||
args += ['--audio-device',output['pcm_name'],'--output-identity',output['output_identity']]
|
||||
args += ['--audio-device',output['pcm_name'],'--output-identity',output['output_identity'],'--output-latency','0.25']
|
||||
return args
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import argparse
|
||||
import unittest
|
||||
|
||||
from mac_showcase import native_bluetooth_latency, output_latency_seconds, parser
|
||||
|
||||
|
||||
class NativeBluetoothLatencyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.env={'OPENPILOT_PREFIX':'roadscore_replay','ROADSCORE_PREPARED_SHOWCASE':'1'}
|
||||
self.args=parser().parse_args(['--audio-worker','--no-control-server',
|
||||
'--audio-device','roadscore_bluetooth',
|
||||
'--output-identity','bluealsa:AA:BB:CC:DD:EE:FF',
|
||||
'--output-latency','0.25'])
|
||||
|
||||
def test_default_leaves_host_latency_unchanged(self):
|
||||
args=parser().parse_args([])
|
||||
self.assertIsNone(args.output_latency)
|
||||
self.assertIsNone(native_bluetooth_latency(args,{}))
|
||||
self.args.output_latency=None
|
||||
self.assertIsNone(native_bluetooth_latency(self.args,self.env))
|
||||
|
||||
def test_native_selected_bluetooth_accepts_bounded_numeric_latency(self):
|
||||
self.assertEqual(native_bluetooth_latency(self.args,self.env),.25)
|
||||
for value in ('.02','.25','.5'):
|
||||
self.assertEqual(output_latency_seconds(value),float(value))
|
||||
|
||||
def test_invalid_latency_is_rejected_before_any_audio_opens(self):
|
||||
for value in ('nan','inf','-inf','0','-1','.019','.501','text',True,None):
|
||||
with self.subTest(value=value),self.assertRaises(argparse.ArgumentTypeError):
|
||||
output_latency_seconds(value)
|
||||
|
||||
def test_override_cannot_change_mac_system_or_unowned_output(self):
|
||||
for field,value in [('audio_worker',False),('no_control_server',False),
|
||||
('audio_device',None),('audio_device','default'),
|
||||
('output_identity','system'),('output_identity','bluealsa:invalid')]:
|
||||
old=getattr(self.args,field);setattr(self.args,field,value)
|
||||
with self.subTest(field=field,value=value),self.assertRaises(ValueError):
|
||||
native_bluetooth_latency(self.args,self.env)
|
||||
setattr(self.args,field,old)
|
||||
for env in ({},{**self.env,'OPENPILOT_PREFIX':'roadscore-showcase-mac'},
|
||||
{**self.env,'ROADSCORE_PREPARED_SHOWCASE':'0'}):
|
||||
with self.subTest(env=env),self.assertRaises(ValueError):
|
||||
native_bluetooth_latency(self.args,env)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -61,8 +61,14 @@ class NativePreparedTests(unittest.TestCase):
|
||||
args=worker_arguments(Path(self.tmp.name),self.root,self.out,Path('/archive'),'fixture',False,output={'bluetooth_selected':True,'pcm_name':'roadscore_bluetooth','output_identity':'bluealsa:AA:BB'})
|
||||
self.assertIn('--audio-worker',args);self.assertIn('--no-control-server',args)
|
||||
self.assertIn('--presentation-root',args);self.assertIn('bluealsa:AA:BB',args)
|
||||
self.assertEqual(args[args.index('--output-latency')+1],'0.25')
|
||||
self.assertTrue(args[1].endswith('/mac_showcase.py'))
|
||||
self.assertFalse(any('ace_worker' in item or 'power_worker' in item for item in args))
|
||||
def test_system_output_keeps_its_existing_latency_default(self):
|
||||
for output in (None, {'bluetooth_selected':False}):
|
||||
args=worker_arguments(Path(self.tmp.name),self.root,self.out,Path('/archive'),'fixture',False,output=output)
|
||||
self.assertNotIn('--output-latency',args)
|
||||
self.assertNotIn('--output-identity',args)
|
||||
def test_continuous_capture_is_absent_even_with_inherited_mirror_directory(self):
|
||||
from screen_mirror import ScreenMirror
|
||||
inherited={'ROADSCORE_MIRROR_DIR':'/old/session/mirror'}
|
||||
|
||||
Reference in New Issue
Block a user