Refine C3 Disengage Sound

This commit is contained in:
firestar5683
2026-10-25 23:00:35 -05:00
parent 87b42f9127
commit 6413fef0f9
3 changed files with 120 additions and 1 deletions
+25
View File
@@ -264,6 +264,29 @@ def tici_speaker_samples(samples: np.ndarray) -> np.ndarray:
return output
def tici_disengage_samples(samples: np.ndarray) -> np.ndarray:
"""Lighten the existing disengage chime at load time, preserving its timing."""
taps = np.arange(2049, dtype=np.float64) - 1024
lowpass = 2 * 700 / 48000 * np.sinc(2 * 700 / 48000 * taps) * np.hamming(len(taps))
lowpass /= lowpass.sum()
bass = np.convolve(samples, lowpass, mode="full")[1024:1024 + len(samples)]
result = samples.astype(np.float64) - 0.65 * bass
elapsed = np.arange(len(samples)) / 48000
result *= np.exp(-np.maximum(elapsed - 0.25, 0))
fade_frames = min(240, len(samples) // 2)
if fade_frames:
fade = 0.5 - 0.5 * np.cos(np.linspace(0, np.pi, fade_frames))
result[:fade_frames] *= fade
result[-fade_frames:] *= fade[::-1]
original_peak = float(np.max(np.abs(samples)))
filtered_peak = float(np.max(np.abs(result)))
if filtered_peak > original_peak:
result *= original_peak / filtered_peak
output = result.astype(np.float32)
output.flags.writeable = False
return output
class SoundPackLoader:
def __init__(self, params, stock: Path, root: Path = PACK_ROOT, *, device_type: str = ""):
self.device_type = device_type
@@ -307,6 +330,8 @@ class SoundPackLoader:
builtin_loaded[builtin] = read_wav(self.stock.parent / "sounds_starpilot" / builtin)
if self.device_type == "tici":
builtin_loaded[builtin] = tici_speaker_samples(builtin_loaded[builtin])
if builtin == "disengage.wav":
builtin_loaded[builtin] = tici_disengage_samples(builtin_loaded[builtin])
selected = builtin_loaded[builtin]
self.builtin_sounds[builtin] = selected
except (OSError, EOFError, ValueError, wave.Error):
@@ -1,5 +1,6 @@
from pathlib import Path
import tempfile
import shutil
import unittest
from unittest.mock import patch
import wave
@@ -10,6 +11,7 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.selfdrive.ui.soundd import AudibleAlert, Soundd, sound_list
from openpilot.starpilot.audio.sound_pack import BUILTIN_FILES, MAX_WAV_BYTES, SoundPackLoader, installed_packs, read_wav, tici_speaker_samples
from openpilot.starpilot.audio.sound_pack import tici_disengage_samples
from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest
from openpilot.starpilot.ui.sounds_owner import SoundsOwner
@@ -267,3 +269,95 @@ def test_tici_speaker_targets_reported_resonance_without_cutting_chimes():
assert amplitudes[frequency] < 0.05
for frequency in (880, 1200, 1570):
assert 0.95 < amplitudes[frequency] <= 1.001
ASSETS = Path(__file__).resolve().parents[3] / 'selfdrive/assets'
class FileSelection:
def __init__(self, root):
self.root = root
def get_param_path(self, name):
return str(self.root / name)
class TiciDisengageTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.params = FileSelection(self.root)
self.stock = ASSETS / 'sounds'
self.packs = self.root / 'packs'
def test_only_tici_builtin_disengage_gets_additional_refinement(self):
names = ('engage.wav', 'disengage.wav', 'warning.wav', 'critical.wav', 'dm_critical.wav')
for device in ('tici', 'tizi', 'mici', 'pc', ''):
with self.subTest(device=device):
loaded = SoundPackLoader(self.params, self.stock, self.packs, device_type=device).refresh(names, 0)
assert loaded is not None
for name in names:
expected = read_wav(ASSETS / 'sounds_starpilot' / BUILTIN_FILES[name])
if device == 'tici':
expected = tici_speaker_samples(expected)
if name == 'disengage.wav':
expected = tici_disengage_samples(expected)
np.testing.assert_array_equal(loaded[name], expected)
self.assertFalse(loaded[name].flags.writeable)
def test_stock_selection_bypasses_refinement(self):
(self.root / 'SoundPack').write_bytes(b'stock')
with patch('openpilot.starpilot.audio.sound_pack.tici_disengage_samples', side_effect=AssertionError('stock EQ')):
loaded = SoundPackLoader(self.params, self.stock, self.packs, device_type='tici').refresh(('disengage.wav',), 0)
assert loaded is not None
np.testing.assert_array_equal(loaded['disengage.wav'], read_wav(self.stock / 'disengage.wav'))
def test_custom_selection_bypasses_refinement(self):
(self.root / 'SoundPack').write_bytes(b'custom')
directory = self.packs / 'custom/sounds'
directory.mkdir(parents=True)
shutil.copyfile(ASSETS / 'sounds_starpilot/disengage.wav', directory / 'disengage.wav')
with patch('openpilot.starpilot.audio.sound_pack.tici_disengage_samples', side_effect=AssertionError('custom EQ')):
loaded = SoundPackLoader(self.params, self.stock, self.packs, device_type='tici').refresh(('disengage.wav',), 0)
assert loaded is not None
np.testing.assert_array_equal(loaded['disengage.wav'], read_wav(directory / 'disengage.wav'))
def test_refinement_keeps_shape_peak_and_input_and_reduces_low_body_and_tail(self):
samples = tici_speaker_samples(read_wav(ASSETS / 'sounds_starpilot/disengage.wav'))
original = samples.copy()
output = tici_disengage_samples(samples)
self.assertEqual(output.shape, samples.shape)
self.assertEqual(output.dtype, np.float32)
self.assertFalse(output.flags.writeable)
self.assertEqual(output[0], 0.)
self.assertEqual(output[-1], 0.)
self.assertTrue(np.isfinite(output).all())
self.assertLessEqual(np.max(np.abs(output)), np.max(np.abs(samples)))
np.testing.assert_array_equal(samples, original)
frequencies = np.fft.rfftfreq(len(samples), 1 / 48000)
low = frequencies < 600
before, after = (np.abs(np.fft.rfft(x)) ** 2 for x in (samples, output))
self.assertLess(after[low].sum(), before[low].sum() * .3)
self.assertLess(np.mean(output[-9600:] ** 2), np.mean(samples[-9600:] ** 2) * .7)
upper = frequencies > 750
self.assertGreater(np.corrcoef(np.log1p(before[upper]), np.log1p(after[upper]))[0, 1], .98)
def test_silent_and_short_clips_remain_finite_and_same_length(self):
for samples in (np.zeros(1, np.float32), np.zeros(512, np.float32), np.array([.1, -.2, .3], np.float32)):
output = tici_disengage_samples(samples)
self.assertEqual(output.shape, samples.shape)
self.assertTrue(np.isfinite(output).all())
self.assertLessEqual(np.max(np.abs(output)), np.max(np.abs(samples)))
def test_refinement_is_cached_at_load_time(self):
loader = SoundPackLoader(self.params, self.stock, self.packs, device_type='tici')
with patch('openpilot.starpilot.audio.sound_pack.tici_disengage_samples', wraps=tici_disengage_samples) as refine:
loaded = loader.refresh(('disengage.wav',), 0)
assert loaded is not None
prepared = loaded['disengage.wav']
self.assertEqual(refine.call_count, 1)
for timestamp in (.1, .5, 1.1, 2.2):
self.assertIsNone(loader.refresh(('disengage.wav',), timestamp))
self.assertEqual(refine.call_count, 1)
self.assertIs(loader.builtin_sounds['disengage.wav'], prepared)
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "5770347ea29c839e6b641fb1b8294b41ab3fad332653e7200e65a660ada1f9a2", "schema_version": 1}
{"protected_inventory_sha256": "dbee1560539abe36c96e7f5f96f2a5fc4506c76a33028e507c924d6295c0729b", "schema_version": 1}