notification sounds

This commit is contained in:
galister
2024-02-21 21:27:09 +01:00
parent e7710b56d9
commit ddba450475
7 changed files with 121 additions and 59 deletions

View File

@@ -4,7 +4,13 @@ use dbus::{
message::MatchRule, message::MatchRule,
}; };
use serde::Deserialize; use serde::Deserialize;
use std::sync::{mpsc, Arc}; use std::{
sync::{
mpsc::{self},
Arc,
},
time::Duration,
};
use crate::{overlays::toast::Toast, state::AppState}; use crate::{overlays::toast::Toast, state::AppState};
@@ -25,6 +31,10 @@ impl NotificationManager {
} }
pub fn submit_pending(&self, app: &mut AppState) { pub fn submit_pending(&self, app: &mut AppState) {
if let Some((c, _)) = &self.dbus_data {
let _ = c.process(Duration::ZERO);
}
self.rx_toast.try_iter().for_each(|toast| { self.rx_toast.try_iter().for_each(|toast| {
toast.submit(app); toast.submit(app);
}); });
@@ -44,20 +54,20 @@ impl NotificationManager {
let sender = self.tx_toast.clone(); let sender = self.tx_toast.clone();
let token = c.start_receive( let Ok(token) = c.add_match(rule, move |_: (), _, msg| {
rule, if let Ok(toast) = parse_dbus(&msg) {
Box::new(move |msg, _| { match sender.try_send(toast) {
if let Ok(toast) = parse_dbus(&msg) { Ok(_) => {}
match sender.try_send(toast) { Err(e) => {
Ok(_) => {} log::error!("Failed to send notification: {:?}", e);
Err(e) => {
log::error!("Failed to send notification: {:?}", e);
}
} }
} }
true }
}), true
); }) else {
log::error!("Failed to add dbus match. Desktop notifications will not work.");
return;
};
self.dbus_data = Some((c, token)); self.dbus_data = Some((c, token));
} }
@@ -73,7 +83,7 @@ impl NotificationManager {
return; return;
} }
}; };
let mut buf = [0u8; 1500]; let mut buf = [0u8; 1024 * 16]; // vrcx embeds icons as b64
loop { loop {
if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) { if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) {
@@ -84,6 +94,7 @@ impl NotificationManager {
continue; continue;
} }
}; };
log::info!("Received notification message: {}", json_str);
let msg = match serde_json::from_str::<XsoMessage>(json_str) { let msg = match serde_json::from_str::<XsoMessage>(json_str) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
@@ -92,10 +103,13 @@ impl NotificationManager {
} }
}; };
if msg.messageType != 1 {
continue;
}
let toast = Toast::new(msg.title, msg.content.unwrap_or_else(|| "".into())) let toast = Toast::new(msg.title, msg.content.unwrap_or_else(|| "".into()))
.with_timeout(msg.timeout) .with_timeout(msg.timeout.unwrap_or(5.))
.with_volume(msg.volume) .with_sound(msg.volume.unwrap_or(0.) > 0.1);
.with_opacity(msg.opacity);
match sender.try_send(toast) { match sender.try_send(toast) {
Ok(_) => {} Ok(_) => {}
@@ -131,23 +145,26 @@ fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
summary summary
}; };
Ok(Toast::new(title.into(), body.into())) Ok(Toast::new(title.into(), body.into())
.with_sound(true)
.with_timeout(5.0)
.with_opacity(1.0))
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct XsoMessage { struct XsoMessage {
messageType: i32, messageType: i32,
index: i32, index: Option<i32>,
volume: f32, volume: Option<f32>,
audioPath: Arc<str>, audioPath: Option<Arc<str>>,
timeout: f32, timeout: Option<f32>,
title: Arc<str>, title: Arc<str>,
content: Option<Arc<str>>, content: Option<Arc<str>>,
icon: Option<Arc<str>>, icon: Option<Arc<str>>,
height: f32, height: Option<f32>,
opacity: f32, opacity: Option<f32>,
useBase64Icon: bool, useBase64Icon: Option<bool>,
sourceApp: Option<Arc<str>>, sourceApp: Option<Arc<str>>,
alwaysShow: bool, alwaysShow: Option<bool>,
} }

View File

