diff options
Diffstat (limited to 'mumd')
| -rw-r--r-- | mumd/Cargo.toml | 11 | ||||
| -rw-r--r-- | mumd/src/audio.rs | 113 | ||||
| -rw-r--r-- | mumd/src/audio/output.rs | 16 | ||||
| -rw-r--r-- | mumd/src/network/tcp.rs | 8 | ||||
| -rw-r--r-- | mumd/src/notify.rs | 6 | ||||
| -rw-r--r-- | mumd/src/state.rs | 173 |
6 files changed, 255 insertions, 72 deletions
diff --git a/mumd/Cargo.toml b/mumd/Cargo.toml index 5c546d1..f497524 100644 --- a/mumd/Cargo.toml +++ b/mumd/Cargo.toml @@ -9,7 +9,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["libnotify"] +default = ["notifications", "sound-effects"] + +notifications = ["libnotify"] + +sound-effects = ["hound", "samplerate"] [dependencies] mumlib = { path = "../mumlib" } @@ -23,7 +27,7 @@ ipc-channel = "0.14" log = "0.4" mumble-protocol = "0.3" native-tls = "0.2" -openssl = { version = "0.10", optional = true } +openssl = { version = "0.10" } opus = "0.2" serde = { version = "1.0", features = ["derive"] } tokio = { version = "0.2", features = ["blocking", "macros", "rt-core", "sync", "tcp", "time"] } @@ -32,5 +36,8 @@ tokio-util = { version = "0.3", features = ["codec", "udp"] } libnotify = { version = "1.0", optional = true } +hound = { version = "3.4.0", optional = true } +samplerate = { version = "0.2.2", optional = true } + #compressor = "0.3" #daemonize = "0.4" diff --git a/mumd/src/audio.rs b/mumd/src/audio.rs index 05e3ff5..d5617fc 100644 --- a/mumd/src/audio.rs +++ b/mumd/src/audio.rs @@ -1,16 +1,66 @@ pub mod input; pub mod output; +#[cfg(feature = "sound-effects")] +use crate::audio::output::SaturatingAdd; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{SampleFormat, SampleRate, Stream, StreamConfig}; use log::*; use mumble_protocol::voice::VoicePacketPayload; use opus::Channels; +#[cfg(feature = "sound-effects")] +use samplerate::ConverterType; use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; use tokio::sync::{mpsc, watch}; +//TODO? move to mumlib +#[cfg(feature = "sound-effects")] +pub const EVENT_SOUNDS: &[(&str, NotificationEvents)] = &[ + ("resources/connect.wav", NotificationEvents::ServerConnect), + ( + "resources/disconnect.wav", + NotificationEvents::ServerDisconnect, + ), + ( + "resources/channel_join.wav", + NotificationEvents::UserConnected, + ), + ( + "resources/channel_leave.wav", + NotificationEvents::UserDisconnected, + ), + ( + "resources/channel_join.wav", + NotificationEvents::UserJoinedChannel, + ), + ( + "resources/channel_leave.wav", + NotificationEvents::UserLeftChannel, + ), + ("resources/mute.wav", NotificationEvents::Mute), + ("resources/unmute.wav", NotificationEvents::Unmute), + ("resources/deafen.wav", NotificationEvents::Deafen), + ("resources/undeafen.wav", NotificationEvents::Undeafen), +]; + +const SAMPLE_RATE: u32 = 48000; + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] +pub enum NotificationEvents { + ServerConnect, + ServerDisconnect, + UserConnected, + UserDisconnected, + UserJoinedChannel, + UserLeftChannel, + Mute, + Unmute, + Deafen, + Undeafen, +} + pub struct Audio { output_config: StreamConfig, _output_stream: Stream, @@ -24,11 +74,17 @@ pub struct Audio { user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>, client_streams: Arc<Mutex<HashMap<u32, output::ClientStream>>>, + + #[cfg(feature = "sound-effects")] + sounds: HashMap<NotificationEvents, Vec<f32>>, + + #[cfg(feature = "sound-effects")] + play_sounds: Arc<Mutex<VecDeque<f32>>>, } impl Audio { pub fn new(input_volume: f32, output_volume: f32) -> Self { - let sample_rate = SampleRate(48000); + let sample_rate = SampleRate(SAMPLE_RATE); let host = cpal::default_host(); let output_device = host @@ -71,12 +127,14 @@ impl Audio { let user_volumes = Arc::new(Mutex::new(HashMap::new())); let (output_volume_sender, output_volume_receiver) = watch::channel::<f32>(output_volume); + let play_sounds = Arc::new(Mutex::new(VecDeque::new())); let client_streams = Arc::new(Mutex::new(HashMap::new())); let output_stream = match output_supported_sample_format { SampleFormat::F32 => output_device.build_output_stream( &output_config, output::curry_callback::<f32>( + Arc::clone(&play_sounds), Arc::clone(&client_streams), output_volume_receiver, Arc::clone(&user_volumes), @@ -86,6 +144,7 @@ impl Audio { SampleFormat::I16 => output_device.build_output_stream( &output_config, output::curry_callback::<i16>( + Arc::clone(&play_sounds), Arc::clone(&client_streams), output_volume_receiver, Arc::clone(&user_volumes), @@ -95,6 +154,7 @@ impl Audio { SampleFormat::U16 => output_device.build_output_stream( &output_config, output::curry_callback::<u16>( + Arc::clone(&play_sounds), Arc::clone(&client_streams), output_volume_receiver, Arc::clone(&user_volumes), @@ -160,6 +220,34 @@ impl Audio { output_stream.play().unwrap(); + #[cfg(feature = "sound-effects")] + let sounds = EVENT_SOUNDS + .iter() + .map(|(path, event)| { + let reader = hound::WavReader::open(path).unwrap(); + let spec = reader.spec(); + let samples = match spec.sample_format { + hound::SampleFormat::Float => reader + .into_samples::<f32>() + .map(|e| e.unwrap()) + .collect::<Vec<_>>(), + hound::SampleFormat::Int => reader + .into_samples::<i16>() + .map(|e| cpal::Sample::to_f32(&e.unwrap())) + .collect::<Vec<_>>(), + }; + let samples = samplerate::convert( + spec.sample_rate, + SAMPLE_RATE, + spec.channels as usize, + ConverterType::SincBestQuality, + &samples, + ) + .unwrap(); + (*event, samples) + }) + .collect(); + Self { output_config, _output_stream: output_stream, @@ -167,8 +255,12 @@ impl Audio { input_volume_sender, input_channel_receiver: Some(input_receiver), client_streams, + #[cfg(feature = "sound-effects")] + sounds, output_volume_sender, user_volumes, + #[cfg(feature = "sound-effects")] + play_sounds, } } @@ -250,4 +342,21 @@ impl Audio { } } } + + #[cfg(feature = "sound-effects")] + pub fn play_effect(&self, effect: NotificationEvents) { + let samples = self.sounds.get(&effect).unwrap(); + + let mut play_sounds = self.play_sounds.lock().unwrap(); + + for (val, e) in play_sounds.iter_mut().zip(samples.iter()) { + *val = val.saturating_add(*e); + } + + let l = play_sounds.len(); + play_sounds.extend(samples.iter().skip(l)); + } + + #[cfg(not(feature = "sound-effects"))] + pub fn play_effect(&self, _: NotificationEvents) {} } diff --git a/mumd/src/audio/output.rs b/mumd/src/audio/output.rs index ce116a8..5e0cb8d 100644 --- a/mumd/src/audio/output.rs +++ b/mumd/src/audio/output.rs @@ -71,8 +71,9 @@ impl SaturatingAdd for u16 { } } -pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd>( - buf: Arc<Mutex<HashMap<u32, ClientStream>>>, +pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>( + effect_sound: Arc<Mutex<VecDeque<f32>>>, + user_bufs: Arc<Mutex<HashMap<u32, ClientStream>>>, output_volume_receiver: watch::Receiver<f32>, user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>, ) -> impl FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static { @@ -83,8 +84,9 @@ pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd>( let volume = *output_volume_receiver.borrow(); - let mut lock = buf.lock().unwrap(); - for (id, client_stream) in &mut *lock { + let mut effects_sound = effect_sound.lock().unwrap(); + let mut user_bufs = user_bufs.lock().unwrap(); + for (id, client_stream) in &mut *user_bufs { let (user_volume, muted) = user_volumes .lock() .unwrap() @@ -98,5 +100,11 @@ pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd>( } } } + + for sample in data.iter_mut() { + *sample = sample.saturating_add(Sample::from( + &(effects_sound.pop_front().unwrap_or(0.0) * volume), + )); + } } } diff --git a/mumd/src/network/tcp.rs b/mumd/src/network/tcp.rs index 131f066..2a0d01e 100644 --- a/mumd/src/network/tcp.rs +++ b/mumd/src/network/tcp.rs @@ -247,13 +247,7 @@ async fn listen( warn!("Login rejected: {:?}", msg); } ControlPacket::UserState(msg) => { - let mut state = state.lock().unwrap(); - if *state.phase_receiver().borrow() == StatePhase::Connecting { - state.audio_mut().add_client(msg.get_session()); - state.parse_user_state(*msg); - } else { - state.parse_user_state(*msg); - } + state.lock().unwrap().parse_user_state(*msg); } ControlPacket::UserRemove(msg) => { state.lock().unwrap().remove_client(*msg); diff --git a/mumd/src/notify.rs b/mumd/src/notify.rs index 0739c58..ee387cc 100644 --- a/mumd/src/notify.rs +++ b/mumd/src/notify.rs @@ -1,9 +1,9 @@ pub fn init() { - #[cfg(feature = "libnotify")] + #[cfg(feature = "notifications")] libnotify::init("mumd").unwrap(); } -#[cfg(feature = "libnotify")] +#[cfg(feature = "notifications")] pub fn send(msg: String) -> Option<bool> { match libnotify::Notification::new("mumd", Some(msg.as_str()), None).show() { Ok(_) => Some(true), @@ -14,7 +14,7 @@ pub fn send(msg: String) -> Option<bool> { } } -#[cfg(not(feature = "libnotify"))] +#[cfg(not(feature = "notifications"))] pub fn send(_: String) -> Option<bool> { None } diff --git a/mumd/src/state.rs b/mumd/src/state.rs index 8fe5e36..91cf734 100644 --- a/mumd/src/state.rs +++ b/mumd/src/state.rs @@ -2,7 +2,7 @@ pub mod channel; pub mod server; pub mod user; -use crate::audio::Audio; +use crate::audio::{Audio, NotificationEvents}; use crate::network::ConnectionInfo; use crate::notify; use crate::state::server::Server; @@ -85,7 +85,6 @@ impl State { state } - //TODO? move bool inside Result pub fn handle_command(&mut self, command: Command) -> ExecutionContext { match command { Command::ChannelJoin { channel_identifier } => { @@ -219,6 +218,7 @@ impl State { .0 .broadcast(StatePhase::Disconnected) .unwrap(); + self.audio.play_effect(NotificationEvents::ServerDisconnect); now!(Ok(None)) } Command::ConfigReload => { @@ -238,7 +238,7 @@ impl State { return now!(Err(Error::DisconnectedError)); } - let server = self.server_mut().unwrap(); + let server = self.server().unwrap(); let action = match (toggle, server.muted(), server.deafened()) { (Some(false), false, false) => None, (Some(false), false, true) => Some((false, false)), @@ -255,6 +255,19 @@ impl State { }; if let Some((mute, deafen)) = action { + if server.deafened() != deafen { + self.audio.play_effect(if deafen { + NotificationEvents::Deafen + } else { + NotificationEvents::Undeafen + }); + } else if server.muted() != mute { + self.audio.play_effect(if mute { + NotificationEvents::Mute + } else { + NotificationEvents::Unmute + }); + } let mut msg = msgs::UserState::new(); if server.muted() != mute { msg.set_self_mute(mute); @@ -264,6 +277,7 @@ impl State { if server.deafened() != deafen { msg.set_self_deaf(deafen); } + let server = self.server_mut().unwrap(); server.set_muted(mute); server.set_deafened(deafen); self.packet_sender.send(msg.into()).unwrap(); @@ -276,7 +290,7 @@ impl State { return now!(Err(Error::DisconnectedError)); } - let server = self.server_mut().unwrap(); + let server = self.server().unwrap(); let action = match (toggle, server.muted(), server.deafened()) { (Some(false), false, false) => None, (Some(false), false, true) => Some((false, false)), @@ -293,6 +307,19 @@ impl State { }; if let Some((mute, deafen)) = action { + if server.deafened() != deafen { + self.audio.play_effect(if deafen { + NotificationEvents::Deafen + } else { + NotificationEvents::Undeafen + }); + } else if server.muted() != mute { + self.audio.play_effect(if mute { + NotificationEvents::Mute + } else { + NotificationEvents::Unmute + }); + } let mut msg = msgs::UserState::new(); if server.muted() != mute { msg.set_self_mute(mute); @@ -302,6 +329,7 @@ impl State { if server.deafened() != deafen { msg.set_self_deaf(deafen); } + let server = self.server_mut().unwrap(); server.set_muted(mute); server.set_deafened(deafen); self.packet_sender.send(msg.into()).unwrap(); @@ -385,25 +413,29 @@ impl State { } } - pub fn parse_user_state(&mut self, msg: msgs::UserState) -> Option<UserDiff> { + pub fn parse_user_state(&mut self, msg: msgs::UserState) { if !msg.has_session() { warn!("Can't parse user state without session"); - return None; + return; } let session = msg.get_session(); // check if this is initial state if !self.server().unwrap().users().contains_key(&session) { - self.parse_initial_user_state(session, msg); - None + self.create_user(msg); } else { - Some(self.parse_updated_user_state(session, msg)) + self.update_user(msg); } } - fn parse_initial_user_state(&mut self, session: u32, msg: msgs::UserState) { + fn create_user(&mut self, msg: msgs::UserState) { if !msg.has_name() { warn!("Missing name in initial user state"); - } else if msg.get_name() == self.server().unwrap().username().unwrap() { + return; + } + + let session = msg.get_session(); + + if msg.get_name() == self.server().unwrap().username().unwrap() { // this is us *self.server_mut().unwrap().session_id_mut() = Some(session); } else { @@ -412,27 +444,35 @@ impl State { // send notification only if we've passed the connecting phase if *self.phase_receiver().borrow() == StatePhase::Connected { - let channel_id = if msg.has_channel_id() { - msg.get_channel_id() - } else { - 0 - }; - if let Some(channel) = self.server().unwrap().channels().get(&channel_id) { - notify::send(format!( - "{} connected and joined {}", - &msg.get_name(), - channel.name() - )); + let channel_id = msg.get_channel_id(); + + if channel_id + == self.get_users_channel(self.server().unwrap().session_id().unwrap()) + { + if let Some(channel) = self.server().unwrap().channels().get(&channel_id) { + notify::send(format!( + "{} connected and joined {}", + &msg.get_name(), + channel.name() + )); + } + + self.audio.play_effect(NotificationEvents::UserConnected); } } } + self.server_mut() .unwrap() .users_mut() .insert(session, user::User::new(msg)); } - fn parse_updated_user_state(&mut self, session: u32, msg: msgs::UserState) -> UserDiff { + fn update_user(&mut self, msg: msgs::UserState) { + let session = msg.get_session(); + + let from_channel = self.get_users_channel(session); + let user = self .server_mut() .unwrap() @@ -453,40 +493,48 @@ impl State { let diff = UserDiff::from(msg); user.apply_user_diff(&diff); + let user = self.server().unwrap().users().get(&session).unwrap(); - // send notification if the user moved to or from any channel - //TODO our channel only - if let Some(channel_id) = diff.channel_id { - if let Some(channel) = self.server().unwrap().channels().get(&channel_id) { - notify::send(format!( - "{} moved to channel {}", - &user.name(), - channel.name() - )); - } else { - warn!("{} moved to invalid channel {}", &user.name(), channel_id); + if Some(session) != self.server().unwrap().session_id() { + //send notification if the user moved to or from any channel + if let Some(to_channel) = diff.channel_id { + let this_channel = + self.get_users_channel(self.server().unwrap().session_id().unwrap()); + if from_channel == this_channel || to_channel == this_channel { + if let Some(channel) = self.server().unwrap().channels().get(&to_channel) { + notify::send(format!( + "{} moved to channel {}", + user.name(), + channel.name() + )); + } else { + warn!("{} moved to invalid channel {}", user.name(), to_channel); + } + self.audio.play_effect(if from_channel == this_channel { + NotificationEvents::UserJoinedChannel + } else { + NotificationEvents::UserLeftChannel + }); + } } - } - // send notification if a user muted/unmuted - //TODO our channel only - let notify_desc = match (mute, deaf) { - (Some(true), Some(true)) => Some(format!("{} muted and deafend themselves", &user.name())), - (Some(false), Some(false)) => Some(format!("{} unmuted and undeafend themselves", &user.name())), - (None, Some(true)) => Some(format!("{} deafend themselves", &user.name())), - (None, Some(false)) => Some(format!("{} undeafend themselves", &user.name())), - (Some(true), None) => Some(format!("{} muted themselves", &user.name())), - (Some(false), None) => Some(format!("{} unmuted themselves", &user.name())), - (Some(true), Some(false)) => Some(format!("{} muted and undeafened themselves", &user.name())), - (Some(false), Some(true)) => Some(format!("{} unmuted and deafened themselves", &user.name())), - (None, None) => None, - }; - if let Some(notify_desc) = notify_desc { - notify::send(notify_desc); + //send notification if a user muted/unmuted + if mute != None || deaf != None { + let mut s = user.name().to_string(); + if let Some(mute) = mute { + s += if mute { " muted" } else { " unmuted" }; + } + if mute.is_some() && deaf.is_some() { + s += " and"; + } + if let Some(deaf) = deaf { + s += if deaf { " deafened" } else { " undeafened" }; + } + s += " themselves"; + notify::send(s); + } } - - diff } pub fn remove_client(&mut self, msg: msgs::UserRemove) { @@ -494,8 +542,14 @@ impl State { warn!("Tried to remove user state without session"); return; } - if let Some(user) = self.server().unwrap().users().get(&msg.get_session()) { - notify::send(format!("{} disconnected", &user.name())); + + let this_channel = self.get_users_channel(self.server().unwrap().session_id().unwrap()); + let other_channel = self.get_users_channel(msg.get_session()); + if this_channel == other_channel { + self.audio.play_effect(NotificationEvents::UserDisconnected); + if let Some(user) = self.server().unwrap().users().get(&msg.get_session()) { + notify::send(format!("{} disconnected", &user.name())); + } } self.audio().remove_client(msg.get_session()); @@ -521,6 +575,7 @@ impl State { .0 .broadcast(StatePhase::Connected) .unwrap(); + self.audio.play_effect(NotificationEvents::ServerConnect); } pub fn audio(&self) -> &Audio { @@ -544,4 +599,14 @@ impl State { pub fn username(&self) -> Option<&str> { self.server.as_ref().map(|e| e.username()).flatten() } + fn get_users_channel(&self, user_id: u32) -> u32 { + self.server() + .unwrap() + .users() + .iter() + .find(|e| *e.0 == user_id) + .unwrap() + .1 + .channel() + } } |
