diff --git a/openpilot/sunnypilot/selfdrive/ui/custom_audio.py b/openpilot/sunnypilot/selfdrive/ui/custom_audio.py index dcb354ddc2..1654ded544 100644 --- a/openpilot/sunnypilot/selfdrive/ui/custom_audio.py +++ b/openpilot/sunnypilot/selfdrive/ui/custom_audio.py @@ -11,8 +11,7 @@ AUDIO_DIR = Path("/data/media/0/custom_audio") SAMPLE_RATE = 48000 MAX_SECONDS = 300 GEAR_TIMEOUT = 0.5 -DUCK_FRAMES = int(0.05 * SAMPLE_RATE) -RESTORE_FRAMES = int(0.15 * SAMPLE_RATE) +RESUME_FRAMES = int(0.15 * SAMPLE_RATE) class GearTransition: @@ -55,6 +54,7 @@ class AudioPlayer: self.samples = np.empty(0, dtype=np.float32) self.position = 0 self.gain = 1.0 + self.paused = False def render(self, frames, alert_active): command = self.command @@ -63,15 +63,23 @@ class AudioPlayer: self.samples = command if command is not None else np.empty(0, dtype=np.float32) self.position = 0 self.gain = 1.0 + self.paused = False out = np.zeros(frames, dtype=np.float32) if self.position == len(self.samples): return out + if alert_active: + self.paused = True + self.gain = 0.0 + return out + if self.paused: + self.paused = False + self.gain = 0.0 n = min(frames, len(self.samples) - self.position) - step = -0.5 / DUCK_FRAMES if alert_active else 0.5 / RESTORE_FRAMES - gains = np.clip(self.gain + np.arange(n) * step, 0.5, 1.0) + gains = np.minimum(1, self.gain + np.arange(n) / RESUME_FRAMES) out[:n] = self.samples[self.position:self.position + n] * gains self.position += n - self.gain = float(np.clip(self.gain + n * step, 0.5, 1.0)) + if n: + self.gain = float(gains[-1]) return out diff --git a/openpilot/sunnypilot/selfdrive/ui/docs/custom_audio.md b/openpilot/sunnypilot/selfdrive/ui/docs/custom_audio.md index 095fb789ca..5586019ad4 100644 --- a/openpilot/sunnypilot/selfdrive/ui/docs/custom_audio.md +++ b/openpilot/sunnypilot/selfdrive/ui/docs/custom_audio.md @@ -7,8 +7,7 @@ Each gear change plays `.wav`: `park.wav`, `drive.wav`, `sport.wav`, `reve - Entering a gear starts its audio from the beginning at full volume, with no fade-in. - Changing gears cuts the previous audio without a fade-out. If the new file is missing or invalid, playback stays silent. - Returning to a previous gear restarts its audio from the beginning. Staying in the same gear does not replay it. -- While any alert sounds, custom audio keeps playing and fades to **50% volume over 50 ms**. Afterward it fades back to full volume over **150 ms**. -- Alert volume is unchanged. Custom audio may be reduced further to prevent clipping when both sounds are loud. +- Safety alerts pause custom audio immediately. After the alert, it resumes from its saved position with a **150 ms fade-in**. Audio triggered during an alert waits, then fades in. - Startup and recovery from unknown/invalid gear data or data gaps establish a silent baseline. No settings or dismiss control. Replace or remove files to change future playback. diff --git a/openpilot/sunnypilot/selfdrive/ui/soundd.py b/openpilot/sunnypilot/selfdrive/ui/soundd.py index f2e936376a..e75d8f45e0 100644 --- a/openpilot/sunnypilot/selfdrive/ui/soundd.py +++ b/openpilot/sunnypilot/selfdrive/ui/soundd.py @@ -1,7 +1,7 @@ import numpy as np from openpilot.cereal import messaging -from openpilot.selfdrive.ui.soundd import Soundd +from openpilot.selfdrive.ui.soundd import AudibleAlert, Soundd from openpilot.sunnypilot.selfdrive.ui.custom_audio import CustomAudio @@ -12,13 +12,12 @@ class SounddSP(Soundd): self.gear_sock = messaging.sub_sock("carState", conflate=False) def callback(self, data_out: np.ndarray, frames: int, time, status) -> None: + alert_active = self.current_alert != AudibleAlert.none super().callback(data_out, frames, time, status) - alert_data = data_out[:frames, 0] - alert_peak = float(np.max(np.abs(alert_data))) - custom_data = self.custom_audio.player.render(frames, alert_peak > 0) - custom_peak = float(np.max(np.abs(custom_data))) - gain = min(1.0, max(0.0, 1.0 - alert_peak) / max(custom_peak, 1e-9)) - alert_data += custom_data * gain + alert_active = alert_active or self.current_alert != AudibleAlert.none or bool(np.any(data_out[:frames, 0])) + custom_data = self.custom_audio.player.render(frames, alert_active) + if not alert_active: + data_out[:frames, 0] = custom_data def get_audible_alert(self, sm): super().get_audible_alert(sm)