soundd: mix ClearPilot ding alongside alerts; add ding.wav

Adds the ding sound effect that fires on speed-limit changes / cruise-
warning transitions (writers will set ClearpilotPlayDing="1" in
/dev/shm/params; soundd polls at ~2Hz, clears the flag, plays once).

The ding plays at MAX_VOLUME independent of the alert volume map and
mixes into the same output stream. Single-shot — no looping. Loaded
once at startup; failure to load just disables the ding (rest of soundd
keeps working).

print-to-stderr, not cloudlog (CLAUDE.md rule).
This commit is contained in:
2026-05-03 22:50:46 -05:00
parent f687d712e7
commit 5d2fe5248b
2 changed files with 45 additions and 1 deletions
Binary file not shown.
+45 -1
View File
@@ -93,6 +93,29 @@ class Soundd:
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False) self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
# CLEARPILOT ding (plays independently of alerts; triggered by writers
# setting ClearpilotPlayDing="1" in /dev/shm/params).
self.ding_sound = None
self.ding_frame = 0
self.ding_playing = False
self.ding_check_counter = 0
self._load_ding()
def _load_ding(self):
import sys
ding_path = BASEDIR + "/selfdrive/clearpilot/sounds/ding.wav"
try:
wavefile = wave.open(ding_path, 'r')
assert wavefile.getnchannels() == 1
assert wavefile.getsampwidth() == 2
assert wavefile.getframerate() == SAMPLE_RATE
length = wavefile.getnframes()
self.ding_sound = np.frombuffer(wavefile.readframes(length), dtype=np.int16).astype(np.float32) / (2**16/2)
print(f"CLP soundd: ClearPilot ding loaded: {length} frames", file=sys.stderr, flush=True)
except Exception as e:
print(f"CLP soundd: failed to load ding sound: {e}", file=sys.stderr, flush=True)
self.ding_sound = None
def load_sounds(self): def load_sounds(self):
self.loaded_sounds: dict[int, np.ndarray] = {} self.loaded_sounds: dict[int, np.ndarray] = {}
@@ -137,7 +160,20 @@ class Soundd:
written_frames += frames_to_write written_frames += frames_to_write
self.current_sound_frame += frames_to_write self.current_sound_frame += frames_to_write
return ret * self.current_volume ret = ret * self.current_volume
# CLEARPILOT: mix in ding (independent of alerts, always max volume)
if self.ding_playing and self.ding_sound is not None:
ding_remaining = len(self.ding_sound) - self.ding_frame
if ding_remaining > 0:
frames_to_write = min(ding_remaining, frames)
ret[:frames_to_write] += self.ding_sound[self.ding_frame:self.ding_frame + frames_to_write] * MAX_VOLUME
self.ding_frame += frames_to_write
else:
self.ding_playing = False
self.ding_frame = 0
return ret
def callback(self, data_out: np.ndarray, frames: int, time, status) -> None: def callback(self, data_out: np.ndarray, frames: int, time, status) -> None:
if status: if status:
@@ -205,6 +241,14 @@ class Soundd:
if self.params_memory.get_bool("FrogPilotTogglesUpdated"): if self.params_memory.get_bool("FrogPilotTogglesUpdated"):
self.update_frogpilot_params() self.update_frogpilot_params()
# CLEARPILOT: poll ClearpilotPlayDing at ~2Hz; clear it on read.
self.ding_check_counter += 1
if self.ding_check_counter % 10 == 0 and self.ding_sound is not None:
if self.params_memory.get("ClearpilotPlayDing") == b"1":
self.params_memory.put("ClearpilotPlayDing", "0")
self.ding_playing = True
self.ding_frame = 0
def update_frogpilot_params(self): def update_frogpilot_params(self):
self.alert_volume_control = self.params.get_bool("AlertVolumeControl") self.alert_volume_control = self.params.get_bool("AlertVolumeControl")