diff options
| author | Gustav Sörnäs <gustav@sornas.net> | 2021-06-20 22:26:07 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-06-20 22:26:07 +0200 |
| commit | 9ccddaeda2800f9b2323b3e2f75c5758a2341747 (patch) | |
| tree | ab94c8658226afa6915493401a668b01cfddef4e | |
| parent | 1b61ae7a8834db3278fcecb82cd066d5c15ddcf9 (diff) | |
| parent | 3574c2c0b990afb251f96901df02e0eb4518e1c7 (diff) | |
| download | mum-9ccddaeda2800f9b2323b3e2f75c5758a2341747.tar.gz | |
Merge pull request #109 from mum-rs/ogg
| -rw-r--r-- | CHANGELOG | 2 | ||||
| -rw-r--r-- | Cargo.lock | 36 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | mumd/Cargo.toml | 6 | ||||
| -rw-r--r-- | mumd/src/audio.rs | 131 | ||||
| -rw-r--r-- | mumd/src/audio/fallback_sfx.wav (renamed from mumd/src/fallback_sfx.wav) | bin | 32002 -> 32002 bytes | |||
| -rw-r--r-- | mumd/src/audio/output.rs | 2 | ||||
| -rw-r--r-- | mumd/src/audio/sound_effects.rs | 216 | ||||
| -rw-r--r-- | mumd/src/notifications.rs | 1 | ||||
| -rw-r--r-- | mumd/src/state.rs | 17 |
10 files changed, 280 insertions, 138 deletions
@@ -26,6 +26,7 @@ Added * Invalid server certificates are now rejected by default and need to be explicitly allowed either when connecting or permanently per server or globally. + * .ogg sound effects. Changed ~~~~~~~ @@ -45,6 +46,7 @@ Fixed * Lots of other minor informative error messages instead of panics. * Status requests are sent in parallel. * Pings are now less spammy in the log output. + * Sound output is stereo by default. Other ~~~~~ @@ -687,6 +687,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] name = "libc" version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -825,6 +836,7 @@ dependencies = [ "futures-channel", "futures-util", "hound", + "lewton", "libnotify", "log", "mumble-protocol", @@ -1032,6 +1044,15 @@ dependencies = [ ] [[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] name = "once_cell" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1533,6 +1554,21 @@ dependencies = [ ] [[package]] +name = "tinyvec" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] name = "tokio" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -53,9 +53,10 @@ them, build with --no-default-features. Features can then be enabled with The following features can be specified: -| Name | Needed for | -|--------------------|---------------| -| mumd/notifications | Notifications | +| Name | Needed for | +|--------------------+--------------------| +| mumd/notifications | Notifications | +| mumd/ogg | .ogg sound effects | If you're using Cargo 1.51 or later you can specify features directly from the workspace root: diff --git a/mumd/Cargo.toml b/mumd/Cargo.toml index 28ff2ce..02a069c 100644 --- a/mumd/Cargo.toml +++ b/mumd/Cargo.toml @@ -12,9 +12,10 @@ license = "MIT" readme = "../README.md" [features] -default = ["notifications"] +default = ["notifications", "ogg"] notifications = ["libnotify"] +ogg = ["lewton"] [dependencies] mumlib = { version = "0.4", path = "../mumlib" } @@ -40,7 +41,8 @@ tokio-util = { version = "0.6", features = ["codec", "net"] } bincode = "1.3.2" chrono = "0.4" -libnotify = { version = "1.0", optional = true } +libnotify = { version = "1", optional = true } +lewton = { version = "0.10", optional = true } #compressor = "0.3" #daemonize = "0.4" diff --git a/mumd/src/audio.rs b/mumd/src/audio.rs index 6860741..bbaf2e1 100644 --- a/mumd/src/audio.rs +++ b/mumd/src/audio.rs @@ -4,74 +4,30 @@ pub mod input; pub mod output; +pub mod sound_effects; pub mod transformers; -use crate::audio::input::{AudioInputDevice, DefaultAudioInputDevice}; -use crate::audio::output::{AudioOutputDevice, ClientStream, DefaultAudioOutputDevice}; use crate::error::AudioError; use crate::network::VoiceStreamType; use crate::state::StatePhase; -use dasp_interpolate::linear::Linear; -use dasp_signal::{self as signal, Signal}; use futures_util::stream::Stream; use futures_util::StreamExt; -use log::*; use mumble_protocol::voice::{VoicePacket, VoicePacketPayload}; use mumble_protocol::Serverbound; use mumlib::config::SoundEffect; -use std::borrow::Cow; use std::collections::{hash_map::Entry, HashMap}; -use std::convert::TryFrom; use std::fmt::Debug; -use std::fs::File; -use std::io::Read; use std::sync::{Arc, Mutex}; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; use tokio::sync::watch; +use self::input::{AudioInputDevice, DefaultAudioInputDevice}; +use self::output::{AudioOutputDevice, ClientStream, DefaultAudioOutputDevice}; +use self::sound_effects::NotificationEvents; + /// The sample rate used internally. const SAMPLE_RATE: u32 = 48000; -/// All types of notifications that can be shown. Each notification can be bound to its own audio -/// file. -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, EnumIter)] -pub enum NotificationEvents { - ServerConnect, - ServerDisconnect, - UserConnected, - UserDisconnected, - UserJoinedChannel, - UserLeftChannel, - Mute, - Unmute, - Deafen, - Undeafen, -} - -impl TryFrom<&str> for NotificationEvents { - type Error = (); - fn try_from(s: &str) -> Result<Self, Self::Error> { - match s { - "server_connect" => Ok(NotificationEvents::ServerConnect), - "server_disconnect" => Ok(NotificationEvents::ServerDisconnect), - "user_connected" => Ok(NotificationEvents::UserConnected), - "user_disconnected" => Ok(NotificationEvents::UserDisconnected), - "user_joined_channel" => Ok(NotificationEvents::UserJoinedChannel), - "user_left_channel" => Ok(NotificationEvents::UserLeftChannel), - "mute" => Ok(NotificationEvents::Mute), - "unmute" => Ok(NotificationEvents::Unmute), - "deafen" => Ok(NotificationEvents::Deafen), - "undeafen" => Ok(NotificationEvents::Undeafen), - _ => { - warn!("Unknown notification event '{}' in config", s); - Err(()) - } - } - } -} - /// Input audio state. Input audio is picket up from an [AudioInputDevice] (e.g. /// a microphone) and sent over the network. pub struct AudioInput { @@ -168,61 +124,10 @@ impl AudioOutput { Ok(res) } - /// Loads sound effects, getting unspecified effects from [get_default_sfx]. - pub fn load_sound_effects(&mut self, sound_effects: &[SoundEffect]) { - let overrides: HashMap<_, _> = sound_effects - .iter() - .filter_map(|sound_effect| { - let (event, file) = (&sound_effect.event, &sound_effect.file); - if let Ok(event) = NotificationEvents::try_from(event.as_str()) { - Some((event, file)) - } else { - None - } - }) - .collect(); - - // This makes sure that every [NotificationEvent] is present in [self.sounds]. - self.sounds = NotificationEvents::iter() - .map(|event| { - let bytes = overrides - .get(&event) - .map(|file| get_sfx(file)) - .unwrap_or_else(get_default_sfx); - let reader = hound::WavReader::new(bytes.as_ref()).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 iter: Box<dyn Iterator<Item = f32>> = match spec.channels { - 1 => Box::new(samples.into_iter().flat_map(|e| vec![e, e])), - 2 => Box::new(samples.into_iter()), - _ => unimplemented!("Only mono and stereo sound is supported. See #80."), - }; - let mut signal = signal::from_interleaved_samples_iter::<_, [f32; 2]>(iter); - let interp = Linear::new(Signal::next(&mut signal), Signal::next(&mut signal)); - let samples = signal - .from_hz_to_hz(interp, spec.sample_rate as f64, SAMPLE_RATE as f64) - .until_exhausted() - // if the source audio is stereo and is being played as mono, discard the right audio - .flat_map(|e| { - if self.device.num_channels() == 1 { - vec![e[0]] - } else { - e.to_vec() - } - }) - .collect::<Vec<f32>>(); - (event, samples) - }) - .collect(); + /// Sets the sound effects according to some overrides, using some default + /// value if an event isn't overriden. + pub fn load_sound_effects(&mut self, overrides: &[SoundEffect]) { + self.sounds = sound_effects::load_sound_effects(overrides, self.device.num_channels()); } /// Decodes a voice packet. @@ -273,21 +178,3 @@ impl AudioOutput { self.client_streams.lock().unwrap().add_sound_effect(samples); } } - -/// Reads a sound effect from disk. -// moo -fn get_sfx(file: &str) -> Cow<'static, [u8]> { - let mut buf: Vec<u8> = Vec::new(); - if let Ok(mut file) = File::open(file) { - file.read_to_end(&mut buf).unwrap(); - Cow::from(buf) - } else { - warn!("File not found: '{}'", file); - get_default_sfx() - } -} - -/// Gets the default sound effect. -fn get_default_sfx() -> Cow<'static, [u8]> { - Cow::from(include_bytes!("fallback_sfx.wav").as_ref()) -} diff --git a/mumd/src/fallback_sfx.wav b/mumd/src/audio/fallback_sfx.wav Binary files differindex 82ee4d4..82ee4d4 100644 --- a/mumd/src/fallback_sfx.wav +++ b/mumd/src/audio/fallback_sfx.wav diff --git a/mumd/src/audio/output.rs b/mumd/src/audio/output.rs index 6cec6fc..980e65f 100644 --- a/mumd/src/audio/output.rs +++ b/mumd/src/audio/output.rs @@ -183,7 +183,7 @@ impl DefaultAudioOutputDevice { .supported_output_configs() .map_err(|e| AudioError::NoConfigs(AudioStream::Output, e))? .find_map(|c| { - if c.min_sample_rate() <= sample_rate && c.max_sample_rate() >= sample_rate { + if c.min_sample_rate() <= sample_rate && c.max_sample_rate() >= sample_rate && c.channels() == 2 { Some(c) } else { None diff --git a/mumd/src/audio/sound_effects.rs b/mumd/src/audio/sound_effects.rs new file mode 100644 index 0000000..63f08bd --- /dev/null +++ b/mumd/src/audio/sound_effects.rs @@ -0,0 +1,216 @@ +use dasp_interpolate::linear::Linear; +use dasp_signal::{self as signal, Signal}; +use log::warn; +use mumlib::config::SoundEffect; +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fs::File; +#[cfg(feature = "ogg")] +use std::io::Cursor; +use std::io::Read; +use std::path::Path; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +use crate::audio::SAMPLE_RATE; + +/// The different kinds of files we can open. +enum AudioFileKind { + Ogg, + Wav, +} + +impl TryFrom<&str> for AudioFileKind { + type Error = (); + + fn try_from(s: &str) -> Result<Self, Self::Error> { + match s { + "ogg" => Ok(AudioFileKind::Ogg), + "wav" => Ok(AudioFileKind::Wav), + _ => Err(()), + } + } +} + +/// A specification accompanying some audio data. +struct AudioSpec { + channels: u32, + sample_rate: u32, +} + +/// An event where a notification is shown and a sound effect is played. +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, EnumIter)] +pub enum NotificationEvents { + ServerConnect, + ServerDisconnect, + UserConnected, + UserDisconnected, + UserJoinedChannel, + UserLeftChannel, + Mute, + Unmute, + Deafen, + Undeafen, +} + +impl TryFrom<&str> for NotificationEvents { + type Error = (); + + fn try_from(s: &str) -> Result<Self, Self::Error> { + match s { + "server_connect" => Ok(NotificationEvents::ServerConnect), + "server_disconnect" => Ok(NotificationEvents::ServerDisconnect), + "user_connected" => Ok(NotificationEvents::UserConnected), + "user_disconnected" => Ok(NotificationEvents::UserDisconnected), + "user_joined_channel" => Ok(NotificationEvents::UserJoinedChannel), + "user_left_channel" => Ok(NotificationEvents::UserLeftChannel), + "mute" => Ok(NotificationEvents::Mute), + "unmute" => Ok(NotificationEvents::Unmute), + "deafen" => Ok(NotificationEvents::Deafen), + "undeafen" => Ok(NotificationEvents::Undeafen), + _ => { + Err(()) + } + } + } +} + +/// Loads files into an "event -> data"-map, with support for overriding +/// specific events with another sound file. +pub fn load_sound_effects(overrides: &[SoundEffect], num_channels: usize) -> HashMap<NotificationEvents, Vec<f32>> { + let overrides: HashMap<_, _> = overrides + .iter() + .filter_map(|sound_effect| { + let (event, file) = (&sound_effect.event, &sound_effect.file); + if let Ok(event) = NotificationEvents::try_from(event.as_str()) { + Some((event, file)) + } else { + warn!("Unknown notification event '{}'", event); + None + } + }) + .collect(); + + // Construct a hashmap that maps every [NotificationEvent] to a vector of + // plain floating point audio data with the global sample rate as a + // Vec<f32>. We do this by iterating over all [NotificationEvent]-variants + // and opening either the file passed as an override or the fallback sound + // effect (if omitted). We then use dasp to convert to the correct sample rate. + NotificationEvents::iter() + .map(|event| { + let file = overrides.get(&event); + // Try to open the file if overriden, otherwise use the default sound effect. + let (data, kind) = file + .and_then(|file| { + // Try to get the file kind from the extension. + let kind = file + .split('.') + .last() + .and_then(|ext| AudioFileKind::try_from(ext).ok())?; + Some((get_sfx(file), kind)) + }) + .unwrap_or_else(|| (get_default_sfx(), AudioFileKind::Wav)); + // Unpack the samples. + let (samples, spec) = unpack_audio(data, kind); + // If the audio is mono (single channel), pad every sample with + // itself, since we later assume that audio is stored interleaved as + // LRLRLR (or RLRLRL). Without this, mono audio would be played in + // double speed. + let iter: Box<dyn Iterator<Item = f32>> = match spec.channels { + 1 => Box::new(samples.into_iter().flat_map(|e| [e, e])), + 2 => Box::new(samples.into_iter()), + _ => unimplemented!("Only mono and stereo sound is supported. See #80."), + }; + // Create a dasp signal containing stereo sound. + let mut signal = signal::from_interleaved_samples_iter::<_, [f32; 2]>(iter); + // Create a linear interpolator, in case we need to convert the sample rate. + let interp = Linear::new(Signal::next(&mut signal), Signal::next(&mut signal)); + // Create our resulting samples. + let samples = signal + .from_hz_to_hz(interp, spec.sample_rate as f64, SAMPLE_RATE as f64) + .until_exhausted() + // If the source audio is stereo and is being played as mono, discard the first channel. + .flat_map(|e| { + if num_channels == 1 { + vec![e[0]] + } else { + e.to_vec() + } + }) + .collect::<Vec<f32>>(); + (event, samples) + }) + .collect() +} + +/// Unpack audio data. The required audio spec is read from the file and returned as well. +fn unpack_audio(data: Cow<'_, [u8]>, kind: AudioFileKind) -> (Vec<f32>, AudioSpec) { + match kind { + AudioFileKind::Ogg => unpack_ogg(data), + AudioFileKind::Wav => unpack_wav(data), + } +} + +#[cfg(feature = "ogg")] +/// Unpack ogg data. +fn unpack_ogg(data: Cow<'_, [u8]>) -> (Vec<f32>, AudioSpec) { + let mut reader = lewton::inside_ogg::OggStreamReader::new(Cursor::new(data.as_ref())).unwrap(); + let mut samples = Vec::new(); + while let Ok(Some(mut frame)) = reader.read_dec_packet_itl() { + samples.append(&mut frame); + } + let samples = samples.iter().map(|s| cpal::Sample::to_f32(s)).collect(); + let spec = AudioSpec { + channels: reader.ident_hdr.audio_channels as u32, + sample_rate: reader.ident_hdr.audio_sample_rate, + }; + (samples, spec) +} + +#[cfg(not(feature = "ogg"))] +/// Fallback to default sound effect since ogg is disabled. +fn unpack_ogg(_: Cow<'_, [u8]>) -> (Vec<f32>, AudioSpec) { + warn!("Can't open .ogg without the ogg-feature enabled."); + unpack_wav(get_default_sfx()) +} + +/// Unpack wav data. +fn unpack_wav(data: Cow<'_, [u8]>) -> (Vec<f32>, AudioSpec) { + let reader = hound::WavReader::new(data.as_ref()).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 spec = AudioSpec { + channels: spec.channels as u32, + sample_rate: spec.sample_rate, + }; + (samples, spec) +} + +/// Open and return the data contained in a file, or the default sound effect if +/// the file couldn't be found. +// moo +fn get_sfx<P: AsRef<Path>>(file: P) -> Cow<'static, [u8]> { + let mut buf: Vec<u8> = Vec::new(); + if let Ok(mut file) = File::open(file.as_ref()) { + file.read_to_end(&mut buf).unwrap(); + Cow::from(buf) + } else { + warn!("File not found: '{}'", file.as_ref().display()); + get_default_sfx() + } +} + +/// Get the default sound effect. +fn get_default_sfx() -> Cow<'static, [u8]> { + Cow::from(include_bytes!("fallback_sfx.wav").as_ref()) +} diff --git a/mumd/src/notifications.rs b/mumd/src/notifications.rs index bccf4dd..5094a07 100644 --- a/mumd/src/notifications.rs +++ b/mumd/src/notifications.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "notifications")] use log::*; pub fn init() { diff --git a/mumd/src/state.rs b/mumd/src/state.rs index 1992884..ba461fd 100644 --- a/mumd/src/state.rs +++ b/mumd/src/state.rs @@ -2,9 +2,9 @@ pub mod channel; pub mod server; pub mod user; -use crate::{audio::{AudioInput, AudioOutput, NotificationEvents}, network::tcp::DisconnectedReason}; +use crate::audio::{AudioInput, AudioOutput, sound_effects::NotificationEvents}; use crate::error::StateError; -use crate::network::tcp::{TcpEvent, TcpEventData}; +use crate::network::tcp::{DisconnectedReason, TcpEvent, TcpEventData}; use crate::network::{ConnectionInfo, VoiceStreamType}; use crate::notifications; use crate::state::server::Server; @@ -12,19 +12,16 @@ use crate::state::user::UserDiff; use chrono::NaiveDateTime; use log::*; -use mumble_protocol::control::msgs; -use mumble_protocol::control::ControlPacket; +use mumble_protocol::control::{ControlPacket, msgs}; use mumble_protocol::ping::PongPacket; use mumble_protocol::voice::Serverbound; use mumlib::command::{ChannelTarget, Command, CommandResponse, MessageTarget, MumbleEvent, MumbleEventKind}; use mumlib::config::Config; use mumlib::Error; -use std::{ - fmt::Debug, - iter, - net::{SocketAddr, ToSocketAddrs}, - sync::{Arc, RwLock}, -}; +use std::fmt::Debug; +use std::iter; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::sync::{Arc, RwLock}; use tokio::sync::{mpsc, watch}; macro_rules! at { |