@@ -13,12 +13,12 @@ use crate::{
config, config,
gui::{color_parse, CanvasBuilder, Control}, gui::{color_parse, CanvasBuilder, Control},
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER}, hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
state::{AppSession, AppState}, state::AppState,
}; };
use glam::{vec2, vec3a, Affine2, Vec4}; use glam::{vec2, vec3a, Affine2, Vec4};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source}; use rodio::{Decoder, Source};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const PIXELS_PER_UNIT: f32 = 80.; const PIXELS_PER_UNIT: f32 = 80.;
@@ -39,9 +39,6 @@ where
let data = KeyboardData { let data = KeyboardData {
modifiers: 0, modifiers: 0,
processes: vec![], processes: vec![],
audio_stream: None,
first_try: true,
audio_handle: None,
}; };
let mut canvas = CanvasBuilder::new( let mut canvas = CanvasBuilder::new(
@@ -142,7 +139,7 @@ fn key_press(
) { ) {
match control.state.as_mut() { match control.state.as_mut() {
Some(KeyButtonData::Key { vk, pressed }) => { Some(KeyButtonData::Key { vk, pressed }) => {
data.key_click(&app.session); data.key_click(app);
if let PointerMode::Right = mode { if let PointerMode::Right = mode {
data.modifiers |= SHIFT; data.modifiers |= SHIFT;
@@ -155,11 +152,11 @@ fn key_press(
Some(KeyButtonData::Modifier { modifier, sticky }) => { Some(KeyButtonData::Modifier { modifier, sticky }) => {
*sticky = data.modifiers & *modifier == 0; *sticky = data.modifiers & *modifier == 0;
data.modifiers |= *modifier; data.modifiers |= *modifier;
data.key_click(&app.session); data.key_click(app);
app.hid_provider.set_modifiers(data.modifiers); app.hid_provider.set_modifiers(data.modifiers);
} }
Some(KeyButtonData::Macro { verbs }) => { Some(KeyButtonData::Macro { verbs }) => {
data.key_click(&app.session); data.key_click(app);
for (vk, press) in verbs { for (vk, press) in verbs {
app.hid_provider.send_key(*vk as _, *press); app.hid_provider.send_key(*vk as _, *press);
} }
@@ -169,7 +166,7 @@ fn key_press(
data.processes data.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_)))); .retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
data.key_click(&app.session); data.key_click(app);
if let Ok(child) = Command::new(program).args(args).spawn() { if let Ok(child) = Command::new(program).args(args).spawn() {
data.processes.push(child); data.processes.push(child);
} }
@@ -228,28 +225,16 @@ fn test_highlight(
struct KeyboardData { struct KeyboardData {
modifiers: KeyModifier, modifiers: KeyModifier,
processes: Vec<Child>, processes: Vec<Child>,
audio_stream: Option<OutputStream>,
audio_handle: Option<OutputStreamHandle>,
first_try: bool,
} }
impl KeyboardData { impl KeyboardData {
fn key_click(&mut self, session: &AppSession) { fn key_click(&mut self, app: &mut AppState) {
if !session.config.keyboard_sound_enabled { if !app.session.config.keyboard_sound_enabled {
return; return;
} }
if self.audio_stream.is_none() && self.first_try { if let Some(handle) = app.audio.get_handle() {
self.first_try = false; // https://freesound.org/people/UberBosser/sounds/421581/
if let Ok((stream, handle)) = OutputStream::try_default() {
self.audio_stream = Some(stream);
self.audio_handle = Some(handle);
} else {
log::error!("Failed to open audio stream");
}
}
if let Some(handle) = &self.audio_handle {
let wav = include_bytes!("../res/421581.wav"); let wav = include_bytes!("../res/421581.wav");
let cursor = Cursor::new(wav); let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap(); let source = Decoder::new_wav(cursor).unwrap();

View File

@@ -1,8 +1,11 @@
use std::{ use std::{
io::Cursor,
ops::Add, ops::Add,
sync::{atomic::AtomicUsize, Arc}, sync::{atomic::AtomicUsize, Arc},
}; };
use rodio::{Decoder, Source};
use glam::vec3a; use glam::vec3a;
use crate::{ use crate::{
@@ -24,8 +27,8 @@ pub struct Toast {
pub title: Arc<str>, pub title: Arc<str>,
pub body: Arc<str>, pub body: Arc<str>,
pub opacity: f32, pub opacity: f32,
pub volume: f32,
pub timeout: f32, pub timeout: f32,
pub sound: bool,
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -36,7 +39,7 @@ impl Toast {
body, body,
opacity: 1.0, opacity: 1.0,
timeout: 3.0, timeout: 3.0,
volume: 0.0, sound: false,
} }
} }
pub fn with_timeout(mut self, timeout: f32) -> Self { pub fn with_timeout(mut self, timeout: f32) -> Self {
@@ -47,8 +50,8 @@ impl Toast {
self.opacity = opacity; self.opacity = opacity;
self self
} }
pub fn with_volume(mut self, volume: f32) -> Self { pub fn with_sound(mut self, sound: bool) -> Self {
self.volume = volume; self.sound = sound;
self self
} }
pub fn submit(self, app: &mut AppState) { pub fn submit(self, app: &mut AppState) {
@@ -59,6 +62,8 @@ impl Toast {
let destroy_at = let destroy_at =
std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout)); std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout));
let has_sound = self.sound;
app.tasks.enqueue(TaskType::CreateOverlay( app.tasks.enqueue(TaskType::CreateOverlay(
selector.clone(), selector.clone(),
Box::new(move |app| new_toast(self, name, app)), Box::new(move |app| new_toast(self, name, app)),
@@ -66,6 +71,15 @@ impl Toast {
app.tasks app.tasks
.enqueue_at(TaskType::DropOverlay(selector), destroy_at); .enqueue_at(TaskType::DropOverlay(selector), destroy_at);
if has_sound {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../res/557297.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
}
}
} }
} }
@@ -91,9 +105,11 @@ fn new_toast(
.ok()?; .ok()?;
(w0.max(w1), h1 + 50.) (w0.max(w1), h1 + 50.)
} else { } else {
app.fc let (w, h) = app
.fc
.get_text_size(&title, FONT_SIZE, app.graphics.clone()) .get_text_size(&title, FONT_SIZE, app.graphics.clone())
.ok()? .ok()?;
(w, h + 20.)
}; };
let og_width = size.0; let og_width = size.0;
@@ -123,7 +139,7 @@ fn new_toast(
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title); canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
} else { } else {
log::info!("Toast: {}", title); log::info!("Toast: {}", title);
canvas.label(0., 0., size.0, size.1, title); canvas.label_centered(PADDING.0, 0., og_width, size.1, title);
} }
let state = OverlayState { let state = OverlayState {

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
f32::consts::PI, f32::consts::PI,
io::Read, io::{Cursor, Read},
process::{self, Stdio}, process::{self, Stdio},
sync::Arc, sync::Arc,
time::Instant, time::Instant,
@@ -9,6 +9,7 @@ use std::{
use chrono::Local; use chrono::Local;
use chrono_tz::Tz; use chrono_tz::Tz;
use glam::{Quat, Vec3, Vec3A}; use glam::{Quat, Vec3, Vec3A};
use rodio::{Decoder, Source};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
@@ -395,6 +396,15 @@ fn btn_mirror_dn(
} }
} }
fn audio_thump(app: &mut AppState) {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../res/380885.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
}
}
fn btn_func_dn( fn btn_func_dn(
control: &mut Control<(), ElemState>, control: &mut Control<(), ElemState>,
_: &mut (), _: &mut (),
@@ -434,6 +444,7 @@ fn btn_func_dn(
.submit(app); .submit(app);
}), }),
)); ));
audio_thump(app);
} }
ButtonFunc::SwitchWatchHand => { ButtonFunc::SwitchWatchHand => {
app.tasks.enqueue(TaskType::Overlay( app.tasks.enqueue(TaskType::Overlay(
@@ -456,6 +467,7 @@ fn btn_func_dn(
.submit(app); .submit(app);
}), }),
)); ));
audio_thump(app);
} }
} }
} }
@@ -736,6 +748,7 @@ fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut
} }
}), }),
)); ));
audio_thump(app);
} }
PointerMode::Middle => { PointerMode::Middle => {
app.tasks.enqueue(TaskType::Overlay( app.tasks.enqueue(TaskType::Overlay(
@@ -751,6 +764,7 @@ fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut
} }
}), }),
)); ));
audio_thump(app);
} }
_ => {} _ => {}
} }

