move audio system to wlx-common, compress audio data, sample player
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
97
wlx-common/src/audio.rs
Normal 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>>(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod astr_containers;
|
||||
pub mod audio;
|
||||
pub mod cache_dir;
|
||||
pub mod common;
|
||||
pub mod config;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
BIN
wlx-overlay-s/src/res/key_click.mp3
Normal file
BIN
wlx-overlay-s/src/res/key_click.mp3
Normal file
Binary file not shown.
BIN
wlx-overlay-s/src/res/toast.mp3
Normal file
BIN
wlx-overlay-s/src/res/toast.mp3
Normal file
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod audio;
|
||||
pub mod dbus;
|
||||
pub mod hid;
|
||||
pub mod input;
|
||||
|
||||
Reference in New Issue
Block a user