aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock49
-rw-r--r--README.md12
-rw-r--r--mumctl/src/main.rs37
-rw-r--r--mumd/Cargo.toml4
-rw-r--r--mumd/src/audio.rs39
-rw-r--r--mumd/src/audio/input.rs35
-rw-r--r--mumd/src/audio/output.rs134
-rw-r--r--mumd/src/audio/transformers.rs7
-rw-r--r--mumd/src/client.rs2
-rw-r--r--mumd/src/error.rs6
-rw-r--r--mumd/src/main.rs38
-rw-r--r--mumd/src/network.rs4
-rw-r--r--mumd/src/network/tcp.rs41
-rw-r--r--mumd/src/network/udp.rs19
-rw-r--r--mumd/src/state.rs43
-rw-r--r--mumd/src/state/channel.rs13
-rw-r--r--mumd/src/state/server.rs2
-rw-r--r--mumd/src/state/user.rs2
-rw-r--r--mumlib/src/command.rs62
-rw-r--r--mumlib/src/config.rs58
-rw-r--r--mumlib/src/lib.rs24
-rw-r--r--mumlib/src/state.rs41
22 files changed, 511 insertions, 161 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2f4bb07..1161c63 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
+version = 3
+
[[package]]
name = "alsa"
version = "0.5.0"
@@ -686,9 +688,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
-version = "0.2.95"
+version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
+checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
[[package]]
name = "libloading"
@@ -817,10 +819,8 @@ dependencies = [
"bytes",
"chrono",
"cpal",
- "dasp_frame",
"dasp_interpolate",
"dasp_ring_buffer",
- "dasp_sample",
"dasp_signal",
"futures-channel",
"futures-util",
@@ -830,14 +830,12 @@ dependencies = [
"mumble-protocol",
"mumlib",
"native-tls",
- "openssl",
"opus",
"serde",
"strum",
"strum_macros",
"tokio",
"tokio-native-tls",
- "tokio-stream",
"tokio-util",
]
@@ -1035,9 +1033,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.7.2"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "openssl"
@@ -1203,24 +1201,24 @@ dependencies = [
[[package]]
name = "protobuf"
-version = "2.23.0"
+version = "2.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45604fc7a88158e7d514d8e22e14ac746081e7a70d7690074dd0029ee37458d6"
+checksum = "db50e77ae196458ccd3dc58a31ea1a90b0698ab1b7928d89f644c25d72070267"
[[package]]
name = "protobuf-codegen"
-version = "2.23.0"
+version = "2.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb87f342b585958c1c086313dbc468dcac3edf5e90362111c26d7a58127ac095"
+checksum = "09321cef9bee9ddd36884f97b7f7cc92a586cdc74205c4b3aeba65b5fc9c6f90"
dependencies = [
"protobuf",
]
[[package]]
name = "protobuf-codegen-pure"
-version = "2.23.0"
+version = "2.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ca6e0e2f898f7856a6328650abc9b2df71b7c1a5f39be0800d19051ad0214b2"
+checksum = "1afb68a6d768571da3db86ce55f0f62966e0fc25eaf96acd070ea548a91b0d23"
dependencies = [
"protobuf",
"protobuf-codegen",
@@ -1249,9 +1247,9 @@ dependencies = [
[[package]]
name = "rand_chacha"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
@@ -1351,9 +1349,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
-version = "2.3.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b239a3d5db51252f6f48f42172c65317f37202f4a21021bf5f9d40a408f4592c"
+checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
dependencies = [
"bitflags",
"core-foundation",
@@ -1472,9 +1470,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "1.0.72"
+version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
+checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
dependencies = [
"proc-macro2",
"quote",
@@ -1570,17 +1568,6 @@ dependencies = [
]
[[package]]
-name = "tokio-stream"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066"
-dependencies = [
- "futures-core",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
name = "tokio-util"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/README.md b/README.md
index 93ae6f2..e8a4946 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,18 @@ ServerRoot
$ mumctl channel connect Channel2
```
+## Known issues
+
+The main hub for issues is [our issue
+tracker](https://github.com/mum-rs/mum/issues). Additionally, there are some
+features that aren't present on the issue tracker:
+
+- Administration tools. See [the admin tools
+ project](https://github.com/mum-rs/mum/projects/1).
+- Surround output. If this is something you want, [open an
+ issue](https://github.com/mum-rs/mum/issues/new) so we can take a look at
+ implementing it.
+
## Why?
Mostly because it's a fun way of learning a new language. Also:
diff --git a/mumctl/src/main.rs b/mumctl/src/main.rs
index fbeb0cd..7f428eb 100644
--- a/mumctl/src/main.rs
+++ b/mumctl/src/main.rs
@@ -1,5 +1,20 @@
+#![warn(elided_lifetimes_in_paths)]
+#![warn(meta_variable_misuse)]
+#![warn(missing_debug_implementations)]
+#![warn(single_use_lifetimes)]
+#![warn(unreachable_pub)]
+#![warn(unused_crate_dependencies)]
+#![warn(unused_import_braces)]
+#![warn(unused_lifetimes)]
+#![warn(unused_qualifications)]
+#![deny(macro_use_extern_crate)]
+#![deny(missing_abi)]
+#![deny(future_incompatible)]
+#![forbid(unsafe_code)]
+#![forbid(non_ascii_idents)]
+
use colored::Colorize;
-use log::*;
+use log::{Level, LevelFilter, Metadata, Record, error, warn};
use mumlib::command::{ChannelTarget, Command as MumCommand, CommandResponse, MessageTarget};
use mumlib::config::{self, Config, ServerConfig};
use mumlib::state::Channel as MumChannel;
@@ -17,11 +32,11 @@ const INDENTATION: &str = " ";
struct SimpleLogger;
impl log::Log for SimpleLogger {
- fn enabled(&self, metadata: &Metadata) -> bool {
+ fn enabled(&self, metadata: &Metadata<'_>) -> bool {
metadata.level() <= Level::Info
}
- fn log(&self, record: &Record) {
+ fn log(&self, record: &Record<'_>) {
if self.enabled(record.metadata()) {
println!(
"{}{}",
@@ -256,9 +271,9 @@ fn match_opt() -> Result<(), Error> {
server
.username
.as_ref()
- .or(username.as_ref())
+ .or_else(|| username.as_ref())
.ok_or(CliError::NoUsername)?,
- server.password.as_ref().or(password.as_ref()),
+ server.password.as_ref().or_else(|| password.as_ref()),
server.port.unwrap_or(port),
server.accept_invalid_cert,
),
@@ -525,7 +540,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<(
(Some("port"), None) => {
println!(
"{}",
- server.port.ok_or(CliError::NotSet("port".to_string()))?
+ server.port.ok_or_else(|| CliError::NotSet("port".to_string()))?
);
}
(Some("username"), None) => {
@@ -534,7 +549,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<(
server
.username
.as_ref()
- .ok_or(CliError::NotSet("username".to_string()))?
+ .ok_or_else(|| CliError::NotSet("username".to_string()))?
);
}
(Some("password"), None) => {
@@ -543,7 +558,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<(
server
.password
.as_ref()
- .ok_or(CliError::NotSet("password".to_string()))?
+ .ok_or_else(|| CliError::NotSet("password".to_string()))?
);
}
(Some("accept_invalid_cert"), None) => {
@@ -552,11 +567,11 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<(
server
.accept_invalid_cert
.map(|b| b.to_string())
- .ok_or(CliError::NotSet("accept_invalid_cert".to_string()))?
+ .ok_or_else(|| CliError::NotSet("accept_invalid_cert".to_string()))?
);
}
(Some("name"), Some(_)) => {
- return Err(CliError::UseServerRename)?;
+ return Err(CliError::UseServerRename.into());
}
(Some("host"), Some(value)) => {
server.host = value;
@@ -578,7 +593,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<(
}
}
(Some(_), _) => {
- return Err(CliError::ConfigKeyNotFound(key.unwrap()))?;
+ return Err(CliError::ConfigKeyNotFound(key.unwrap()).into());
}
}
}
diff --git a/mumd/Cargo.toml b/mumd/Cargo.toml
index 1e8e63f..28ff2ce 100644
--- a/mumd/Cargo.toml
+++ b/mumd/Cargo.toml
@@ -23,8 +23,6 @@ cpal = "0.13"
bytes = "1.0"
dasp_interpolate = { version = "0.11", features = ["linear"] }
dasp_signal = "0.11"
-dasp_frame = "0.11"
-dasp_sample = "0.11"
dasp_ring_buffer = "0.11"
futures-util = { version = "0.3", features = ["sink"]}
futures-channel = "0.3"
@@ -32,13 +30,11 @@ hound = "3.4"
log = "0.4"
mumble-protocol = "0.4.1"
native-tls = "0.2"
-openssl = { version = "0.10" }
opus = "0.2"
serde = { version = "1.0", features = ["derive"] }
strum = "0.20"
strum_macros = "0.20"
tokio = { version = "1.0", features = ["macros", "rt", "rt-multi-thread", "sync", "net", "time", "fs"] }
-tokio-stream = "0.1.0"
tokio-native-tls = "0.3"
tokio-util = { version = "0.6", features = ["codec", "net"] }
bincode = "1.3.2"
diff --git a/mumd/src/audio.rs b/mumd/src/audio.rs
index 2e20583..6860741 100644
--- a/mumd/src/audio.rs
+++ b/mumd/src/audio.rs
@@ -1,3 +1,7 @@
+//! All things audio.
+//!
+//! Audio is handled mostly as signals from [dasp_signal]. Input/output is handled by [cpal].
+
pub mod input;
pub mod output;
pub mod transformers;
@@ -27,8 +31,11 @@ use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use tokio::sync::watch;
+/// 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,
@@ -65,9 +72,12 @@ impl TryFrom<&str> for NotificationEvents {
}
}
+/// Input audio state. Input audio is picket up from an [AudioInputDevice] (e.g.
+/// a microphone) and sent over the network.
pub struct AudioInput {
device: DefaultAudioInputDevice,
+ /// Outgoing voice packets that should be sent over the network.
channel_receiver:
Arc<tokio::sync::Mutex<Box<dyn Stream<Item = VoicePacket<Serverbound>> + Unpin>>>,
}
@@ -112,12 +122,30 @@ impl AudioInput {
}
}
+impl Debug for AudioInput {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("AudioInput")
+ .field("device", &self.device)
+ .field("channel_receiver", &"receiver")
+ .finish()
+ }
+}
+
+#[derive(Debug)]
+/// Audio output state. The audio is received from each client over the network,
+/// decoded, merged and finally played to an [AudioOutputDevice] (e.g. speaker,
+/// headphones, ...).
pub struct AudioOutput {
device: DefaultAudioOutputDevice,
+ /// The volume and mute-status of a user ID.
user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>,
+ /// The client stream per user ID. A separate stream is kept for UDP and TCP.
+ ///
+ /// Shared with [DefaultAudioOutputDevice].
client_streams: Arc<Mutex<ClientStream>>,
+ /// Which sound effect should be played on an event.
sounds: HashMap<NotificationEvents, Vec<f32>>,
}
@@ -140,6 +168,7 @@ 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()
@@ -153,6 +182,7 @@ impl AudioOutput {
})
.collect();
+ // This makes sure that every [NotificationEvent] is present in [self.sounds].
self.sounds = NotificationEvents::iter()
.map(|event| {
let bytes = overrides
@@ -195,6 +225,7 @@ impl AudioOutput {
.collect();
}
+ /// Decodes a voice packet.
pub fn decode_packet_payload(
&self,
stream_type: VoiceStreamType,
@@ -207,10 +238,12 @@ impl AudioOutput {
.decode_packet((stream_type, session_id), payload);
}
+ /// Sets the volume of the output device.
pub fn set_volume(&self, output_volume: f32) {
self.device.set_volume(output_volume);
}
+ /// Sets the incoming volume of a user.
pub fn set_user_volume(&self, id: u32, volume: f32) {
match self.user_volumes.lock().unwrap().entry(id) {
Entry::Occupied(mut entry) => {
@@ -222,6 +255,7 @@ impl AudioOutput {
}
}
+ /// Mutes another user.
pub fn set_mute(&self, id: u32, mute: bool) {
match self.user_volumes.lock().unwrap().entry(id) {
Entry::Occupied(mut entry) => {
@@ -233,12 +267,14 @@ impl AudioOutput {
}
}
+ /// Queues a sound effect.
pub fn play_effect(&self, effect: NotificationEvents) {
let samples = self.sounds.get(&effect).unwrap();
- self.client_streams.lock().unwrap().extend(None, samples);
+ 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();
@@ -251,6 +287,7 @@ fn get_sfx(file: &str) -> Cow<'static, [u8]> {
}
}
+/// 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/audio/input.rs b/mumd/src/audio/input.rs
index a1227e3..4dfc465 100644
--- a/mumd/src/audio/input.rs
+++ b/mumd/src/audio/input.rs
@@ -1,6 +1,8 @@
+//! Listens to the microphone and sends it to the networking.
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{InputCallbackInfo, Sample, SampleFormat, SampleRate, StreamConfig};
use log::*;
+use std::fmt::Debug;
use tokio::sync::watch;
use crate::audio::SAMPLE_RATE;
@@ -8,6 +10,7 @@ use crate::audio::transformers::{NoiseGate, Transformer};
use crate::error::{AudioError, AudioStream};
use crate::state::StatePhase;
+/// Generates a callback that receives [Sample]s and sends them as floats to a [futures_channel::mpsc::Sender].
pub fn callback<T: Sample>(
mut input_sender: futures_channel::mpsc::Sender<Vec<u8>>,
mut transformers: Vec<Box<dyn Transformer + Send + 'static>>,
@@ -29,8 +32,8 @@ pub fn callback<T: Sample>(
buffer.extend(data.by_ref().take(buffer_size - buffer.len()));
let encoded = transformers
.iter_mut()
- .try_fold(&mut buffer[..], |acc, e| e.transform(acc))
- .map(|buf| opus_encoder.encode_vec_float(&*buf, buffer_size).unwrap());
+ .try_fold((opus::Channels::Mono, &mut buffer[..]), |acc, e| e.transform(acc))
+ .map(|buf| opus_encoder.encode_vec_float(&*buf.1, buffer_size).unwrap());
if let Some(encoded) = encoded {
if let Err(e) = input_sender.try_send(encoded) {
@@ -43,11 +46,19 @@ pub fn callback<T: Sample>(
}
}
+/// Something that can listen to audio and send it somewhere.
+///
+/// One sample is assumed to be an encoded opus frame. See [opus::Encoder].
pub trait AudioInputDevice {
+ /// Starts the device.
fn play(&self) -> Result<(), AudioError>;
+ /// Stops the device.
fn pause(&self) -> Result<(), AudioError>;
+ /// Sets the input volume of the device.
fn set_volume(&self, volume: f32);
+ /// Returns a receiver to this device's values.
fn sample_receiver(&mut self) -> Option<futures_channel::mpsc::Receiver<Vec<u8>>>;
+ /// The amount of channels this device has.
fn num_channels(&self) -> usize;
}
@@ -59,6 +70,7 @@ pub struct DefaultAudioInputDevice {
}
impl DefaultAudioInputDevice {
+ /// Initializes the default audio input.
pub fn new(
input_volume: f32,
phase_watcher: watch::Receiver<StatePhase>,
@@ -160,20 +172,35 @@ impl AudioInputDevice for DefaultAudioInputDevice {
fn play(&self) -> Result<(), AudioError> {
self.stream
.play()
- .map_err(|e| AudioError::InputPlayError(e))
+ .map_err(AudioError::InputPlayError)
}
+
fn pause(&self) -> Result<(), AudioError> {
self.stream
.pause()
- .map_err(|e| AudioError::InputPauseError(e))
+ .map_err(AudioError::InputPauseError)
}
+
fn set_volume(&self, volume: f32) {
self.volume_sender.send(volume).unwrap();
}
+
fn sample_receiver(&mut self) -> Option<futures_channel::mpsc::Receiver<Vec<u8>>> {
self.sample_receiver.take()
}
+
fn num_channels(&self) -> usize {
self.channels as usize
}
}
+
+impl Debug for DefaultAudioInputDevice {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("DefaultAudioInputDevice")
+ .field("sample_receiver", &self.sample_receiver)
+ .field("channels", &self.channels)
+ .field("volume_sender", &self.volume_sender)
+ .field("stream", &"cpal::Stream")
+ .finish()
+ }
+}
diff --git a/mumd/src/audio/output.rs b/mumd/src/audio/output.rs
index a2f6bcc..6cec6fc 100644
--- a/mumd/src/audio/output.rs
+++ b/mumd/src/audio/output.rs
@@ -1,23 +1,79 @@
+//! Receives audio packets from the networking and plays them.
+
use crate::audio::SAMPLE_RATE;
use crate::error::{AudioError, AudioStream};
use crate::network::VoiceStreamType;
+use bytes::Bytes;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{OutputCallbackInfo, Sample, SampleFormat, SampleRate, StreamConfig};
+use dasp_ring_buffer::Bounded;
use log::*;
use mumble_protocol::voice::VoicePacketPayload;
use std::collections::{HashMap, VecDeque};
+use std::fmt::Debug;
+use std::iter;
use std::ops::AddAssign;
use std::sync::{Arc, Mutex};
use tokio::sync::watch;
type ClientStreamKey = (VoiceStreamType, u32);
+/// State for decoding audio received from another user.
+#[derive(Debug)]
+pub struct ClientAudioData {
+ buf: Bounded<Vec<f32>>,
+ output_channels: opus::Channels,
+ // We need both since a client can hypothetically send both mono
+ // and stereo packets, and we can't switch a decoder on the fly
+ // to reuse it.
+ mono_decoder: opus::Decoder,
+ stereo_decoder: opus::Decoder,
+}
+
+impl ClientAudioData {
+ pub fn new(sample_rate: u32, output_channels: opus::Channels) -> Self {
+ Self {
+ mono_decoder: opus::Decoder::new(sample_rate, opus::Channels::Mono).unwrap(),
+ stereo_decoder: opus::Decoder::new(sample_rate, opus::Channels::Stereo).unwrap(),
+ output_channels,
+ buf: Bounded::from_full(vec![0.0; sample_rate as usize * output_channels as usize]), //buffer 1 s of audio
+ }
+ }
+
+ pub fn store_packet(&mut self, bytes: Bytes) {
+ let packet_channels = opus::packet::get_nb_channels(&bytes).unwrap();
+ let (decoder, channels) = match packet_channels {
+ opus::Channels::Mono => (&mut self.mono_decoder, 1),
+ opus::Channels::Stereo => (&mut self.stereo_decoder, 2),
+ };
+ let mut out: Vec<f32> = vec![0.0; 720 * channels * 4]; //720 is because that is the max size of packet we can get that we want to decode
+ let parsed = decoder
+ .decode_float(&bytes, &mut out, false)
+ .expect("Error decoding");
+ out.truncate(parsed);
+ match (packet_channels, self.output_channels) {
+ (opus::Channels::Mono, opus::Channels::Mono) | (opus::Channels::Stereo, opus::Channels::Stereo) => for sample in out {
+ self.buf.push(sample);
+ },
+ (opus::Channels::Mono, opus::Channels::Stereo) => for sample in out {
+ self.buf.push(sample);
+ self.buf.push(sample);
+ },
+ (opus::Channels::Stereo, opus::Channels::Mono) => for sample in out.into_iter().step_by(2) {
+ self.buf.push(sample);
+ },
+ }
+ }
+}
+
+/// Collected state for client opus decoders and sound effects.
+#[derive(Debug)]
pub struct ClientStream {
- buffer_clients: HashMap<ClientStreamKey, (VecDeque<f32>, opus::Decoder)>, //TODO ring buffer?
+ buffer_clients: HashMap<ClientStreamKey, ClientAudioData>,
buffer_effects: VecDeque<f32>,
sample_rate: u32,
- channels: opus::Channels,
+ output_channels: opus::Channels,
}
impl ClientStream {
@@ -31,29 +87,21 @@ impl ClientStream {
buffer_clients: HashMap::new(),
buffer_effects: VecDeque::new(),
sample_rate,
- channels,
+ output_channels: channels,
}
}
- fn get_client(&mut self, client: ClientStreamKey) -> &mut (VecDeque<f32>, opus::Decoder) {
- let sample_rate = self.sample_rate;
- let channels = self.channels;
- self.buffer_clients.entry(client).or_insert_with(|| {
- let opus_decoder = opus::Decoder::new(sample_rate, channels).unwrap();
- (VecDeque::new(), opus_decoder)
- })
+ fn get_client(&mut self, client: ClientStreamKey) -> &mut ClientAudioData {
+ self.buffer_clients.entry(client).or_insert(
+ ClientAudioData::new(self.sample_rate, self.output_channels)
+ )
}
+ /// Decodes a voice packet.
pub fn decode_packet(&mut self, client: ClientStreamKey, payload: VoicePacketPayload) {
match payload {
VoicePacketPayload::Opus(bytes, _eot) => {
- let mut out: Vec<f32> = vec![0.0; 720 * (self.channels as usize) * 4]; //720 is because that is the max size of packet we can get that we want to decode
- let (buffer, decoder) = self.get_client(client);
- let parsed = decoder
- .decode_float(&bytes, &mut out, false)
- .expect("Error decoding");
- out.truncate(parsed);
- buffer.extend(&out);
+ self.get_client(client).store_packet(bytes);
}
_ => {
unimplemented!("Payload type not supported");
@@ -61,16 +109,19 @@ impl ClientStream {
}
}
- pub fn extend(&mut self, client: Option<ClientStreamKey>, values: &[f32]) {
- let buffer = match client {
- Some(x) => &mut self.get_client(x).0,
- None => &mut self.buffer_effects,
- };
- buffer.extend(values.iter().copied());
+ /// Extends the sound effect buffer queue with some received values.
+ pub fn add_sound_effect(&mut self, values: &[f32]) {
+ self.buffer_effects.extend(values.iter().copied());
}
}
+/// Adds two values in some saturating way.
+///
+/// Since we support [f32], [i16] and [u16] we need some way of adding two values
+/// without peaking above/below the edge values. This trait ensures that we can
+/// use all three primitive types as a generic parameter.
pub trait SaturatingAdd {
+ /// Adds two values in some saturating way. See trait documentation.
fn saturating_add(self, rhs: Self) -> Self;
}
@@ -104,14 +155,20 @@ pub trait AudioOutputDevice {
fn client_streams(&self) -> Arc<Mutex<ClientStream>>;
}
+/// The default audio output device, as determined by [cpal].
pub struct DefaultAudioOutputDevice {
config: StreamConfig,
stream: cpal::Stream,
+ /// The client stream per user ID. A separate stream is kept for UDP and TCP.
+ ///
+ /// Shared with [super::AudioOutput].
client_streams: Arc<Mutex<ClientStream>>,
+ /// Output volume configuration.
volume_sender: watch::Sender<f32>,
}
impl DefaultAudioOutputDevice {
+ /// Initializes the default audio output.
pub fn new(
output_volume: f32,
user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>,
@@ -148,7 +205,7 @@ impl DefaultAudioOutputDevice {
let output_stream = match output_supported_sample_format {
SampleFormat::F32 => output_device.build_output_stream(
&output_config,
- curry_callback::<f32>(
+ callback::<f32>(
Arc::clone(&client_streams),
output_volume_receiver,
user_volumes,
@@ -157,7 +214,7 @@ impl DefaultAudioOutputDevice {
),
SampleFormat::I16 => output_device.build_output_stream(
&output_config,
- curry_callback::<i16>(
+ callback::<i16>(
Arc::clone(&client_streams),
output_volume_receiver,
user_volumes,
@@ -166,7 +223,7 @@ impl DefaultAudioOutputDevice {
),
SampleFormat::U16 => output_device.build_output_stream(
&output_config,
- curry_callback::<u16>(
+ callback::<u16>(
Arc::clone(&client_streams),
output_volume_receiver,
user_volumes,
@@ -189,13 +246,13 @@ impl AudioOutputDevice for DefaultAudioOutputDevice {
fn play(&self) -> Result<(), AudioError> {
self.stream
.play()
- .map_err(|e| AudioError::OutputPlayError(e))
+ .map_err(AudioError::OutputPlayError)
}
fn pause(&self) -> Result<(), AudioError> {
self.stream
.pause()
- .map_err(|e| AudioError::OutputPauseError(e))
+ .map_err(AudioError::OutputPauseError)
}
fn set_volume(&self, volume: f32) {
@@ -211,7 +268,9 @@ impl AudioOutputDevice for DefaultAudioOutputDevice {
}
}
-pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>(
+/// Returns a function that fills a buffer with audio from client streams
+/// modified according to some audio configuration.
+pub fn callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>(
user_bufs: Arc<Mutex<ClientStream>>,
output_volume_receiver: watch::Receiver<f32>,
user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>,
@@ -227,10 +286,10 @@ pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>
let user_volumes = user_volumes.lock().unwrap();
for (k, v) in user_bufs.buffer_clients.iter_mut() {
let (user_volume, muted) = user_volumes.get(&k.1).cloned().unwrap_or((1.0, false));
- for sample in data.iter_mut() {
- if !muted {
+ if !muted {
+ for (sample, val) in data.iter_mut().zip(v.buf.drain().chain(iter::repeat(0.0))) {
*sample = sample.saturating_add(Sample::from(
- &(v.0.pop_front().unwrap_or(0.0) * volume * user_volume),
+ &(val * volume * user_volume),
));
}
}
@@ -242,3 +301,14 @@ pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>
}
}
}
+
+impl Debug for DefaultAudioOutputDevice {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("DefaultAudioInputDevice")
+ .field("client_streams", &self.client_streams)
+ .field("config", &self.config)
+ .field("volume_sender", &self.volume_sender)
+ .field("stream", &"cpal::Stream")
+ .finish()
+ }
+}
diff --git a/mumd/src/audio/transformers.rs b/mumd/src/audio/transformers.rs
index 25e28b8..21a71b5 100644
--- a/mumd/src/audio/transformers.rs
+++ b/mumd/src/audio/transformers.rs
@@ -2,10 +2,11 @@
pub trait Transformer {
/// Do the transform. Returning `None` is interpreted as "the buffer is unwanted".
/// The implementor is free to modify the buffer however it wants to.
- fn transform<'a>(&mut self, buf: &'a mut [f32]) -> Option<&'a mut [f32]>;
+ fn transform<'a>(&mut self, buf: (opus::Channels, &'a mut [f32])) -> Option<(opus::Channels, &'a mut [f32])>;
}
/// A struct representing a noise gate transform.
+#[derive(Debug)]
pub struct NoiseGate {
alltime_high: f32,
open: usize,
@@ -25,7 +26,7 @@ impl NoiseGate {
}
impl Transformer for NoiseGate {
- fn transform<'a>(&mut self, buf: &'a mut [f32]) -> Option<&'a mut [f32]> {
+ fn transform<'a>(&mut self, (channels, buf): (opus::Channels, &'a mut [f32])) -> Option<(opus::Channels, &'a mut [f32])> {
const MUTE_PERCENTAGE: f32 = 0.1;
let max = buf.iter().map(|e| e.abs()).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
@@ -43,7 +44,7 @@ impl Transformer for NoiseGate {
if self.open == 0 {
None
} else {
- Some(buf)
+ Some((channels, buf))
}
}
}
diff --git a/mumd/src/client.rs b/mumd/src/client.rs
index 9e8ca18..753583f 100644
--- a/mumd/src/client.rs
+++ b/mumd/src/client.rs
@@ -33,7 +33,7 @@ pub async fn handle(
packet_sender.clone(),
packet_receiver,
event_queue.clone(),
- ).fuse() => r.map_err(|e| ClientError::TcpError(e)),
+ ).fuse() => r.map_err(ClientError::TcpError),
_ = udp::handle(
Arc::clone(&state),
connection_info_receiver.clone(),
diff --git a/mumd/src/error.rs b/mumd/src/error.rs
index da1bdd3..4277d7f 100644
--- a/mumd/src/error.rs
+++ b/mumd/src/error.rs
@@ -5,6 +5,7 @@ use tokio::sync::mpsc;
pub type ServerSendError = mpsc::error::SendError<ControlPacket<Serverbound>>;
+#[derive(Debug)]
pub enum TcpError {
NoConnectionInfoReceived,
TlsConnectorBuilderError(native_tls::Error),
@@ -40,6 +41,7 @@ impl From<ServerSendError> for TcpError {
}
}
+#[derive(Debug)]
pub enum UdpError {
NoConnectionInfoReceived,
DisconnectBeforeCryptSetup,
@@ -53,6 +55,7 @@ impl From<std::io::Error> for UdpError {
}
}
+#[derive(Debug)]
pub enum ClientError {
TcpError(TcpError),
}
@@ -65,6 +68,7 @@ impl fmt::Display for ClientError {
}
}
+#[derive(Debug)]
pub enum AudioStream {
Input,
Output,
@@ -79,6 +83,7 @@ impl fmt::Display for AudioStream {
}
}
+#[derive(Debug)]
pub enum AudioError {
NoDevice(AudioStream),
NoConfigs(AudioStream, cpal::SupportedStreamConfigsError),
@@ -105,6 +110,7 @@ impl fmt::Display for AudioError {
}
}
+#[derive(Debug)]
pub enum StateError {
AudioError(AudioError),
ConfigError(ConfigError),
diff --git a/mumd/src/main.rs b/mumd/src/main.rs
index a96944c..e7ac033 100644
--- a/mumd/src/main.rs
+++ b/mumd/src/main.rs
@@ -1,10 +1,25 @@
-mod audio;
-mod client;
-mod command;
-mod error;
-mod network;
-mod notifications;
-mod state;
+#![warn(elided_lifetimes_in_paths)]
+#![warn(meta_variable_misuse)]
+#![warn(missing_debug_implementations)]
+#![warn(single_use_lifetimes)]
+#![warn(unreachable_pub)]
+#![warn(unused_crate_dependencies)]
+#![warn(unused_import_braces)]
+#![warn(unused_lifetimes)]
+#![warn(unused_qualifications)]
+#![deny(macro_use_extern_crate)]
+#![deny(missing_abi)]
+#![deny(future_incompatible)]
+#![forbid(unsafe_code)]
+#![forbid(non_ascii_idents)]
+
+pub mod audio;
+pub mod client;
+pub mod command;
+pub mod error;
+pub mod network;
+pub mod notifications;
+pub mod state;
use crate::state::State;
@@ -76,12 +91,9 @@ async fn main() {
_ = receive_commands(command_sender).fuse() => Ok(()),
};
- match run {
- Err(e) => {
- error!("mumd: {}", e);
- std::process::exit(1);
- }
- _ => {}
+ if let Err(e) = run {
+ error!("mumd: {}", e);
+ std::process::exit(1);
}
}
diff --git a/mumd/src/network.rs b/mumd/src/network.rs
index 2b803c0..1066fef 100644
--- a/mumd/src/network.rs
+++ b/mumd/src/network.rs
@@ -30,8 +30,8 @@ impl ConnectionInfo {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum VoiceStreamType {
- TCP,
- UDP,
+ Tcp,
+ Udp,
}
async fn run_until<F, R>(
diff --git a/mumd/src/network/tcp.rs b/mumd/src/network/tcp.rs
index f620a32..0fdc4c5 100644
--- a/mumd/src/network/tcp.rs
+++ b/mumd/src/network/tcp.rs
@@ -14,6 +14,7 @@ use mumble_protocol::{Clientbound, Serverbound};
use mumlib::command::MumbleEventKind;
use std::collections::HashMap;
use std::convert::{Into, TryInto};
+use std::fmt::Debug;
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use tokio::net::TcpStream;
@@ -31,8 +32,8 @@ type TcpSender = SplitSink<
type TcpReceiver =
SplitStream<Framed<TlsStream<TcpStream>, ControlCodec<Serverbound, Clientbound>>>;
-pub(crate) type TcpEventCallback = Box<dyn FnOnce(TcpEventData)>;
-pub(crate) type TcpEventSubscriber = Box<dyn FnMut(TcpEventData) -> bool>; //the bool indicates if it should be kept or not
+pub(crate) type TcpEventCallback = Box<dyn FnOnce(TcpEventData<'_>)>;
+pub(crate) type TcpEventSubscriber = Box<dyn FnMut(TcpEventData<'_>) -> bool>; //the bool indicates if it should be kept or not
/// Why the TCP was disconnected.
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
@@ -57,15 +58,15 @@ pub enum TcpEvent {
/// Having two different types might feel a bit confusing. Essentially, a
/// callback _registers_ to a [TcpEvent] but _takes_ a [TcpEventData] as
/// parameter.
-#[derive(Clone)]
+#[derive(Clone, Debug)]
pub enum TcpEventData<'a> {
Connected(Result<&'a msgs::ServerSync, mumlib::Error>),
Disconnected(DisconnectedReason),
TextMessage(&'a msgs::TextMessage),
}
-impl<'a> From<&TcpEventData<'a>> for TcpEvent {
- fn from(t: &TcpEventData) -> Self {
+impl From<&TcpEventData<'_>> for TcpEvent {
+ fn from(t: &TcpEventData<'_>) -> Self {
match t {
TcpEventData::Connected(_) => TcpEvent::Connected,
TcpEventData::Disconnected(reason) => TcpEvent::Disconnected(*reason),
@@ -74,7 +75,7 @@ impl<'a> From<&TcpEventData<'a>> for TcpEvent {
}
}
-#[derive(Clone)]
+#[derive(Clone, Default)]
pub struct TcpEventQueue {
callbacks: Arc<RwLock<HashMap<TcpEvent, Vec<TcpEventCallback>>>>,
subscribers: Arc<RwLock<HashMap<TcpEvent, Vec<TcpEventSubscriber>>>>,
@@ -111,7 +112,7 @@ impl TcpEventQueue {
/// Fires all callbacks related to a specific TCP event and removes them from the event queue.
/// Also calls all event subscribers, but keeps them in the queue
- pub fn resolve<'a>(&self, data: TcpEventData<'a>) {
+ pub fn resolve(&self, data: TcpEventData<'_>) {
if let Some(vec) = self
.callbacks
.write()
@@ -139,6 +140,13 @@ impl TcpEventQueue {
}
}
+impl Debug for TcpEventQueue {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("TcpEventQueue")
+ .finish()
+ }
+}
+
pub async fn handle(
state: Arc<RwLock<State>>,
mut connection_info_receiver: watch::Receiver<Option<ConnectionInfo>>,
@@ -148,13 +156,14 @@ pub async fn handle(
event_queue: TcpEventQueue,
) -> Result<(), TcpError> {
loop {
- let connection_info = 'data: loop {
- while connection_info_receiver.changed().await.is_ok() {
+ let connection_info = loop {
+ if connection_info_receiver.changed().await.is_ok() {
if let Some(data) = connection_info_receiver.borrow().clone() {
- break 'data data;
+ break data;
}
+ } else {
+ return Err(TcpError::NoConnectionInfoReceived);
}
- return Err(TcpError::NoConnectionInfoReceived);
};
let connect_result = connect(
connection_info.socket_addr,
@@ -242,12 +251,12 @@ async fn connect(
builder.danger_accept_invalid_certs(accept_invalid_cert);
let connector: TlsConnector = builder
.build()
- .map_err(|e| TcpError::TlsConnectorBuilderError(e))?
+ .map_err(TcpError::TlsConnectorBuilderError)?
.into();
let tls_stream = connector
.connect(&server_host, stream)
.await
- .map_err(|e| TcpError::TlsConnectError(e))?;
+ .map_err(TcpError::TlsConnectError)?;
debug!("TLS connected");
// Wrap the TLS stream with Mumble's client-side control-channel codec
@@ -304,13 +313,13 @@ async fn send_voice(
inner_phase_watcher.changed().await.unwrap();
if matches!(
*inner_phase_watcher.borrow(),
- StatePhase::Connected(VoiceStreamType::TCP)
+ StatePhase::Connected(VoiceStreamType::Tcp)
) {
break;
}
}
run_until(
- |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::TCP)),
+ |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::Tcp)),
async {
loop {
packet_sender.send(
@@ -465,7 +474,7 @@ async fn listen(
..
} => {
state.read().unwrap().audio_output().decode_packet_payload(
- VoiceStreamType::TCP,
+ VoiceStreamType::Tcp,
session_id,
payload,
);
diff --git a/mumd/src/network/udp.rs b/mumd/src/network/udp.rs
index 95dcf33..0f78638 100644
--- a/mumd/src/network/udp.rs
+++ b/mumd/src/network/udp.rs
@@ -37,13 +37,14 @@ pub async fn handle(
let receiver = state.read().unwrap().audio_input().receiver();
loop {
- let connection_info = 'data: loop {
- while connection_info_receiver.changed().await.is_ok() {
+ let connection_info = loop {
+ if connection_info_receiver.changed().await.is_ok() {
if let Some(data) = connection_info_receiver.borrow().clone() {
- break 'data data;
+ break data;
}
+ } else {
+ return Err(UdpError::NoConnectionInfoReceived);
}
- return Err(UdpError::NoConnectionInfoReceived);
};
let (sink, source) = connect(&mut crypt_state_receiver).await?;
@@ -136,7 +137,7 @@ async fn listen(
state
.read()
.unwrap()
- .broadcast_phase(StatePhase::Connected(VoiceStreamType::UDP));
+ .broadcast_phase(StatePhase::Connected(VoiceStreamType::Udp));
last_ping_recv.store(timestamp, Ordering::Relaxed);
}
VoicePacket::Audio {
@@ -147,7 +148,7 @@ async fn listen(
..
} => {
state.read().unwrap().audio_output().decode_packet_payload(
- VoiceStreamType::UDP,
+ VoiceStreamType::Udp,
session_id,
payload,
);
@@ -173,7 +174,7 @@ async fn send_pings(
state
.read()
.unwrap()
- .broadcast_phase(StatePhase::Connected(VoiceStreamType::TCP));
+ .broadcast_phase(StatePhase::Connected(VoiceStreamType::Tcp));
}
match sink
.lock()
@@ -208,13 +209,13 @@ async fn send_voice(
inner_phase_watcher.changed().await.unwrap();
if matches!(
*inner_phase_watcher.borrow(),
- StatePhase::Connected(VoiceStreamType::UDP)
+ StatePhase::Connected(VoiceStreamType::Udp)
) {
break;
}
}
run_until(
- |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::UDP)),
+ |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::Udp)),
async {
let mut receiver = receiver.lock().await;
loop {
diff --git a/mumd/src/state.rs b/mumd/src/state.rs
index d2d77b1..1992884 100644
--- a/mumd/src/state.rs
+++ b/mumd/src/state.rs
@@ -20,6 +20,7 @@ use mumlib::command::{ChannelTarget, Command, CommandResponse, MessageTarget, Mu
use mumlib::config::Config;
use mumlib::Error;
use std::{
+ fmt::Debug,
iter,
net::{SocketAddr, ToSocketAddrs},
sync::{Arc, RwLock},
@@ -29,7 +30,7 @@ use tokio::sync::{mpsc, watch};
macro_rules! at {
( $( $event:expr => $generator:expr ),+ $(,)? ) => {
ExecutionContext::TcpEventCallback(vec![
- $( ($event, Box::new($generator)), )*
+ $( ($event, Box::new($generator)), )+
])
};
}
@@ -42,18 +43,18 @@ macro_rules! now {
type Responses = Box<dyn Iterator<Item = mumlib::error::Result<Option<CommandResponse>>>>;
+type TcpEventCallback = Box<dyn FnOnce(TcpEventData<'_>) -> Responses>;
+type TcpEventSubscriberCallback = Box<
+ dyn FnMut(
+ TcpEventData<'_>,
+ &mut mpsc::UnboundedSender<mumlib::error::Result<Option<CommandResponse>>>,
+ ) -> bool
+>;
+
//TODO give me a better name
pub enum ExecutionContext {
- TcpEventCallback(Vec<(TcpEvent, Box<dyn FnOnce(TcpEventData) -> Responses>)>),
- TcpEventSubscriber(
- TcpEvent,
- Box<
- dyn FnMut(
- TcpEventData,
- &mut mpsc::UnboundedSender<mumlib::error::Result<Option<CommandResponse>>>,
- ) -> bool,
- >,
- ),
+ TcpEventCallback(Vec<(TcpEvent, TcpEventCallback)>),
+ TcpEventSubscriber(TcpEvent, TcpEventSubscriberCallback),
Now(Box<dyn FnOnce() -> Responses>),
Ping(
Box<dyn FnOnce() -> mumlib::error::Result<SocketAddr>>,
@@ -63,6 +64,17 @@ pub enum ExecutionContext {
),
}
+impl Debug for ExecutionContext {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_tuple(match self {
+ ExecutionContext::TcpEventCallback(_) => "TcpEventCallback",
+ ExecutionContext::TcpEventSubscriber(_, _) => "TcpEventSubscriber",
+ ExecutionContext::Now(_) => "Now",
+ ExecutionContext::Ping(_, _) => "Ping",
+ }).finish()
+ }
+}
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StatePhase {
Disconnected,
@@ -70,6 +82,7 @@ pub enum StatePhase {
Connected(VoiceStreamType),
}
+#[derive(Debug)]
pub struct State {
config: Config,
server: Option<Server>,
@@ -90,9 +103,9 @@ impl State {
config.audio.input_volume.unwrap_or(1.0),
phase_watcher.1.clone(),
)
- .map_err(|e| StateError::AudioError(e))?;
+ .map_err(StateError::AudioError)?;
let audio_output = AudioOutput::new(config.audio.output_volume.unwrap_or(1.0))
- .map_err(|e| StateError::AudioError(e))?;
+ .map_err(StateError::AudioError)?;
let mut state = Self {
config,
server: None,
@@ -308,7 +321,7 @@ impl State {
}
pub fn initialized(&self) {
- self.broadcast_phase(StatePhase::Connected(VoiceStreamType::TCP));
+ self.broadcast_phase(StatePhase::Connected(VoiceStreamType::Tcp));
self.audio_output
.play_effect(NotificationEvents::ServerConnect);
}
@@ -773,7 +786,7 @@ pub fn handle_command(
.unwrap()
.users()
.iter()
- .find(|(_, user)| user.name() == &name)
+ .find(|(_, user)| user.name() == name)
.map(|(e, _)| *e);
let id = match id {
diff --git a/mumd/src/state/channel.rs b/mumd/src/state/channel.rs
index 6995e1e..2ed05c5 100644
--- a/mumd/src/state/channel.rs
+++ b/mumd/src/state/channel.rs
@@ -169,13 +169,10 @@ pub fn into_channel(
impl From<&Channel> for mumlib::state::Channel {
fn from(channel: &Channel) -> Self {
- mumlib::state::Channel {
- description: channel.description.clone(),
- links: Vec::new(),
- max_users: channel.max_users,
- name: channel.name.clone(),
- children: Vec::new(),
- users: Vec::new(),
- }
+ mumlib::state::Channel::new(
+ channel.name.clone(),
+ channel.description.clone(),
+ channel.max_users,
+ )
}
}
diff --git a/mumd/src/state/server.rs b/mumd/src/state/server.rs
index 4abde49..5d49457 100644
--- a/mumd/src/state/server.rs
+++ b/mumd/src/state/server.rs
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Server {
channels: HashMap<u32, Channel>,
users: HashMap<u32, User>,
diff --git a/mumd/src/state/user.rs b/mumd/src/state/user.rs
index 5770bca..0ffde90 100644
--- a/mumd/src/state/user.rs
+++ b/mumd/src/state/user.rs
@@ -78,7 +78,7 @@ impl User {
}
}
- pub fn apply_user_diff(&mut self, diff: &crate::state::user::UserDiff) {
+ pub fn apply_user_diff(&mut self, diff: &UserDiff) {
if let Some(comment) = diff.comment.clone() {
self.comment = Some(comment);
}
diff --git a/mumlib/src/command.rs b/mumlib/src/command.rs
index 8cb7cb9..9d98a38 100644
--- a/mumlib/src/command.rs
+++ b/mumlib/src/command.rs
@@ -1,3 +1,6 @@
+//! [Command]s can be sent from a controller to mumd which might respond with a
+//! [CommandResponse].
+
use crate::state::{Channel, Server};
use chrono::NaiveDateTime;
@@ -7,7 +10,9 @@ use std::fmt;
/// Something that happened in our channel at a point in time.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MumbleEvent {
+ /// When the event occured.
pub timestamp: NaiveDateTime,
+ /// What occured.
pub kind: MumbleEventKind
}
@@ -20,11 +25,17 @@ impl fmt::Display for MumbleEvent {
/// The different kinds of events that can happen.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MumbleEventKind {
+ /// A user connected to the server and joined our channel. Contains `(user, channel)`.
UserConnected(String, String),
+ /// A user disconnected from the server while in our channel. Contains `(user, channel)`.
UserDisconnected(String, String),
+ /// A user {un,}{muted,deafened}. Contains a rendered message with what changed and the user.
UserMuteStateChanged(String), // This logic is kinda weird so we only store the rendered message.
+ /// A text message was received. Contains who sent the message.
TextMessageReceived(String),
+ /// A user switched to our channel from some other channel. Contains `(user, previous-channel)`.
UserJoinedChannel(String, String),
+ /// A user switched from our channel to some other channel. Contains `(user, new-channel)`.
UserLeftChannel(String, String),
}
@@ -56,73 +67,124 @@ impl fmt::Display for MumbleEventKind {
}
}
+/// Sent by a controller to mumd who might respond with a [CommandResponse]. Not
+/// all commands receive a response.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Command {
+ /// No response.
ChannelJoin {
channel_identifier: String,
},
+
+ /// Response: [CommandResponse::ChannelList].
ChannelList,
+
+ /// Force reloading of config file from disk. No response.
ConfigReload,
+ /// Response: [CommandResponse::DeafenStatus]. Toggles if None.
DeafenSelf(Option<bool>),
Events {
block: bool
},
+ /// Set the outgoing audio volume (i.e. from you to the server). No response.
InputVolumeSet(f32),
+
+ /// Response: [CommandResponse::MuteStatus]. Toggles mute state if None.
MuteOther(String, Option<bool>),
+
+ /// Response: [CommandResponse::MuteStatus]. Toggles mute state if None.
MuteSelf(Option<bool>),
+
+ /// Set the master incoming audio volume (i.e. from the server to you).
+ /// No response.
OutputVolumeSet(f32),
+
+ /// Request a list of past messages. Blocks while waiting for more messages
+ /// if block is true. Response: multiple [CommandResponse::PastMessage].
PastMessages {
block: bool,
},
+
+ /// Response: [CommandResponse::Pong]. Used to test existance of a
+ /// mumd-instance.
Ping,
+
+ /// Send a message to some [MessageTarget].
SendMessage {
+ /// The message to send.
message: String,
+ /// The target(s) to send the message to.
targets: MessageTarget,
},
+
+ /// Connect to the specified server. Response: [CommandResponse::ServerConnect].
ServerConnect {
+ /// The URL or IP-adress to connect to.
host: String,
+ /// The port to connect to.
port: u16,
+ /// The username to connect with.
username: String,
+ /// The server password, if applicable. Not sent if None.
password: Option<String>,
+ /// Whether to accept an invalid server certificate or not.
accept_invalid_cert: bool,
},
+
+ /// Disconnect from the currently connected server. No response.
ServerDisconnect,
+
+ /// Send a server status request via UDP (e.g. not requiring a TCP connection).
+ /// Response: [CommandResponse::ServerStatus].
ServerStatus {
host: String,
port: u16,
},
+
+ /// Request the status of the current server. Response: [CommandResponse::Status].
Status,
+
+ /// The the volume of the specified user. No response.
UserVolumeSet(String, f32),
}
+/// A response to a sent [Command].
#[derive(Debug, Deserialize, Serialize)]
pub enum CommandResponse {
ChannelList {
channels: Channel,
},
+
DeafenStatus {
is_deafened: bool,
},
+
Event {
event: MumbleEvent,
},
+
MuteStatus {
is_muted: bool,
},
+
PastMessage {
message: (NaiveDateTime, String, String),
},
+
Pong,
+
ServerConnect {
welcome_message: Option<String>,
server_state: Server,
},
+
ServerStatus {
version: u32,
users: u32,
max_users: u32,
bandwidth: u32,
},
+
Status {
server_state: Server,
},
diff --git a/mumlib/src/config.rs b/mumlib/src/config.rs
index 3edef37..c2544e8 100644
--- a/mumlib/src/config.rs
+++ b/mumlib/src/config.rs
@@ -1,3 +1,5 @@
+//! Representations of the mumdrc configuration file.
+
use crate::error::ConfigError;
use crate::DEFAULT_PORT;
@@ -23,9 +25,13 @@ struct TOMLConfig {
servers: Option<Array>,
}
+/// Our representation of the mumdrc config file.
+// Deserialized via [TOMLConfig].
#[derive(Clone, Debug, Default)]
pub struct Config {
+ /// General audio configuration.
pub audio: AudioConfig,
+ /// Saved servers.
pub servers: Vec<ServerConfig>,
/// Whether we allow connecting to servers with invalid server certificates.
///
@@ -34,6 +40,15 @@ pub struct Config {
}
impl Config {
+ /// Writes this config to the specified path.
+ ///
+ /// Pass create = true if you want the file to be created if it doesn't already exist.
+ ///
+ /// # Errors
+ ///
+ /// - [ConfigError::WontCreateFile] if the file doesn't exist and create = false was passed.
+ /// - Any [ConfigError::TOMLErrorSer] encountered when serializing the config.
+ /// - Any [ConfigError::IOError] encountered when writing the file.
pub fn write(&self, path: &Path, create: bool) -> Result<(), ConfigError> {
// Possible race here. It's fine since it shows when:
// 1) the file doesn't exist when checked and is then created
@@ -55,41 +70,60 @@ impl Config {
}
}
+/// Overwrite a specific sound effect with a file that should be played instead.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SoundEffect {
+ /// During which event the effect should be played.
pub event: String,
+ /// The file that should be played.
pub file: String,
}
+/// General audio configuration.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AudioConfig {
+ /// The microphone input sensitivity.
pub input_volume: Option<f32>,
+ /// The output main gain.
pub output_volume: Option<f32>,
+ /// Overriden sound effects.
pub sound_effects: Option<Vec<SoundEffect>>,
}
+/// A saved server.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ServerConfig {
+ /// The alias of the server.
pub name: String,
+ /// The host (URL or IP-adress) of the server.
pub host: String,
+ /// The port, if non-default.
pub port: Option<u16>,
+ /// The username to connect with. Prompted on connection if omitted.
pub username: Option<String>,
+ /// The password to connect with. Nothing is sent to the server if omitted.
pub password: Option<String>,
+ /// Whether to accept invalid server certifications for this server.
pub accept_invalid_cert: Option<bool>,
}
impl ServerConfig {
+ /// Creates a [SocketAddr] for this server.
+ ///
+ /// Returns `None` if no resolution could be made. See
+ /// [std::net::ToSocketAddrs] for more information.
pub fn to_socket_addr(&self) -> Option<SocketAddr> {
- match (self.host.as_str(), self.port.unwrap_or(DEFAULT_PORT))
- .to_socket_addrs()
- .map(|mut e| e.next())
- {
- Ok(Some(addr)) => Some(addr),
- _ => None,
- }
+ Some((self.host.as_str(), self.port.unwrap_or(DEFAULT_PORT))
+ .to_socket_addrs()
+ .ok()?
+ .next()?)
}
}
+/// Finds the default path of the configuration file.
+///
+/// The user config dir is looked for first (cross-platform friendly) and
+/// `/etc/mumdrc` is used as a fallback.
pub fn default_cfg_path() -> PathBuf {
match dirs::config_dir() {
Some(mut p) => {
@@ -143,6 +177,14 @@ impl From<Config> for TOMLConfig {
}
}
+/// Reads the config at the specified path.
+///
+/// If the file isn't found, returns a default config.
+///
+/// # Errors
+///
+/// - Any [ConfigError::TOMLErrorDe] encountered when deserializing the config.
+/// - Any [ConfigError::IOError] encountered when reading the file.
pub fn read_cfg(path: &Path) -> Result<Config, ConfigError> {
match fs::read_to_string(path) {
Ok(s) => {
@@ -155,7 +197,7 @@ pub fn read_cfg(path: &Path) -> Result<Config, ConfigError> {
} else {
error!("Error reading config file: {}", e);
}
- return Ok(Config::default());
+ Ok(Config::default())
}
}
}
diff --git a/mumlib/src/lib.rs b/mumlib/src/lib.rs
index 9b7d686..2282df5 100644
--- a/mumlib/src/lib.rs
+++ b/mumlib/src/lib.rs
@@ -1,3 +1,21 @@
+#![warn(elided_lifetimes_in_paths)]
+#![warn(meta_variable_misuse)]
+#![warn(missing_debug_implementations)]
+#![warn(single_use_lifetimes)]
+#![warn(unreachable_pub)]
+#![warn(unused_crate_dependencies)]
+#![warn(unused_import_braces)]
+#![warn(unused_lifetimes)]
+#![warn(unused_qualifications)]
+// #![warn(missing_docs)] may be enabled later when more is documented
+#![deny(macro_use_extern_crate)]
+#![deny(missing_abi)]
+#![deny(future_incompatible)]
+#![forbid(unsafe_code)]
+#![forbid(non_ascii_idents)]
+//! Shared items for crates that want to communicate with mumd and/or mumctl.
+
+
pub mod command;
pub mod config;
pub mod error;
@@ -8,9 +26,15 @@ pub use error::Error;
use colored::*;
use log::*;
+/// The default file path to use for the socket.
pub const SOCKET_PATH: &str = "/tmp/mumd";
+
+/// The default mumble port.
pub const DEFAULT_PORT: u16 = 64738;
+/// Setup a minimal fern logger.
+///
+/// Format: `LEVEL [yyyy-mm-dd][HH:MM:SS] FILE:LINE MESSAGE`
pub fn setup_logger<T: Into<fern::Output>>(target: T, color: bool) {
fern::Dispatch::new()
.format(move |out, message, record| {
diff --git a/mumlib/src/state.rs b/mumlib/src/state.rs
index 6fad332..5008465 100644
--- a/mumlib/src/state.rs
+++ b/mumlib/src/state.rs
@@ -1,25 +1,58 @@
use serde::{Deserialize, Serialize};
use std::fmt;
+/// The state of the currently connected Mumble server.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Server {
+ /// State of the currently connected channel.
pub channels: Channel,
+ /// The welcome text we received when we connected.
pub welcome_text: Option<String>,
+ /// Our username.
pub username: String,
+ /// The host (ip:port) of the server.
pub host: String,
}
+/// A representation of a channel in a Mumble server.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Channel {
+ /// The description of the channel, if set.
pub description: Option<String>,
- pub links: Vec<Vec<usize>>, //to represent several walks through the tree to find channels its linked to
+ /// The maximum number of allowed users in this channel.
pub max_users: u32,
+ /// The name of this channel.
pub name: String,
+ /// Any children this channel has.
pub children: Vec<Channel>,
+ /// This channel's connected users.
pub users: Vec<User>,
+
+ links: Vec<Vec<usize>>, //to represent several walks through the tree to find channels its linked to
}
impl Channel {
+ /// Create a new Channel representation.
+ pub fn new(
+ name: String,
+ description: Option<String>,
+ max_users: u32,
+ ) -> Self {
+ Self {
+ description,
+ max_users,
+ name,
+ children: Vec::new(),
+ users: Vec::new(),
+
+ links: Vec::new(),
+ }
+ }
+
+ /// Create an iterator over this channel and its children in a [pre-order
+ /// traversal](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR)
+ /// which ensures that parent channels are returned before any of its
+ /// children.
pub fn iter(&self) -> Iter<'_> {
Iter {
me: Some(&self),
@@ -32,6 +65,8 @@ impl Channel {
}
}
+ /// Create an iterator over this channel and its childrens connected users
+ /// in a pre-order traversal.
pub fn users_iter(&self) -> UsersIter<'_> {
UsersIter {
channels: self.children.iter().map(|e| e.users_iter()).collect(),
@@ -46,6 +81,8 @@ impl Channel {
}
}
+/// An iterator over channels. Created by [Channel::iter].
+#[derive(Debug)]
pub struct Iter<'a> {
me: Option<&'a Channel>,
channel: Option<usize>,
@@ -76,6 +113,8 @@ impl<'a> Iterator for Iter<'a> {
}
}
+/// An iterator over users. Created by [Channel::users_iter].
+#[derive(Debug)]
pub struct UsersIter<'a> {
channel: Option<usize>,
channels: Vec<UsersIter<'a>>,