move audio system to wlx-common, compress audio data, sample player

This commit is contained in:
Aleksander
2026-01-02 00:19:17 +01:00
parent b6c16dff18
commit 383bf3b11f
17 changed files with 132 additions and 116 deletions

29
Cargo.lock generated
View File

@@ -1818,12 +1818,6 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "extended"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -5189,20 +5183,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
dependencies = [
"lazy_static",
"symphonia-codec-pcm",
"symphonia-bundle-mp3",
"symphonia-core",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-pcm"
name = "symphonia-bundle-mp3"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
@@ -5218,18 +5213,6 @@ dependencies = [
"log",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
dependencies = [
"extended",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-metadata"
version = "0.5.5"
@@ -6814,6 +6797,7 @@ dependencies = [
"idmap",
"idmap-derive",
"log",
"rodio",
"serde",
"smol",
"wayvr-ipc",
@@ -6850,7 +6834,6 @@ dependencies = [
"openxr",
"ovr_overlay",
"regex",
"rodio",
"rosc",
"rust-embed",
"serde",

View File

@@ -14,6 +14,10 @@ idmap-derive.workspace = true
log.workspace = true
serde = { workspace = true, features = ["rc"] }
xdg.workspace = true
chrono = "0.4.42"
smol = "2.0.2"
rodio = { version = "0.21.1", default-features = false, features = [
"playback",
"mp3",
"hound",
] }

97
wlx-common/src/audio.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::{collections::HashMap, io::Cursor};
use rodio::Source;
pub struct AudioSystem {
audio_stream: Option<rodio::OutputStream>,
first_try: bool,
}
pub struct AudioSample {
buffer: rodio::buffer::SamplesBuffer,
}
pub struct SamplePlayer {
samples: HashMap<String, AudioSample>,
}
impl SamplePlayer {
pub fn new() -> Self {
Self {
samples: HashMap::new(),
}
}
pub fn register_sample(&mut self, sample_name: &str, sample: AudioSample) {
self.samples.insert(String::from(sample_name), sample);
}
pub fn play_sample(&mut self, system: &mut AudioSystem, sample_name: &str) {
let Some(sample) = self.samples.get(sample_name) else {
log::error!("failed to play sample by name {}", sample_name);
return;
};
system.play_sample(sample);
}
}
impl Default for SamplePlayer {
fn default() -> Self {
Self::new()
}
}
impl AudioSystem {
pub const fn new() -> Self {
Self {
audio_stream: None,
first_try: true,
}
}
fn get_handle(&mut self) -> Option<&rodio::OutputStream> {
if self.audio_stream.is_none() && self.first_try {
self.first_try = false;
if let Ok(stream) = rodio::OutputStreamBuilder::open_default_stream() {
self.audio_stream = Some(stream);
} else {
log::error!("Failed to open audio stream. Audio will not work.");
return None;
}
}
self.audio_stream.as_ref()
}
pub fn play_sample(&mut self, sample: &AudioSample) -> Option<()> {
let handle = self.get_handle()?;
handle.mixer().add(sample.buffer.clone());
Some(())
}
}
impl Default for AudioSystem {
fn default() -> Self {
Self::new()
}
}
impl AudioSample {
pub fn from_mp3(encoded_bin: &[u8]) -> anyhow::Result<Self> {
// SAFETY: this is safe
// rodio requires us to provide 'static data to decode it
// we are casting &T into &'static T just to prevent unnecessary memory copy into Vec<u8>.
// `encoded_bin` data will be always valid, because we are dropping `decoder` in this scope afterwards.
// Compliant and slower version would be: Cursor::new(encoded_bin.to_vec())
let cursor = unsafe { Cursor::new(std::mem::transmute::<&[u8], &'static [u8]>(encoded_bin)) };
let decoder = rodio::Decoder::new_mp3(cursor)?;
Ok(Self {
buffer: rodio::buffer::SamplesBuffer::new(
decoder.channels(),
decoder.sample_rate(),
decoder.collect::<Vec<rodio::Sample>>(),
),
})
}
}

View File

@@ -136,9 +136,6 @@ pub struct GeneralConfig {
#[serde(default)]
pub notification_topics: IdMap<ToastTopic, ToastDisplayMethod>,
#[serde(default = "def_empty")]
pub notification_sound: Arc<str>,
#[serde(default = "def_true")]
pub keyboard_sound_enabled: bool,

View File

@@ -1,4 +1,5 @@
pub mod astr_containers;
pub mod audio;
pub mod cache_dir;
pub mod common;
pub mod config;

View File

@@ -30,7 +30,7 @@ anyhow.workspace = true
clap.workspace = true
glam = { workspace = true, features = ["mint", "serde"] }
idmap = { workspace = true, features = ["serde"] }
idmap-derive. workspace = true
idmap-derive.workspace = true
log.workspace = true
rust-embed.workspace = true
regex.workspace = true
@@ -68,11 +68,6 @@ ovr_overlay = { features = [
"ovr_input",
"ovr_system",
], git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "8d62c73d5f17e4210d6d0cd52e7f3953eb9b481a", optional = true }
rodio = { version = "0.21.1", default-features = false, features = [
"playback",
"wav",
"hound",
] }
rosc = { version = "0.11.4", optional = true }
serde_json5 = "0.2.1"
serde_yaml = "0.9.34"

View File

@@ -325,10 +325,9 @@ impl KeyboardState {
}
}
const KEY_AUDIO_WAV: &[u8] = include_bytes!("../../res/421581.wav");
fn play_key_click(app: &mut AppState) {
app.audio_provider.play(KEY_AUDIO_WAV);
app.audio_sample_player
.play_sample(&mut app.audio_system, "key_click");
}
struct KeyState {

View File

@@ -67,7 +67,8 @@ impl Toast {
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
if self.sound && app.session.config.notifications_sound_enabled {
app.audio_provider.play(app.toast_sound);
app.audio_sample_player
.play_sample(&mut app.audio_system, "toast");
}
// drop any toast that was created before us.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,6 +7,7 @@ use wgui::{
renderer_vk::context::SharedContext as WSharedContext,
};
use wlx_common::{
audio,
config::GeneralConfig,
overlays::{ToastDisplayMethod, ToastTopic},
};
@@ -26,7 +27,7 @@ use crate::{
graphics::WGfxExtras,
gui,
ipc::{event_queue::SyncEventQueue, ipc_server, signal::WayVRSignal},
subsystem::{audio::AudioOutput, dbus::DbusConnector, input::HidWrapper},
subsystem::{dbus::DbusConnector, input::HidWrapper},
};
pub struct AppState {
@@ -36,7 +37,9 @@ pub struct AppState {
pub gfx: Arc<WGfx>,
pub gfx_extras: WGfxExtras,
pub hid_provider: HidWrapper,
pub audio_provider: AudioOutput,
pub audio_system: audio::AudioSystem,
pub audio_sample_player: audio::SamplePlayer,
pub wgui_shared: WSharedContext,
@@ -44,7 +47,6 @@ pub struct AppState {
pub screens: SmallVec<[ScreenMeta; 8]>,
pub anchor: Affine3A,
pub anchor_grabbed: bool,
pub toast_sound: &'static [u8],
pub wgui_globals: WguiGlobals,
@@ -93,14 +95,20 @@ impl AppState {
#[cfg(feature = "osc")]
let osc_sender = crate::subsystem::osc::OscSender::new(session.config.osc_out_port).ok();
let toast_sound_wav = Self::try_load_bytes(
&session.config.notification_sound,
include_bytes!("res/557297.wav"),
);
let wgui_shared = WSharedContext::new(gfx.clone())?;
let theme = session.config.theme_path.clone();
let mut audio_sample_player = audio::SamplePlayer::new();
audio_sample_player.register_sample(
"key_click",
audio::AudioSample::from_mp3(include_bytes!("res/key_click.mp3"))?,
);
audio_sample_player.register_sample(
"toast",
audio::AudioSample::from_mp3(include_bytes!("res/toast.mp3"))?,
);
let mut defaults = wgui::globals::Defaults::default();
{
@@ -131,13 +139,13 @@ impl AppState {
gfx,
gfx_extras,
hid_provider,
audio_provider: AudioOutput::new(),
audio_system: audio::AudioSystem::new(),
audio_sample_player,
wgui_shared,
input_state: InputState::new(),
screens: smallvec![],
anchor: Affine3A::IDENTITY,
anchor_grabbed: false,
toast_sound: toast_sound_wav,
wgui_globals: WguiGlobals::new(
Box::new(gui::asset::GuiAsset {}),
defaults,
@@ -156,29 +164,6 @@ impl AppState {
wvr_server: wayvr_server,
})
}
pub fn try_load_bytes(path: &str, fallback_data: &'static [u8]) -> &'static [u8] {
if path.is_empty() {
return fallback_data;
}
let real_path = config_io::get_config_root().join(path);
if std::fs::File::open(real_path.clone()).is_err() {
log::warn!("Could not open file at: {path}");
return fallback_data;
}
match std::fs::read(real_path) {
// Box is used here to work around `f`'s limited lifetime
Ok(f) => Box::leak(Box::new(f)).as_slice(),
Err(e) => {
log::warn!("Failed to read file at: {path}");
log::warn!("{e:?}");
fallback_data
}
}
}
}
pub struct AppSession {

View File

@@ -1,45 +0,0 @@
use std::io::Cursor;
use rodio::{Decoder, OutputStreamBuilder, stream::OutputStream};
pub struct AudioOutput {
audio_stream: Option<OutputStream>,
first_try: bool,
}
impl AudioOutput {
pub const fn new() -> Self {
Self {
audio_stream: None,
first_try: true,
}
}
fn get_handle(&mut self) -> Option<&OutputStream> {
if self.audio_stream.is_none() && self.first_try {
self.first_try = false;
if let Ok(stream) = OutputStreamBuilder::open_default_stream() {
self.audio_stream = Some(stream);
} else {
log::error!("Failed to open audio stream. Audio will not work.");
return None;
}
}
self.audio_stream.as_ref()
}
pub fn play(&mut self, wav_bytes: &'static [u8]) {
let Some(handle) = self.get_handle() else {
return;
};
let cursor = Cursor::new(wav_bytes);
let source = match Decoder::new_wav(cursor) {
Ok(source) => source,
Err(e) => {
log::error!("Failed to play sound: {e:?}");
return;
}
};
let () = handle.mixer().add(source);
}
}

View File

@@ -1,4 +1,3 @@
pub mod audio;
pub mod dbus;
pub mod hid;
pub mod input;