Files
StarPilot/openpilot/starpilot/audio/tests/test_soundd_output.py
T
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

128 lines
5.7 KiB
Python

"""Actual soundd load, alert service, stream configuration and audio callback."""
from pathlib import Path
from types import SimpleNamespace as NS
from unittest.mock import patch
import numpy as np
import pytest
from openpilot.common.params import Params
from openpilot.selfdrive.ui import soundd
from openpilot.starpilot.audio.sound_pack import DEFAULT_PACK, SoundPackLoader, read_wav
@pytest.mark.parametrize("device,ambient,base", [("tici", 30, 10), ("tizi", 30, 10), ("mici", 22, 20)])
def test_device_ambient_volume_curve(device, ambient, base):
import importlib
try:
with patch.object(soundd.HARDWARE, "get_device_type", return_value=device):
importlib.reload(soundd)
daemon = soundd.Soundd.__new__(soundd.Soundd)
for db in (0, ambient, ambient + 15, ambient + 30, 100):
fraction = max(0., min(1., (db - ambient) / 30))
expected = base ** (0.1 + fraction * 0.9 - 1)
assert daemon.calculate_volume(db) == pytest.approx(expected)
finally:
importlib.reload(soundd)
@pytest.mark.parametrize("raw", [None, b"", b"default", b"Frog", b"frogpilot", b"missing", b"\xff"])
def test_default_repair_persists_and_flows_through_actual_stream(tmp_path, raw):
params = Params(str(tmp_path / "params"))
if raw is not None:
Path(params.get_param_path("SoundPack")).write_bytes(raw)
daemon = soundd.Soundd(params, pack_root=tmp_path / "packs")
assert params.get("SoundPack") == DEFAULT_PACK
root = Path(soundd.__file__).resolve().parents[1] / "assets/sounds_starpilot"
class Stream:
def __init__(self, **kw):
self.kw = kw
device = NS(_terminate=lambda: None, _initialize=lambda: None, OutputStream=Stream)
stream = daemon.get_stream(device)
assert stream.kw["samplerate"] == 48000 and stream.kw["channels"] == 1
assert stream.kw["blocksize"] == 4096
for alert in (soundd.AudibleAlert.engage, soundd.AudibleAlert.disengage,
soundd.AudibleAlert.prompt, soundd.AudibleAlert.warningSoft, soundd.AudibleAlert.warningImmediate):
daemon.current_alert = soundd.AudibleAlert.none
class State:
updated = {"selfdriveState": True}
logMonoTime = {}
valid = {"selfdriveState": True}
alive = {"selfdriveState": True}
def __init__(self, sound):
self.sound = sound
def __getitem__(self, service):
return NS(alertSound=NS(raw=self.sound))
daemon.get_audible_alert(State(alert))
daemon.current_volume = .5
output = np.zeros((4096, 1), np.float32)
with patch("pathlib.Path.open", side_effect=AssertionError("callback file IO")):
stream.kw["callback"](output, 4096, None, None)
assert np.isfinite(output).all() and np.max(np.abs(output)) <= .5 and np.any(output != 0)
logical = soundd.sound_list[alert][0]
from openpilot.starpilot.audio.sound_pack import BUILTIN_FILES
expected = read_wav(root / BUILTIN_FILES[logical])
np.testing.assert_array_equal(output[:, 0], expected[:4096] * .5)
def test_warning_assets_are_distinct_and_loop_finishes(tmp_path):
daemon = soundd.Soundd(Params(str(tmp_path / "params")), pack_root=tmp_path / "packs")
alerts = (soundd.AudibleAlert.promptRepeat, soundd.AudibleAlert.warningSoft, soundd.AudibleAlert.warningImmediate)
rms, peak, waveforms = [], [], []
for alert in alerts:
samples = daemon.loaded_sounds[alert]
rms.append(float(np.sqrt(np.mean(samples ** 2))))
peak.append(float(np.max(np.abs(samples))))
assert np.isfinite(samples).all() and 0 < rms[-1] <= peak[-1] < 1
waveforms.append(samples.tobytes())
daemon.update_alert(alert)
daemon.current_sound_frame, daemon.current_volume = 0, 1.
output = np.zeros((len(samples) * 2 + 4096, 1), np.float32)
daemon.callback(output, len(output), None, None)
np.testing.assert_array_equal(output[:len(samples), 0], samples)
np.testing.assert_array_equal(output[len(samples):2*len(samples), 0], samples)
daemon.update_alert(soundd.AudibleAlert.none)
tail = np.zeros((len(samples), 1), np.float32)
daemon.callback(tail, len(tail), None, None)
assert daemon.current_alert == soundd.AudibleAlert.none
output.fill(1)
daemon.callback(output, len(output), None, None)
assert not np.any(output)
assert len(set(waveforms)) == len(alerts)
def test_builtin_loading_does_not_require_stock_and_damaged_clip_uses_stock(tmp_path):
from openpilot.starpilot.audio.sound_pack import BUILTIN_FILES
params = Params(str(tmp_path / "params"))
params.put("SoundPack", DEFAULT_PACK, block=True)
actual = Path(soundd.__file__).resolve().parents[1] / "assets"
stock = tmp_path / "assets/sounds"
builtin = stock.parent / "sounds_starpilot"
builtin.mkdir(parents=True)
for name in set(BUILTIN_FILES.values()):
(builtin / name).write_bytes((actual / "sounds_starpilot" / name).read_bytes())
loader = SoundPackLoader(params, stock, tmp_path / "packs")
loaded = loader.refresh(("engage.wav", "warning.wav"), 0)
assert loaded is not None
assert np.any(loaded["engage.wav"])
stock.mkdir()
(stock / "engage.wav").write_bytes((actual / "sounds/engage.wav").read_bytes())
(builtin / "engage.wav").write_text("version https://git-lfs.github.com/spec/v1\n")
restored = loader.refresh(("engage.wav", "warning.wav"), 1)
assert restored is not None
np.testing.assert_array_equal(restored["engage.wav"], read_wav(stock / "engage.wav"))
np.testing.assert_array_equal(restored["warning.wav"], loaded["warning.wav"])
@pytest.mark.parametrize("name", ["stock", "custom"])
def test_valid_explicit_pack_survives_restart(tmp_path, name):
params = Params(str(tmp_path / "params"))
packs = tmp_path / "packs"
custom = packs / "custom/sounds"
custom.mkdir(parents=True)
params.put("SoundPack", name, block=True)
soundd.Soundd(params, pack_root=packs)
soundd.Soundd(params, pack_root=packs)
assert params.get("SoundPack") == name