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",
|
"zune-inflate",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "extended"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -5189,20 +5183,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
|
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"symphonia-codec-pcm",
|
"symphonia-bundle-mp3",
|
||||||
"symphonia-core",
|
"symphonia-core",
|
||||||
"symphonia-format-riff",
|
|
||||||
"symphonia-metadata",
|
"symphonia-metadata",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia-codec-pcm"
|
name = "symphonia-bundle-mp3"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
|
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"symphonia-core",
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5218,18 +5213,6 @@ dependencies = [
|
|||||||
"log",
|
"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]]
|
[[package]]
|
||||||
name = "symphonia-metadata"
|
name = "symphonia-metadata"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -6814,6 +6797,7 @@ dependencies = [
|
|||||||
"idmap",
|
"idmap",
|
||||||
"idmap-derive",
|
"idmap-derive",
|
||||||
"log",
|
"log",
|
||||||
|
"rodio",
|
||||||
"serde",
|
"serde",
|
||||||
"smol",
|
"smol",
|
||||||
"wayvr-ipc",
|
"wayvr-ipc",
|
||||||
@@ -6850,7 +6834,6 @@ dependencies = [
|
|||||||
"openxr",
|
"openxr",
|
||||||
"ovr_overlay",
|
"ovr_overlay",
|
||||||
"regex",
|
"regex",
|
||||||
"rodio",
|
|
||||||
"rosc",
|
"rosc",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ idmap-derive.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde = { workspace = true, features = ["rc"] }
|
serde = { workspace = true, features = ["rc"] }
|
||||||
xdg.workspace = true
|
xdg.workspace = true
|
||||||
|
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
smol = "2.0.2"
|
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)]
|
#[serde(default)]
|
||||||
pub notification_topics: IdMap<ToastTopic, ToastDisplayMethod>,
|
pub notification_topics: IdMap<ToastTopic, ToastDisplayMethod>,
|
||||||
|
|
||||||
#[serde(default = "def_empty")]
|
|
||||||
pub notification_sound: Arc<str>,
|
|
||||||
|
|
||||||
#[serde(default = "def_true")]
|
#[serde(default = "def_true")]
|
||||||
pub keyboard_sound_enabled: bool,
|
pub keyboard_sound_enabled: bool,
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod astr_containers;
|
pub mod astr_containers;
|
||||||
|
pub mod audio;
|
||||||
pub mod cache_dir;
|
pub mod cache_dir;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ anyhow.workspace = true
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
glam = { workspace = true, features = ["mint", "serde"] }
|
glam = { workspace = true, features = ["mint", "serde"] }
|
||||||
idmap = { workspace = true, features = ["serde"] }
|
idmap = { workspace = true, features = ["serde"] }
|
||||||
idmap-derive. workspace = true
|
idmap-derive.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
@@ -68,11 +68,6 @@ ovr_overlay = { features = [
|
|||||||
"ovr_input",
|
"ovr_input",
|
||||||
"ovr_system",
|
"ovr_system",
|
||||||
], git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "8d62c73d5f17e4210d6d0cd52e7f3953eb9b481a", optional = true }
|
], 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 }
|
rosc = { version = "0.11.4", optional = true }
|
||||||
serde_json5 = "0.2.1"
|
serde_json5 = "0.2.1"
|
||||||
serde_yaml = "0.9.34"
|
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) {
|
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 {
|
struct KeyState {
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ impl Toast {
|
|||||||
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
|
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
|
||||||
|
|
||||||
if self.sound && app.session.config.notifications_sound_enabled {
|
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.
|
// 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,
|
renderer_vk::context::SharedContext as WSharedContext,
|
||||||
};
|
};
|
||||||
use wlx_common::{
|
use wlx_common::{
|
||||||
|
audio,
|
||||||
config::GeneralConfig,
|
config::GeneralConfig,
|
||||||
overlays::{ToastDisplayMethod, ToastTopic},
|
overlays::{ToastDisplayMethod, ToastTopic},
|
||||||
};
|
};
|
||||||
@@ -26,7 +27,7 @@ use crate::{
|
|||||||
graphics::WGfxExtras,
|
graphics::WGfxExtras,
|
||||||
gui,
|
gui,
|
||||||
ipc::{event_queue::SyncEventQueue, ipc_server, signal::WayVRSignal},
|
ipc::{event_queue::SyncEventQueue, ipc_server, signal::WayVRSignal},
|
||||||
subsystem::{audio::AudioOutput, dbus::DbusConnector, input::HidWrapper},
|
subsystem::{dbus::DbusConnector, input::HidWrapper},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@@ -36,7 +37,9 @@ pub struct AppState {
|
|||||||
pub gfx: Arc<WGfx>,
|
pub gfx: Arc<WGfx>,
|
||||||
pub gfx_extras: WGfxExtras,
|
pub gfx_extras: WGfxExtras,
|
||||||
pub hid_provider: HidWrapper,
|
pub hid_provider: HidWrapper,
|
||||||
pub audio_provider: AudioOutput,
|
|
||||||
|
pub audio_system: audio::AudioSystem,
|
||||||
|
pub audio_sample_player: audio::SamplePlayer,
|
||||||
|
|
||||||
pub wgui_shared: WSharedContext,
|
pub wgui_shared: WSharedContext,
|
||||||
|
|
||||||
@@ -44,7 +47,6 @@ pub struct AppState {
|
|||||||
pub screens: SmallVec<[ScreenMeta; 8]>,
|
pub screens: SmallVec<[ScreenMeta; 8]>,
|
||||||
pub anchor: Affine3A,
|
pub anchor: Affine3A,
|
||||||
pub anchor_grabbed: bool,
|
pub anchor_grabbed: bool,
|
||||||
pub toast_sound: &'static [u8],
|
|
||||||
|
|
||||||
pub wgui_globals: WguiGlobals,
|
pub wgui_globals: WguiGlobals,
|
||||||
|
|
||||||
@@ -93,14 +95,20 @@ impl AppState {
|
|||||||
#[cfg(feature = "osc")]
|
#[cfg(feature = "osc")]
|
||||||
let osc_sender = crate::subsystem::osc::OscSender::new(session.config.osc_out_port).ok();
|
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 wgui_shared = WSharedContext::new(gfx.clone())?;
|
||||||
let theme = session.config.theme_path.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();
|
let mut defaults = wgui::globals::Defaults::default();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -131,13 +139,13 @@ impl AppState {
|
|||||||
gfx,
|
gfx,
|
||||||
gfx_extras,
|
gfx_extras,
|
||||||
hid_provider,
|
hid_provider,
|
||||||
audio_provider: AudioOutput::new(),
|
audio_system: audio::AudioSystem::new(),
|
||||||
|
audio_sample_player,
|
||||||
wgui_shared,
|
wgui_shared,
|
||||||
input_state: InputState::new(),
|
input_state: InputState::new(),
|
||||||
screens: smallvec![],
|
screens: smallvec![],
|
||||||
anchor: Affine3A::IDENTITY,
|
anchor: Affine3A::IDENTITY,
|
||||||
anchor_grabbed: false,
|
anchor_grabbed: false,
|
||||||
toast_sound: toast_sound_wav,
|
|
||||||
wgui_globals: WguiGlobals::new(
|
wgui_globals: WguiGlobals::new(
|
||||||
Box::new(gui::asset::GuiAsset {}),
|
Box::new(gui::asset::GuiAsset {}),
|
||||||
defaults,
|
defaults,
|
||||||
@@ -156,29 +164,6 @@ impl AppState {
|
|||||||
wvr_server: wayvr_server,
|
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 {
|
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 dbus;
|
||||||
pub mod hid;
|
pub mod hid;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
|||||||
Reference in New Issue
Block a user