mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-08 03:03:43 +08:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f12b9e0ae9 | |||
| e6cce08ff6 | |||
| d02db967fe | |||
| 63c725d507 | |||
| 2f3f1d76a5 | |||
| f3cc96b0a7 | |||
| 12f2c30c14 | |||
| 0999b0cbe9 | |||
| ed1121e61c | |||
| 50d7f75bfc | |||
| 15c7f52e40 | |||
| 8dfe04a318 | |||
| 9648ac2f04 | |||
| a03a11e333 | |||
| 53b1070b09 | |||
| 68bf6d8162 | |||
| 8b472291d0 | |||
| 797c8d4293 | |||
| 7568528507 | |||
| 6665c3acf6 | |||
| 3b13eb4d0d | |||
| f7eb4c2561 | |||
| be9255358c | |||
| f48ccb5d7a | |||
| 99559d6749 | |||
| 4b6f0ffb46 | |||
| 55415382ce | |||
| 9f30901eb6 | |||
| d2045d24fb | |||
| 6488a11e4d | |||
| 7b22b65313 | |||
| b59a50101e | |||
| 3e30962bd3 | |||
| 3274043063 | |||
| c624550d2e | |||
| 6759038671 | |||
| aa636e75c8 | |||
| 5ff3c3bd8d | |||
| 1e11b52290 | |||
| 76e0204025 | |||
| cc88f2cbd6 | |||
| ee0ac199a4 | |||
| d5ed828eaa | |||
| 9fca585f2a | |||
| bb1259303e | |||
| e135051ca8 | |||
| 12bef55d8a | |||
| 2f7a45e6c8 | |||
| 936ebfc12b | |||
| aa0c9dc0eb | |||
| 7476a866e7 | |||
| 610d857e33 | |||
| d2f47407d0 | |||
| db75ec76ea | |||
| 24066465d7 | |||
| a7abbd6e25 | |||
| 878982447c | |||
| 576527a36b | |||
| ef8c35da24 | |||
| 85688b1040 | |||
| 0fb2199130 | |||
| d48d756c1d | |||
| 2ed298a0c9 | |||
| d68f038949 | |||
| 7231571e57 | |||
| b37f1419d3 | |||
| cd85a66790 | |||
| 305ea87daf | |||
| 4bbfc793e0 | |||
| d5d983676e | |||
| de8a96a398 | |||
| 0cbf45f699 | |||
| 0d68a3a2ab | |||
| 9e85a85059 | |||
| 0373c327c0 | |||
| efe9e5c200 | |||
| 8a249a45dc | |||
| bdbefe67f6 | |||
| 675bb166ad | |||
| 1b717a7e88 | |||
| 86f55a8ba9 | |||
| 629392d2f7 | |||
| bc414bdc8b | |||
| 7ca5649f2c | |||
| 641ee8fa87 | |||
| 56c276158c | |||
| c65308a8bd | |||
| 994e526460 | |||
| 1defae36b7 | |||
| 8f029fd0ef | |||
| ddb46284dc | |||
| 9effc754d9 | |||
| e49ffc2a2d | |||
| 2cacd0b3e5 | |||
| c4b8859dff | |||
| 8fb0953205 | |||
| 63d1c8835f | |||
| 17a185606d | |||
| da10131392 | |||
| 7107c2ba14 | |||
| 95b6e877ac | |||
| eb02c6570e | |||
| 1be8ae31c4 | |||
| 04dcd38856 | |||
| 22ccf0d72f | |||
| 3c969bb627 | |||
| 20f8011feb | |||
| 9cf17e74a1 | |||
| 2c4efdf557 | |||
| 4cd3d3c16c | |||
| 637f3ae9c8 | |||
| 464ee80f71 | |||
| 2743a04613 | |||
| 7f9978d001 | |||
| 4b83961c67 | |||
| c00eaf428a | |||
| 0a9993e8d4 | |||
| 0af214a985 | |||
| af43385e3a | |||
| 0ab2b8c590 | |||
| 67ab18a0de | |||
| e87dc15b30 | |||
| 192d08516c | |||
| 3cf001c59c | |||
| f2ccd021da | |||
| c9fc900f64 | |||
| 3c37c5ce5d | |||
| 7c45889e4e | |||
| 2aabb7aee8 | |||
| 3859e9962f | |||
| 810efbab72 | |||
| ec27bec326 | |||
| 250d553157 | |||
| cea54a0ca8 | |||
| 8e72d783bd | |||
| 1b0dc103dc | |||
| 6c364d292b | |||
| bcdec2ce84 | |||
| 3deaeb3759 | |||
| c669f0984a | |||
| 46dd946740 | |||
| 9da4b3653e | |||
| 4e21ae7c50 | |||
| bb91e92237 | |||
| 14b4c4f85b | |||
| 0660b542c3 | |||
| 2b893b90c9 | |||
| f5139178ed | |||
| fb43b755f2 | |||
| 07f5b967d8 | |||
| ea19c7d3bb | |||
| e461842cbb | |||
| a73c9659d5 | |||
| cb796fbc76 | |||
| 6bf75fc557 | |||
| 9a1fc28819 | |||
| 0741d05e92 | |||
| 1ad008107d | |||
| feebd9df93 | |||
| c2e5ced3e5 | |||
| 15e5d2efb9 | |||
| a3929d0b54 | |||
| 794f8f9991 | |||
| 68fa5e3f21 | |||
| 86c6cc1f48 | |||
| eb7ffbf093 | |||
| 3919095752 | |||
| 74d63be1c3 | |||
| 8894486a1a | |||
| 810599315d | |||
| 6f3ab810c8 | |||
| 230f78b8d3 | |||
| f1affec088 | |||
| 97d8ef242c | |||
| a63fff9b45 | |||
| cb3893daaa | |||
| 29f60df74b | |||
| c6c072e1f4 | |||
| d101cbb83e | |||
| 1536d59633 | |||
| dc99b865ae | |||
| e59bc027ff | |||
| cf7e5efaca | |||
| 4b44f2eb31 | |||
| 107d2ab400 | |||
| 5432d9062c | |||
| f533f6c843 | |||
| 58e9ac763c | |||
| cb50d54169 | |||
| bd5de4ed0a | |||
| 0d4073fadb | |||
| ebc70dcb52 | |||
| 4d0426999e | |||
| 286da42573 | |||
| 8a836710a9 | |||
| 5d515bcf33 | |||
| 1c7f6d5133 | |||
| 05d57c7aeb | |||
| e4b0eaf352 | |||
| a710276472 | |||
| af086db671 | |||
| 0d9eb0e25e | |||
| 0616caed6d | |||
| 095337b3c1 | |||
| 1edec2d22c | |||
| affabb9ee0 | |||
| dc27e8711c | |||
| cf7329a264 | |||
| 5ee5ecd820 | |||
| b064f730dd |
+1
-1
Submodule opendbc_repo updated: f95f996f59...f9f1223de9
@@ -211,7 +211,9 @@ class Soundd(QuietMode):
|
||||
|
||||
|
||||
def main():
|
||||
s = Soundd()
|
||||
from openpilot.sunnypilot.selfdrive.ui.soundd import SounddSP
|
||||
|
||||
s = SounddSP()
|
||||
s.soundd_thread()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import wave
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class GearTransition:
|
||||
def __init__(self):
|
||||
self.previous = None
|
||||
self.last_time = None
|
||||
|
||||
def update(self, gear, timestamp, valid=True):
|
||||
if not valid or gear == "unknown":
|
||||
self.previous = self.last_time = None
|
||||
return None
|
||||
if self.last_time is None or not 0 < timestamp - self.last_time <= GEAR_TIMEOUT:
|
||||
self.previous = None
|
||||
transition = gear if self.previous is not None and gear != self.previous else None
|
||||
self.previous = gear
|
||||
self.last_time = timestamp
|
||||
return transition
|
||||
|
||||
|
||||
def lower_loader_priority():
|
||||
if sys.platform == "linux":
|
||||
try:
|
||||
tid = threading.get_native_id()
|
||||
os.sched_setscheduler(tid, os.SCHED_OTHER, os.sched_param(0))
|
||||
os.setpriority(os.PRIO_PROCESS, tid, max(10, os.getpriority(os.PRIO_PROCESS, tid)))
|
||||
except OSError:
|
||||
cloudlog.exception("Unable to lower custom audio loader priority")
|
||||
|
||||
|
||||
def load_audio(filename):
|
||||
path = AUDIO_DIR / filename
|
||||
if not path.is_file():
|
||||
return None
|
||||
with wave.open(str(path), "rb") as wav:
|
||||
if (wav.getnchannels(), wav.getsampwidth(), wav.getframerate(), wav.getcomptype()) != (1, 2, SAMPLE_RATE, "NONE"):
|
||||
raise ValueError("Custom audio requires 48 kHz mono 16-bit PCM WAV")
|
||||
count = wav.getnframes()
|
||||
if not 0 < count <= MAX_SECONDS * SAMPLE_RATE:
|
||||
raise ValueError("Custom audio must be between 0 and 300 seconds")
|
||||
raw = wav.readframes(count)
|
||||
if len(raw) != count * 2:
|
||||
raise ValueError("Truncated custom audio")
|
||||
return np.frombuffer(raw, dtype="<i2").astype(np.float32) / 32768
|
||||
|
||||
|
||||
class AudioPlayer:
|
||||
def __init__(self):
|
||||
self.command = None
|
||||
self.seen_command = None
|
||||
self.samples = np.empty(0, dtype=np.float32)
|
||||
self.position = 0
|
||||
self.gain = 1.0
|
||||
|
||||
def render(self, frames, alert_active):
|
||||
command = self.command
|
||||
if command is not self.seen_command:
|
||||
self.seen_command = command
|
||||
self.samples = command if command is not None else np.empty(0, dtype=np.float32)
|
||||
self.position = 0
|
||||
self.gain = 1.0
|
||||
out = np.zeros(frames, dtype=np.float32)
|
||||
if self.position == len(self.samples):
|
||||
return out
|
||||
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)
|
||||
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))
|
||||
return out
|
||||
|
||||
|
||||
class CustomAudio:
|
||||
def __init__(self):
|
||||
self.player = AudioPlayer()
|
||||
self.transition = GearTransition()
|
||||
self.loader = ThreadPoolExecutor(max_workers=1, thread_name_prefix="custom-audio-source", initializer=lower_loader_priority)
|
||||
self.pending = None
|
||||
|
||||
def update(self, messages):
|
||||
now = time.monotonic()
|
||||
event = None
|
||||
if self.transition.last_time is not None and now - self.transition.last_time > GEAR_TIMEOUT:
|
||||
self.transition.update(None, now, False)
|
||||
for msg in messages:
|
||||
timestamp = msg.logMonoTime / 1e9
|
||||
valid = msg.valid and msg.carState.canValid and msg.carState.gearShifter != "unknown" and 0 <= now - timestamp <= GEAR_TIMEOUT
|
||||
transition = self.transition.update(str(msg.carState.gearShifter), timestamp, valid)
|
||||
if not valid:
|
||||
event = None
|
||||
elif transition:
|
||||
event = transition
|
||||
|
||||
if event:
|
||||
self.player.command = None
|
||||
if self.pending:
|
||||
self.pending.cancel()
|
||||
self.pending = self.loader.submit(load_audio, f"{event}.wav")
|
||||
|
||||
if self.pending and self.pending.done():
|
||||
try:
|
||||
samples = self.pending.result()
|
||||
if samples is not None:
|
||||
self.player.command = samples
|
||||
except Exception:
|
||||
cloudlog.exception("Unable to load custom audio")
|
||||
self.pending = None
|
||||
@@ -0,0 +1,22 @@
|
||||
# Custom Audio
|
||||
|
||||
Put short **48 kHz, mono, 16-bit PCM WAV** files in `/data/media/0/custom_audio/` (maximum five minutes each).
|
||||
|
||||
Each gear change plays `<gear>.wav`: `park.wav`, `drive.wav`, `sport.wav`, `reverse.wav`, `neutral.wav`, `low.wav`, `brake.wav`, `eco.wav`, or `manumatic.wav`.
|
||||
|
||||
- Entering a gear starts its audio from the beginning at 25% 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 **half its normal level (12.5%) over 50 ms**. Afterward it fades back to 25% over **150 ms**.
|
||||
- Alert volume is unchanged. Custom audio may be reduced further to prevent clipping when both sounds are loud.
|
||||
- Startup and recovery from unknown/invalid gear data or data gaps establish a silent baseline.
|
||||
|
||||
WAV loading runs on a low-priority worker thread on Linux (nice +10). Playback shares the existing safety-alert output callback; its priority is unchanged.
|
||||
|
||||
No settings or dismiss control. Replace or remove files to change future playback.
|
||||
|
||||
Convert files with:
|
||||
|
||||
```sh
|
||||
ffmpeg -i input_audio -ar 48000 -ac 1 -c:a pcm_s16le drive.wav
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.selfdrive.ui.soundd import Soundd
|
||||
from openpilot.sunnypilot.selfdrive.ui.custom_audio import CustomAudio
|
||||
|
||||
|
||||
class SounddSP(Soundd):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.custom_audio = CustomAudio()
|
||||
self.gear_sock = messaging.sub_sock("carState", conflate=False)
|
||||
|
||||
def callback(self, data_out: np.ndarray, frames: int, time, status) -> 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) * 0.25
|
||||
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
|
||||
|
||||
def get_audible_alert(self, sm):
|
||||
super().get_audible_alert(sm)
|
||||
self.custom_audio.update(messaging.drain_sock(self.gear_sock, wait_for_one=False))
|
||||
Reference in New Issue
Block a user