mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 07:23:45 +08:00
Spotify: Audio Routing And Resampling Quality
This commit is contained in:
@@ -253,7 +253,8 @@ class BlueZ:
|
||||
f"Bluetooth Device · {address[-5:]}" if relevant else address)
|
||||
devices.append({'path': path, 'address': address, 'name': str(name)[:80],
|
||||
'paired': props.get('Paired') is True, 'connected': props.get('Connected') is True,
|
||||
'trusted': props.get('Trusted') is True})
|
||||
'trusted': props.get('Trusted') is True,
|
||||
'audioSink': '0000110b-0000-1000-8000-00805f9b34fb' in profiles})
|
||||
return devices
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
|
||||
@@ -855,3 +855,13 @@ class BluetoothHeadUnitDiscoveryTest(unittest.TestCase):
|
||||
self.assertEqual({d['address'] for d in found}, {f'11:22:33:44:55:{i:02X}' for i in range(5)})
|
||||
self.assertTrue(all(d['name'] for d in found))
|
||||
self.assertTrue(all(not d['paired'] for d in found))
|
||||
|
||||
|
||||
class BluetoothAudioOutputTest(unittest.TestCase):
|
||||
def test_only_audio_sink_profile_can_receive_music(self):
|
||||
profiles = ['0000110B-0000-1000-8000-00805F9B34FB', '0000110a-0000-1000-8000-00805f9b34fb',
|
||||
'4de17a00-52cb-11e6-bdf4-0800200c9a66', None]
|
||||
objects = {f'/org/bluez/hci0/dev_{i}': {'org.bluez.Device1': {
|
||||
'Address': f'11:22:33:44:55:0{i}', 'Paired': True, 'UUIDs': [profile] if profile else [],
|
||||
}} for i, profile in enumerate(profiles)}
|
||||
self.assertEqual([device['audioSink'] for device in BlueZ._devices(objects)], [True, False, False, False])
|
||||
|
||||
@@ -90,7 +90,8 @@ class SpotifySetup:
|
||||
self._require_session(session)
|
||||
prefs = preferences.load(self.layout)
|
||||
try:
|
||||
devices = [{'address': d['address'], 'name': d['name'], 'connected': d.get('connected') is True}
|
||||
devices = [{'address': d['address'], 'name': d['name'], 'connected': d.get('connected') is True,
|
||||
'audioSink': d.get('audioSink') is True}
|
||||
for d in self.paired_devices(session) if d.get('paired')]
|
||||
except Exception:
|
||||
devices = []
|
||||
|
||||
@@ -144,3 +144,17 @@ assert.doesNotMatch(SpotifyPage.template, /Pause smoothing[^>]*parked/)
|
||||
}
|
||||
|
||||
assert.equal(serviceLabel({ enabled: true, service: { state: "ready", player: { status: "paused" }, outputWaiting: "Car" } }), "Paused Audio waits for Car.")
|
||||
|
||||
// Audio outputs require the receiving-music profile; the phone source list stays separate.
|
||||
{
|
||||
const devices = [
|
||||
{ address: "AA:BB:CC:DD:EE:01", name: "Car", connected: true, audioSink: true },
|
||||
{ address: "AA:BB:CC:DD:EE:02", name: "Speaker", connected: false, audioSink: true },
|
||||
{ address: "AA:BB:CC:DD:EE:03", name: "Phone", connected: true, audioSink: false },
|
||||
{ address: "AA:BB:CC:DD:EE:04", name: "Radar detector", connected: true },
|
||||
]
|
||||
const deviceChoices = SpotifyPage.computed.deviceChoices.call({ setup: { devices } })
|
||||
const outputs = SpotifyPage.computed.outputChoices.call({ deviceChoices })
|
||||
assert.deepEqual(outputs.map(choice => choice.label), ["Car", "Speaker (Not Connected)"])
|
||||
assert.equal(deviceChoices.length, 4)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from openpilot.starpilot.soloist import credentials, preferences
|
||||
from openpilot.starpilot.soloist.paths import Layout
|
||||
|
||||
SESSION = ('token', 1)
|
||||
DEVICES = [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'My Car', 'paired': True, 'connected': True},
|
||||
DEVICES = [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'My Car', 'paired': True, 'connected': True, 'audioSink': True},
|
||||
{'address': '11:22:33:44:55:66', 'name': 'Nearby', 'paired': False, 'connected': False}]
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_status_never_contains_the_key(owner):
|
||||
setup.set_key(SESSION, 'sk_live_0123456789abcdef')
|
||||
status = setup.status(SESSION)
|
||||
assert status['keyPresent'] is True and 'sk_live' not in json.dumps(status)
|
||||
assert status['devices'] == [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'My Car', 'connected': True}]
|
||||
assert status['devices'] == [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'My Car', 'connected': True, 'audioSink': True}]
|
||||
assert status['links']['download'].startswith('https://developer.spotify.com/')
|
||||
with pytest.raises(SetupRejected):
|
||||
setup.set_key(SESSION, 'bad key')
|
||||
|
||||
@@ -346,8 +346,11 @@ export const SpotifyPage = {
|
||||
keyValid() { return /^[A-Za-z0-9._~+/=-]{16,256}$/.test(this.keyValue.trim()) },
|
||||
nameValid() { return /^[^\x00-\x1f\x7f]{1,40}$/.test(this.nameValue.trim()) },
|
||||
outputChoices() {
|
||||
return this.deviceChoices.filter((choice) => choice.audioSink)
|
||||
},
|
||||
deviceChoices() {
|
||||
const devices = this.setup?.devices || []
|
||||
return devices.map((device) => ({ value: device.address, label: device.name + (device.connected ? "" : " (Not Connected)") }))
|
||||
return devices.map((device) => ({ value: device.address, audioSink: device.audioSink === true, label: device.name + (device.connected ? "" : " (Not Connected)") }))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -441,12 +444,13 @@ export const SpotifyPage = {
|
||||
<option value="phone">Phone over Bluetooth</option>
|
||||
</select>
|
||||
</label>
|
||||
<p v-if="!phoneSource" class="gx-note">Spotify streams on the comma. Your phone controls playback in Spotify; the comma sends the music to the audio output you choose below.</p>
|
||||
<template v-if="phoneSource">
|
||||
<p class="gx-note">Pair your phone in the companion app, or tap <strong>Pair phone</strong> on the <a class="gx-link" href="#/bluetooth">Bluetooth page</a>. Then choose it below and turn on phone audio. On your phone, select the comma as the media audio output. No Spotify install or API key is needed.</p>
|
||||
<label class="gx-stack"><span>Phone</span>
|
||||
<select class="gx-field" :value="setup.preferences.phoneAddress" :disabled="busy || !setup.parked" @change="choosePhone">
|
||||
<option value="">Choose your paired phone…</option>
|
||||
<option v-for="choice in outputChoices" :key="choice.value" :value="choice.value">{{ choice.label }}</option>
|
||||
<option v-for="choice in deviceChoices" :key="choice.value" :value="choice.value">{{ choice.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<GalaxySettingRow :row="switchRow('Phone Audio', setup.enabled, 'Forward music from the selected phone. Playback and volume stay on your phone.')"
|
||||
@@ -462,15 +466,17 @@ export const SpotifyPage = {
|
||||
<p class="gx-note">Music only. Calls and microphone audio are not forwarded.</p>
|
||||
</template>
|
||||
<template v-if="setupDone && !phoneSource && !showSetup">
|
||||
<label class="gx-stack"><span>Play on</span>
|
||||
<label class="gx-stack"><span>Send audio to</span>
|
||||
<select class="gx-field" :value="setup.preferences.output === 'bluetooth' ? setup.preferences.outputAddress : setup.preferences.output" :disabled="busy || !setup.parked" @change="chooseOutput">
|
||||
<option value="" disabled>Choose…</option>
|
||||
<option value="starport">StarPort (car audio)</option>
|
||||
<optgroup label="Bluetooth">
|
||||
<option value="" disabled>Choose an audio output…</option>
|
||||
<option value="starport">Car speakers via StarPort (Android Auto)</option>
|
||||
<optgroup label="Bluetooth car stereos and speakers">
|
||||
<option v-for="choice in outputChoices" :key="choice.value" :value="choice.value">{{ choice.label }}</option>
|
||||
</optgroup>
|
||||
<option value="test">Silent test (no speaker)</option>
|
||||
</select></label>
|
||||
<p class="gx-note">Choose your car speakers through StarPort, or a Bluetooth car stereo or speaker paired with the comma. Only Bluetooth devices that support receiving music appear here.</p>
|
||||
<p v-if="!outputChoices.length" class="gx-note">No paired Bluetooth audio outputs found. Pair your car stereo or speaker on the <a class="gx-link" href="#/bluetooth">Bluetooth page</a>, or use StarPort.</p>
|
||||
</template>
|
||||
<small v-if="phoneSource && !setup.parked" class="gx-note">Put the car in Park or use offroad mode to change the phone.</small>
|
||||
</section>
|
||||
@@ -562,21 +568,22 @@ export const SpotifyPage = {
|
||||
<li class="gx-card gx-starport-step" :class="{ 'gx-starport-step--done': outputChosen }">
|
||||
<div class="gx-starport-step__head">
|
||||
<span class="gx-starport-step__num" aria-hidden="true"><i v-if="outputChosen" class="bi bi-check-lg"></i><template v-else>3</template></span>
|
||||
<div><h3>Where Music Plays</h3><p>Through Android Auto, or your car or a speaker over Bluetooth. Alerts keep playing from the comma itself.</p></div>
|
||||
<div><h3>Where Music Plays</h3><p>Send music from the comma to your car speakers through StarPort (Android Auto), or to a Bluetooth car stereo or speaker. Alerts keep playing from the comma itself.</p></div>
|
||||
</div>
|
||||
<div class="gx-starport-step__body">
|
||||
<label class="gx-stack"><span>Play on</span>
|
||||
<label class="gx-stack"><span>Send audio to</span>
|
||||
<select class="gx-field" :value="setup.preferences.output === 'bluetooth' ? setup.preferences.outputAddress : setup.preferences.output" :disabled="busy || !setup.parked" @change="chooseOutput">
|
||||
<option value="" disabled>Choose…</option>
|
||||
<option value="starport">StarPort (car audio)</option>
|
||||
<optgroup label="Bluetooth">
|
||||
<option value="" disabled>Choose an audio output…</option>
|
||||
<option value="starport">Car speakers via StarPort (Android Auto)</option>
|
||||
<optgroup label="Bluetooth car stereos and speakers">
|
||||
<option v-for="choice in outputChoices" :key="choice.value" :value="choice.value">{{ choice.label }}</option>
|
||||
</optgroup>
|
||||
<option value="test">Silent test (no speaker)</option>
|
||||
</select></label>
|
||||
<p class="gx-note">Only Bluetooth devices that support receiving music appear here. Pairing a phone or radar detector does not make it an audio output.</p>
|
||||
<small v-if="!setup.parked" class="gx-note">Put the car in Park or use offroad mode to change this.</small>
|
||||
<p v-if="setup.preferences.output === 'starport'" class="gx-note">Music plays through the car while <a class="gx-link" href="#/starport">StarPort</a> is connected, and the car shows it as Android Auto audio. If you switch the car to the radio, playback is paused when the source supports it.</p>
|
||||
<p v-else-if="!outputChoices.length" class="gx-note">No paired devices yet. Pair your car or speaker on the <a class="gx-link" href="#/bluetooth">Bluetooth</a> page first.</p>
|
||||
<p v-else-if="!outputChoices.length" class="gx-note">No paired Bluetooth audio outputs found. Pair your car stereo or speaker on the <a class="gx-link" href="#/bluetooth">Bluetooth</a> page first.</p>
|
||||
<p v-else-if="setup.preferences.output === 'bluetooth'" class="gx-note">Don’t see it? Pair it on the <a class="gx-link" href="#/bluetooth">Bluetooth</a> page. In your car, choose <strong>Bluetooth Audio</strong> as the source.</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -638,7 +645,7 @@ export const SpotifyPage = {
|
||||
<p>Music plays through your car or speaker; alerts keep playing from the comma’s own speaker. Keep the music at a level where you can still hear the comma.</p>
|
||||
</details>
|
||||
<details class="gx-starport-wiki"><summary>Can I use it with Android Auto?</summary>
|
||||
<p>Yes. In step 3 choose <strong>Android Auto</strong>, and music plays through the car as Android Auto audio alongside the StarPilot screen. It needs Android Auto to be connected, so if it isn’t, music has nowhere to play.</p>
|
||||
<p>Yes. Under <strong>Send audio to</strong>, choose <strong>Car speakers via StarPort (Android Auto)</strong>, and music plays through the car as Android Auto audio alongside the StarPilot screen. It needs Android Auto to be connected, so if it isn’t, music has nowhere to play.</p>
|
||||
</details>
|
||||
<details class="gx-starport-wiki"><summary>More help from Spotify</summary>
|
||||
<p><a class="gx-link" :href="setup?.links.help || '#'" target="_blank" rel="noopener noreferrer">Spotify’s Soloist getting-started guide</a></p>
|
||||
|
||||
@@ -73,7 +73,7 @@ class Launch:
|
||||
def runtime_paths(layout: Layout) -> dict[str, Path]:
|
||||
run = layout.runtime
|
||||
return {'run': run, 'home': run / 'home', 'pulse': run / 'pulse', 'socket': run / 'pulse' / 'native', 'alsa': run / 'alsa.conf',
|
||||
'starport': run / 'starport.pcm'}
|
||||
'starport': run / 'starport.pcm', 'daemon': run / 'daemon.conf'}
|
||||
|
||||
|
||||
def starport_pipe(layout: Layout | None = None) -> Path:
|
||||
@@ -98,8 +98,9 @@ def server_launch(layout: Layout, prefs: Preferences, *, silent: bool = False, a
|
||||
env = {
|
||||
'HOME': str(paths['home']), 'XDG_RUNTIME_DIR': str(paths['run']), 'XDG_CONFIG_HOME': str(paths['home']),
|
||||
'PULSE_RUNTIME_PATH': str(paths['pulse']), 'PULSE_STATE_PATH': str(paths['pulse']),
|
||||
'LD_LIBRARY_PATH': library_path(root), 'PATH': '/usr/bin:/bin',
|
||||
'LD_LIBRARY_PATH': library_path(root), 'PATH': '/usr/bin:/bin', 'PULSE_CONFIG': str(paths['daemon']),
|
||||
}
|
||||
paths['daemon'].write_text('nice-level = -5\n')
|
||||
if prefs.output == 'bluetooth':
|
||||
# Written even when starting silent, so the speaker can be added to the running server later.
|
||||
paths['alsa'].write_text(alsa_config(prefs.output_address, alsa_base))
|
||||
@@ -109,8 +110,8 @@ def server_launch(layout: Layout, prefs: Preferences, *, silent: bool = False, a
|
||||
paths['starport'].unlink(missing_ok=True)
|
||||
sink = ' '.join(_sink('silent' if silent else prefs.output, paths))
|
||||
argv = [str(root / 'bin' / 'pulseaudio'), '--daemonize=no', '--system=no', '--use-pid-file=no', '-n',
|
||||
'--exit-idle-time=-1', '--realtime=no', '--high-priority=no', '--disallow-exit', '--log-target=stderr',
|
||||
'--log-level=notice', f'--dl-search-path={root / "modules"}',
|
||||
'--exit-idle-time=-1', '--realtime=no', '--high-priority=yes', '--disallow-exit', '--log-target=stderr',
|
||||
'--log-level=notice', '--resample-method=speex-float-3', f'--dl-search-path={root / "modules"}',
|
||||
'-L', f'module-native-protocol-unix socket={paths["socket"]} auth-anonymous=1', '-L', sink]
|
||||
return Launch(argv, env)
|
||||
|
||||
|
||||
@@ -602,7 +602,7 @@ def parked() -> bool:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.nice(10) # music yields to everything driving-related; children inherit this
|
||||
os.nice(5) # music yields to driving-related work; the audio server sets its own priority
|
||||
supervisor = Supervisor()
|
||||
reap_orphans(supervisor.layout)
|
||||
legacy_profile = Path('/run/systemd/system/bluealsa.service.d/90-starpilot-phone.conf')
|
||||
|
||||
@@ -61,6 +61,10 @@ def test_launch_commands_isolate_the_server_and_player(layout):
|
||||
prefs = preferences.Preferences(output_address='AA:BB:CC:DD:EE:FF', initial_volume=60)
|
||||
server = backend.server_launch(layout, prefs, alsa_base=Path('/usr/share/alsa/alsa.conf'))
|
||||
assert '-n' in server.argv and '--system=no' in server.argv
|
||||
assert '--realtime=no' in server.argv and '--high-priority=yes' in server.argv
|
||||
assert '--resample-method=speex-float-3' in server.argv
|
||||
assert (layout.runtime / 'daemon.conf').read_text() == 'nice-level = -5\n'
|
||||
assert server.env['PULSE_CONFIG'] == str(layout.runtime / 'daemon.conf')
|
||||
assert any(arg.startswith('module-alsa-sink device=starpilot_spotify') for arg in server.argv)
|
||||
assert server.env['ALSA_CONFIG_PATH'].startswith(str(layout.runtime))
|
||||
assert (layout.runtime / 'alsa.conf').stat().st_mode & 0o777 == 0o600
|
||||
|
||||
@@ -374,6 +374,7 @@ class VASMRuntime:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.nice(max(0, 10 - os.getpriority(os.PRIO_PROCESS, 0)))
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.cereal.visionipc import VisionStreamType
|
||||
from msgq.visionipc import VisionIpcClient
|
||||
|
||||
Reference in New Issue
Block a user