aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGustav Sörnäs <gustav@sornas.net>2021-06-20 22:26:07 +0200
committerGitHub <noreply@github.com>2021-06-20 22:26:07 +0200
commit9ccddaeda2800f9b2323b3e2f75c5758a2341747 (patch)
treeab94c8658226afa6915493401a668b01cfddef4e
parent1b61ae7a8834db3278fcecb82cd066d5c15ddcf9 (diff)
parent3574c2c0b990afb251f96901df02e0eb4518e1c7 (diff)
downloadmum-9ccddaeda2800f9b2323b3e2f75c5758a2341747.tar.gz
Merge pull request #109 from mum-rs/ogg
-rw-r--r--CHANGELOG2
-rw-r--r--Cargo.lock36
-rw-r--r--README.md7
-rw-r--r--mumd/Cargo.toml6
-rw-r--r--mumd/src/audio.rs131
-rw-r--r--mumd/src/audio/fallback_sfx.wav (renamed from mumd/src/fallback_sfx.wav)bin32002 -> 32002 bytes
-rw-r--r--mumd/src/audio/output.rs2
-rw-r--r--mumd/src/audio/sound_effects.rs216
-rw-r--r--mumd/src/notifications.rs1
-rw-r--r--mumd/src/state.rs17
10 files changed, 280 insertions, 138 deletions
diff --git a/CHANGELOG b/CHANGELOG
index ef99d3d..b707bb5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
~~~~~
diff --git a/Cargo.lock b/Cargo.lock
index 1161c63..8d9a66a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/README.md b/README.md
index 9e62a3c..c3a7fad 100644
--- a/README.md
+++ b/README.md
@@ -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
index 82ee4d4..82ee4d4 100644
--- a/mumd/src/fallback_sfx.wav
+++ b/mumd/src/audio/fallback_sfx.wav
Binary files differ
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 {