BIN
src/res/380885.wav Normal file

Binary file not shown.

BIN
src/res/557297.wav Normal file

Binary file not shown.

View File

@@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::Arc};
use anyhow::bail; use anyhow::bail;
use glam::{Quat, Vec3}; use glam::{Quat, Vec3};
use rodio::{OutputStream, OutputStreamHandle};
use vulkano::format::Format; use vulkano::format::Format;
use crate::{ use crate::{
@@ -25,6 +26,7 @@ pub struct AppState {
pub format: vulkano::format::Format, pub format: vulkano::format::Format,
pub input_state: InputState, pub input_state: InputState,
pub hid_provider: Box<dyn HidProvider>, pub hid_provider: Box<dyn HidProvider>,
pub audio: AudioOutput,
} }
impl AppState { impl AppState {
@@ -59,6 +61,7 @@ impl AppState {
format: Format::R8G8B8A8_UNORM, format: Format::R8G8B8A8_UNORM,
input_state: InputState::new(), input_state: InputState::new(),
hid_provider: crate::hid::initialize(), hid_provider: crate::hid::initialize(),
audio: AudioOutput::new(),
}) })
} }
} }
@@ -115,3 +118,30 @@ impl AppSession {
} }
} }
} }
pub struct AudioOutput {
audio_stream: Option<(OutputStream, OutputStreamHandle)>,
first_try: bool,
}
impl AudioOutput {
pub fn new() -> Self {
AudioOutput {
audio_stream: None,
first_try: true,
}
}
pub fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
if self.audio_stream.is_none() && self.first_try {
self.first_try = false;
if let Ok((stream, handle)) = OutputStream::try_default() {
self.audio_stream = Some((stream, handle));
} else {
log::error!("Failed to open audio stream");
return None;
}
}
self.audio_stream.as_ref().map(|(_, h)| h)
}
}