diff --git a/Cargo.lock b/Cargo.lock index 895cd95..3a75f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,8 +71,9 @@ dependencies = [ name = "bassbox" version = "0.1.0" dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "cpal 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "dsp-chain 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dsp-chain 0.13.1 (git+https://github.com/fwcd/dsp-chain?rev=55ecc85)", "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 13.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core-client 13.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -249,19 +250,19 @@ dependencies = [ [[package]] name = "daggy" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" +version = "0.6.0" +source = "git+https://github.com/mitchmindtree/daggy?rev=bcb36c7b#bcb36c7b1a32043b7e5e44ccf2417564e43465a9" dependencies = [ - "petgraph 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "petgraph 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "dsp-chain" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "git+https://github.com/fwcd/dsp-chain?rev=55ecc85#55ecc856ef7f96a55744984f1db4dd3da6f9c735" dependencies = [ - "daggy 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "sample 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "daggy 0.6.0 (git+https://github.com/mitchmindtree/daggy?rev=bcb36c7b)", + "sample 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -669,7 +670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "petgraph" -version = "0.2.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -885,7 +886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "sample" -version = "0.6.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1310,8 +1311,8 @@ dependencies = [ "checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" -"checksum daggy 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d6a9b61ac11d223e884f431772d311087644e4710c1555d7db270cefb5fb9c69" -"checksum dsp-chain 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5251bbb15727afffbf50403b1a5a6f030c1662bad1a5c665a895547330a17e17" +"checksum daggy 0.6.0 (git+https://github.com/mitchmindtree/daggy?rev=bcb36c7b)" = "" +"checksum dsp-chain 0.13.1 (git+https://github.com/fwcd/dsp-chain?rev=55ecc85)" = "" "checksum env_logger 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3ddf21e73e016298f5cb37d6ef8e8da8e39f91f9ec8b0df44b7deb16a9f8cd5b" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" @@ -1359,7 +1360,7 @@ dependencies = [ "checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" "checksum peeking_take_while 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" -"checksum petgraph 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "3418aea32729bb24e3751c3893aa18f829a804f5030af2e9395c47c9b38f9535" +"checksum petgraph 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f" "checksum pkg-config 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c1d2cfa5a714db3b5f24f0915e74fcdf91d09d496ba61329705dda7774d2af" "checksum proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e10d4b51f154c8a7fb96fd6dad097cb74b863943ec010ac94b9fd1be8861fe1e" "checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" @@ -1385,7 +1386,7 @@ dependencies = [ "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" -"checksum sample 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "80fb6a09c9909286e083dd666073427801c875452c6aada9e4f56bf98a10d76c" +"checksum sample 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc0966ad788ec7289562643e05c611b7853e0618b7f9306aafd2fc727abfe168" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" diff --git a/Cargo.toml b/Cargo.toml index 318cf3c..472276e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ edition = "2018" [dependencies] # General getopts = "0.2" +byteorder = "1.3.2" # Audio -dsp-chain = "0.13.1" +# TODO: Update to official version once merged +dsp-chain = { git = "https://github.com/fwcd/dsp-chain", rev = "55ecc85" } cpal = "0.10.0" minimp3 = "0.3.3" # RPC diff --git a/src/audioformat.rs b/src/audioformat.rs index 6a1f3a9..e396cee 100644 --- a/src/audioformat.rs +++ b/src/audioformat.rs @@ -7,8 +7,6 @@ pub type StandardSample = f32; pub const STANDARD_CHANNELS: usize = 2; pub type StandardFrame = [StandardSample; STANDARD_CHANNELS]; -pub fn empty_standard_frame() -> StandardFrame { StandardFrame::equilibrium() } - /// Basic arithmetic operating on the signal's amplitude /// for standard frames. More flexible than add_amp and scale_amp /// since it provides subtraction and division operations too. diff --git a/src/engine/speaker.rs b/src/engine/speaker.rs index b473485..4ee5bca 100644 --- a/src/engine/speaker.rs +++ b/src/engine/speaker.rs @@ -1,9 +1,9 @@ use super::{AudioEngine, BackgroundEngine, ControlMsg, EngineControls}; -use crate::audioformat::{StandardFrame, empty_standard_frame, STANDARD_CHANNELS, StandardSample}; +use crate::audioformat::{StandardFrame, STANDARD_CHANNELS, StandardSample}; use crate::graph::SharedAudioGraph; use std::sync::mpsc; use std::thread; -use dsp::sample::{Sample, Frame, FromSample, conv::ToFrameSliceMut}; +use dsp::{Sample, Frame, FromSample, Node, sample::conv::ToFrameSliceMut}; use cpal::{StreamData, UnknownTypeOutputBuffer, OutputBuffer}; use cpal::traits::{DeviceTrait, EventLoopTrait, HostTrait}; @@ -70,7 +70,7 @@ impl AudioEngine for SpeakerEngine { // Read audio from graph into temporary buffer let sample_count = buffer_sample_count(&data).unwrap_or(0); let frame_count = sample_count / STANDARD_CHANNELS; - let mut audio: Vec = vec![empty_standard_frame(); frame_count]; + let mut audio: Vec = vec![StandardFrame::equilibrium(); frame_count]; shared_graph.lock().unwrap().audio_requested(&mut audio, sample_hz); with_buffer_of!(data, |buffer| write_audio(&audio, buffer)); diff --git a/src/graph.rs b/src/graph.rs index cec5152..61f6038 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,196 +1,10 @@ use std::sync::{Arc, Mutex}; -use dsp::Node; use crate::processing::DspNode; use crate::audioformat::StandardFrame; pub type NodeIndex = dsp::NodeIndex; pub type EdgeIndex = dsp::EdgeIndex; -pub type WouldCycle = dsp::WouldCycle; - -/// A wrapper around the dsp graph whose node -/// indices remain stable after removals. -/// -/// Note that _different_ nodes may get the _same_ -/// index if a node is removed and another one added. -/// node indices should generally not outlive -/// their referenced node. -pub struct AudioGraph { - inner: dsp::Graph, - free: Vec, - has_free: bool -} - -impl AudioGraph { - pub fn new() -> AudioGraph { - AudioGraph { inner: dsp::Graph::new(), free: Vec::new(), has_free: false } - } - - /// Adds the given node to the graph in O(n) - pub fn add_node(&mut self, node: DspNode) -> NodeIndex { - if let Some(index) = self.take_free() { - let node_ref = self.inner.node_mut(index).expect("A free node index should still hold a (no longer used) node"); - *node_ref = node; - index - } else { - let index = self.inner.add_node(node); - self.free.insert(index.index(), false); - index - } - } - - /// Adds the given edge to the graph - pub fn add_edge(&mut self, src: NodeIndex, dest: NodeIndex) -> Result { - // TODO: Prevent duplicate edges? - self.inner.add_connection(src, dest) - } - - /// Adds the given new node and input edge to the graph - pub fn add_input(&mut self, src: DspNode, dest: NodeIndex) -> (EdgeIndex, NodeIndex) { - let node_index = self.add_node(src); - let edge_index = self.add_edge(node_index, dest).expect("Adding an input edge with a new node should never cause a cycle"); - (edge_index, node_index) - } - - /// Takes the next free index (if available) in O(n) - fn take_free(&mut self) -> Option { - if self.has_free { - let mut has_free = false; - let mut taken: Option = None; - - for i in 0..self.node_count() { - if self.free[i] { - match taken { - Some(..) => has_free = true, - None => { - self.free[i] = false; - taken = Some(NodeIndex::new(i)) - } - } - } - } - - self.has_free = has_free; - taken - } else { - None - } - } - - /// Removes a node from the graph. - /// - /// Any indices referring to the given node should - /// be dropped by now. - pub fn remove_node(&mut self, node: NodeIndex) { - self.free[node.index()] = true; - self.inner.remove_all_input_connections(node); - self.inner.remove_all_output_connections(node); - self.has_free = true; - } - - /// Checks whether the node at the given index exists - fn node_exists(&self, node: NodeIndex) -> bool { - let i = node.index(); - if i > self.free.len() { - false - } else { - !self.free[i] - } - } - - /// The number of nodes in the graph - pub fn node_count(&self) -> usize { self.inner.node_count() } - - /// An immutable reference to the node at the given index - pub fn node(&self, node: NodeIndex) -> Option<&DspNode> { - if self.node_exists(node) { - self.inner.node(node) - } else { - None - } - } - - /// A mutable reference to the node at the given index - pub fn node_mut(&mut self, node: NodeIndex) -> Option<&mut DspNode> { - if self.node_exists(node) { - self.inner.node_mut(node) - } else { - None - } - } - - /// An iterator over all (possibly freed) nodes at their corresponding indices - pub fn node_iter(&self) -> NodeIterator { - NodeIterator { nodes: self.inner.raw_nodes(), free: &self.free, i: 0 } - } - - /// An iterator over all edges in the graph - pub fn edge_iter(&self) -> EdgeIterator { - EdgeIterator { edges: self.inner.raw_edges(), i: 0 } - } - - /// Sets the master (output) node of this graph - pub fn set_master(&mut self, master: Option) { - self.inner.set_master(master); - } - - /// Fetches the master (output) node of this graph - pub fn master(&self) -> Option { - self.inner.master_index() - } - - /// Requests audio from the master node - #[inline] - pub fn audio_requested(&mut self, output: &mut [StandardFrame], sample_hz: f64) { - self.inner.audio_requested(output, sample_hz) - } -} - -/// An iterator over the nodes in an AudioGraph -pub struct NodeIterator<'a> { - nodes: dsp::RawNodes<'a, DspNode>, - free: &'a Vec, - i: usize -} - -impl<'a> Iterator for NodeIterator<'a> { - type Item = Option<&'a DspNode>; - - fn next(&mut self) -> Option> { - if self.i < self.nodes.len() { - let node = Some(&self.nodes[self.i].weight).filter(|_| !self.free[self.i]); - self.i += 1; - Some(node) - } else { - None - } - } -} - -/// A directed edge -pub struct Edge { - pub src: NodeIndex, - pub dest: NodeIndex -} - -/// An iterator over the edges in an AudioGraph -pub struct EdgeIterator<'a> { - edges: dsp::RawEdges<'a, StandardFrame>, - i: usize -} - -impl<'a> Iterator for EdgeIterator<'a> { - type Item = Edge; - - fn next(&mut self) -> Option { - if self.i < self.edges.len() { - let raw_edge = &self.edges[self.i]; - self.i += 1; - Some(Edge { src: raw_edge.source(), dest: raw_edge.target() }) - } else { - None - } - } -} +pub type AudioGraph = dsp::Graph; /// The audio graph which is shared between /// an engine and possibly control threads diff --git a/src/main.rs b/src/main.rs index b773979..c9dca56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod processing; mod source; mod graph; mod audioformat; +mod util; use graph::new_shared_graph; use engine::{AudioEngine, speaker::SpeakerEngine}; diff --git a/src/processing/filter.rs b/src/processing/filter.rs index e036db6..0d19933 100644 --- a/src/processing/filter.rs +++ b/src/processing/filter.rs @@ -1,6 +1,6 @@ //! A collection of lowpass and highpass filters -use crate::audioformat::{StandardFrame, OpsExt, empty_standard_frame}; +use crate::audioformat::{StandardFrame, OpsExt}; use dsp::sample::Frame; use std::collections::VecDeque; use std::f32; @@ -8,8 +8,10 @@ use std::f32; /// A facility that processes audio on a frame-by-frame /// basis, possibly maintaining state across calls. pub trait Filter { + type Frame: Frame; + /// Applies the filter to a single frame. - fn apply(&mut self, input: StandardFrame) -> StandardFrame; + fn apply(&mut self, input: Self::Frame) -> Self::Frame; } /// Anything that has a cutoff frequency @@ -33,7 +35,7 @@ pub struct IIRLowpassFilter { impl IIRLowpassFilter { pub fn from_cutoff_hz(cutoff_hz: f32, sample_hz: f64) -> IIRLowpassFilter { - IIRLowpassFilter::new(empty_standard_frame(), cutoff_hz, sample_hz) + IIRLowpassFilter::new(StandardFrame::equilibrium(), cutoff_hz, sample_hz) } pub fn new(last_output: StandardFrame, cutoff_hz: f32, sample_hz: f64) -> IIRLowpassFilter { @@ -56,6 +58,8 @@ impl CutoffFreq for IIRLowpassFilter { } impl Filter for IIRLowpassFilter { + type Frame = StandardFrame; + fn apply(&mut self, input: StandardFrame) -> StandardFrame { // y[i] = y[i - 1] + alpha * (x[i] - y[i - 1]) let output = self.last_output.add((input.sub(self.last_output)).scale(self.alpha)); @@ -78,7 +82,7 @@ pub struct IIRHighpassFilter { impl IIRHighpassFilter { pub fn from_cutoff_hz(cutoff_hz: f32, sample_hz: f64) -> IIRHighpassFilter { - IIRHighpassFilter::new(empty_standard_frame(), empty_standard_frame(), cutoff_hz, sample_hz) + IIRHighpassFilter::new(StandardFrame::equilibrium(), StandardFrame::equilibrium(), cutoff_hz, sample_hz) } pub fn new(last_input: StandardFrame, last_output: StandardFrame, cutoff_hz: f32, sample_hz: f64) -> IIRHighpassFilter { @@ -102,6 +106,8 @@ impl CutoffFreq for IIRHighpassFilter { } impl Filter for IIRHighpassFilter { + type Frame = StandardFrame; + fn apply(&mut self, input: StandardFrame) -> StandardFrame { // y[i] = alpha * (y[i - 1] + x[i] - x[i - 1]) let output = self.last_output.add(input).sub(self.last_input).scale(self.alpha); @@ -128,42 +134,48 @@ impl MovingAverageFilter { } impl Filter for MovingAverageFilter { + type Frame = StandardFrame; + fn apply(&mut self, input: StandardFrame) -> StandardFrame { if self.last.len() == self.length { self.last.pop_front(); } self.last.push_back(input); - let output = self.last.iter().fold(empty_standard_frame(), |a, b| a.add_amp(*b)); + let output = self.last.iter().fold(StandardFrame::equilibrium(), |a, b| a.add_amp(*b)); return output.scale_amp(1.0 / (self.length as f32)); } } /// A disableable filter that lets the original signal /// "pass-through" if disabled. -pub struct Disableable { - pub wrapped: F, +pub struct Disableable { + pub wrapped: L, pub disabled: bool } -impl Disableable { - pub fn new(wrapped: F, disabled: bool) -> Disableable { Disableable { wrapped: wrapped, disabled: disabled } } +impl Disableable { + pub fn new(wrapped: L, disabled: bool) -> Disableable { Disableable { wrapped: wrapped, disabled: disabled } } - pub fn enabled(wrapped: F) -> Disableable { Disableable { wrapped: wrapped, disabled: false } } + pub fn enabled(wrapped: L) -> Disableable { Disableable { wrapped: wrapped, disabled: false } } - pub fn disabled(wrapped: F) -> Disableable { Disableable { wrapped: wrapped, disabled: true } } + pub fn disabled(wrapped: L) -> Disableable { Disableable { wrapped: wrapped, disabled: true } } pub fn with(&self, wrapped: E) -> Disableable { Disableable { wrapped: wrapped, disabled: self.disabled } } } -impl Filter for Disableable where F: Filter { - fn apply(&mut self, input: StandardFrame) -> StandardFrame { +impl Filter for Disableable where L: Filter { + type Frame = L::Frame; + + fn apply(&mut self, input: L::Frame) -> L::Frame { let output = self.wrapped.apply(input); if self.disabled { input } else { output } } } -impl Filter for Box where F: Filter + ?Sized { - fn apply(&mut self, input: StandardFrame) -> StandardFrame { +impl Filter for Box where L: Filter + ?Sized { + type Frame = L::Frame; + + fn apply(&mut self, input: L::Frame) -> L::Frame { (**self).apply(input) } } diff --git a/src/processing/mod.rs b/src/processing/mod.rs index 922ab96..6465b50 100644 --- a/src/processing/mod.rs +++ b/src/processing/mod.rs @@ -6,8 +6,8 @@ pub mod filter; use dsp::Node; use dsp::sample::Frame; use filter::{Filter, MovingAverageFilter, IIRLowpassFilter, IIRHighpassFilter, Disableable}; -use crate::source::{AudioSource, file::FileSource, conv::Converting, pausable::Pausable}; -use crate::audioformat::{StandardFrame, empty_standard_frame}; +use crate::source::{AudioSource, file::FileSource, command::CommandSource, conv::Converting, pausable::Pausable}; +use crate::audioformat::StandardFrame; /// An audio processing node which can either be a source /// or an intermediate node that performs some transformation @@ -15,17 +15,17 @@ use crate::audioformat::{StandardFrame, empty_standard_frame}; /// /// _Generally_ all intermediate operations that perform modifications /// on the audio source (like pausing playback) should either -/// be implemented as a parameter of `DspNode::DynSource` or in an -/// `AudioSource` implementation wrapping another `AudioSource`. +/// be implemented as an `AudioSource` decorator. pub enum DspNode { Empty, Silence, Volume(f32), File(Pausable>), + Command(Pausable>), MovingAverage(Disableable), IIRLowpass(Disableable), IIRHighpass(Disableable), - DynFilter(Box) + DynFilter(Box + Send>) } impl Node for DspNode { @@ -35,7 +35,8 @@ impl Node for DspNode { DspNode::Silence => silence(buffer), DspNode::Volume(factor) => dsp::slice::map_in_place(buffer, |frame| frame.scale_amp(factor)), // Static source implementations to avoid boxing - DspNode::File(ref mut source) => read_source_into(buffer, source), + DspNode::File(ref mut source) => read_signal_into(buffer, source), + DspNode::Command(ref mut source) => read_signal_into(buffer, source), // Static filter implementations to avoid boxing DspNode::MovingAverage(ref mut filter) => apply_filter(buffer, filter), DspNode::IIRLowpass(ref mut filter) => apply_filter(buffer, filter), @@ -45,22 +46,21 @@ impl Node for DspNode { } } -fn read_source_into(buffer: &mut [StandardFrame], source: &mut S) where S: AudioSource { - let empty = empty_standard_frame(); +fn read_signal_into(buffer: &mut [F], source: &mut S) where S: AudioSource, F: Frame { for i in 0..buffer.len() { - buffer[i] = source.next().unwrap_or(empty); + buffer[i] = source.next(); } } -fn apply_filter(buffer: &mut [StandardFrame], filter: &mut F) where F: Filter { +fn apply_filter(buffer: &mut [F], filter: &mut L) where L: Filter, F: Frame { for frame in buffer { *frame = filter.apply(*frame); } } /// Silences the buffer. -fn silence(buffer: &mut [StandardFrame]) { +fn silence(buffer: &mut [F]) where F: Frame { for i in 0..buffer.len() { - buffer[i] = empty_standard_frame(); + buffer[i] = F::equilibrium(); } } diff --git a/src/services/graph.rs b/src/services/graph.rs index 8f6e5cc..8f3f54e 100644 --- a/src/services/graph.rs +++ b/src/services/graph.rs @@ -2,10 +2,12 @@ use jsonrpc_core::Result as RpcResult; use jsonrpc_core::{Error as RpcError, ErrorCode as RpcErrorCode}; use jsonrpc_derive::rpc; use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use dsp::EdgeRef; use super::rpcutils::server_error; use crate::processing::{DspNode, filter::{Disableable, CutoffFreq, IIRHighpassFilter, IIRLowpassFilter}}; use crate::graph::{AudioGraph, SharedAudioGraph}; -use crate::source::{pausable::Pausable, conv::Converting, file::FileSource}; +use crate::source::{AudioSource, pausable::Pausable, conv::Converting, file::FileSource, command::CommandSource}; use crate::engine::BackgroundEngine; #[derive(Serialize, Deserialize)] @@ -26,6 +28,14 @@ pub enum RpcNode { file_path: String, #[serde(default)] paused: bool }, + /// A command source + #[serde(rename_all = "camelCase")] + Command { + command: String, + #[serde(default)] args: Vec, + sample_hz: f64, + paused: bool + }, /// A lowpass filter #[serde(rename_all = "camelCase")] IIRLowpass { @@ -49,7 +59,16 @@ impl From<&DspNode> for RpcNode { match *node { DspNode::Empty => RpcNode::Empty, DspNode::Silence => RpcNode::Silence, - DspNode::File(Pausable { wrapped: ref converting_source, paused }) => RpcNode::File { file_path: converting_source.wrapped().file_path().to_owned(), paused: paused }, + DspNode::File(Pausable { wrapped: ref converting_source, paused }) => RpcNode::File { + file_path: converting_source.wrapped().file_path().to_owned(), + paused: paused + }, + DspNode::Command(Pausable { wrapped: ref converting_source, paused }) => RpcNode::Command { + command: converting_source.wrapped().command().to_owned(), + args: converting_source.wrapped().args().iter().map(|s| s.to_owned()).collect(), + sample_hz: converting_source.wrapped().sample_hz(), + paused: paused + }, DspNode::Volume(volume) => RpcNode::Volume { level: volume }, DspNode::IIRLowpass(Disableable { wrapped: ref filter, disabled }) => RpcNode::IIRLowpass { cutoff_hz: filter.cutoff_hz(), disabled: disabled }, DspNode::IIRHighpass(Disableable { wrapped: ref filter, disabled }) => RpcNode::IIRHighpass { cutoff_hz: filter.cutoff_hz(), disabled: disabled }, @@ -60,7 +79,7 @@ impl From<&DspNode> for RpcNode { } impl RpcNode { - pub fn into_dsp_node(self, sample_hz: f64) -> RpcResult { + pub fn into_dsp_node(self, target_sample_hz: f64) -> RpcResult { match self { RpcNode::Empty => Ok(DspNode::Empty), RpcNode::Silence => Ok(DspNode::Silence), @@ -68,14 +87,23 @@ impl RpcNode { RpcNode::File { ref file_path, paused } => Ok(DspNode::File( Pausable::new( Converting::to_sample_hz( - sample_hz, - FileSource::new(file_path.as_ref()).map_or_else(|| Err(server_error("Could not create file source")), Ok)? + target_sample_hz, + FileSource::new(file_path.as_ref()).map_err(|e| server_error(e))? + ), + paused + ) + )), + RpcNode::Command { ref command, ref args, sample_hz, paused } => Ok(DspNode::Command( + Pausable::new( + Converting::to_sample_hz( + target_sample_hz, + CommandSource::new(command, &args.iter().map(|s| s.as_ref()).collect::>(), sample_hz).map_err(|e| server_error(e))? ), paused ) )), - RpcNode::IIRLowpass { cutoff_hz, disabled } => Ok(DspNode::IIRLowpass(Disableable::new(IIRLowpassFilter::from_cutoff_hz(cutoff_hz, sample_hz), disabled))), - RpcNode::IIRHighpass { cutoff_hz, disabled } => Ok(DspNode::IIRHighpass(Disableable::new(IIRHighpassFilter::from_cutoff_hz(cutoff_hz, sample_hz), disabled))), + RpcNode::IIRLowpass { cutoff_hz, disabled } => Ok(DspNode::IIRLowpass(Disableable::new(IIRLowpassFilter::from_cutoff_hz(cutoff_hz, target_sample_hz), disabled))), + RpcNode::IIRHighpass { cutoff_hz, disabled } => Ok(DspNode::IIRHighpass(Disableable::new(IIRHighpassFilter::from_cutoff_hz(cutoff_hz, target_sample_hz), disabled))), RpcNode::DynFilter => Err(RpcError { code: RpcErrorCode::InvalidParams, message: "Dynamic DSP nodes can currently not be crated from RPC nodes".to_owned(), @@ -107,7 +135,7 @@ impl RpcEdge { #[derive(Serialize, Deserialize)] pub struct RpcGraph { - pub nodes: Vec>, + pub nodes: HashMap, pub edges: Vec, pub master: Option } @@ -115,9 +143,9 @@ pub struct RpcGraph { impl RpcGraph { pub fn from(graph: &AudioGraph) -> RpcGraph { RpcGraph { - nodes: graph.node_iter().map(|opt_node| opt_node.map(|node| RpcNode::from(node))).collect(), - edges: graph.edge_iter().map(|edge| RpcEdge::between(edge.src.index(), edge.dest.index())).collect(), - master: graph.master().map(|i| i.index()) + nodes: graph.node_references().map(|(id, node)| (id.index(), RpcNode::from(node))).collect(), + edges: graph.edge_references().map(|edge| RpcEdge::between(edge.source().index(), edge.target().index())).collect(), + master: graph.master_index().map(|i| i.index()) } } } @@ -186,7 +214,7 @@ impl AudioGraphServiceRpc for AudioGraphService { } fn add_edge(&self, edge: RpcEdge) -> RpcResult { - match self.shared_graph.lock().unwrap().add_edge(edge.src.into(), edge.dest.into()) { + match self.shared_graph.lock().unwrap().add_connection(edge.src.into(), edge.dest.into()) { Ok(edge_index) => Ok(edge_index.index()), Err(..) => Err(RpcError { code: RpcErrorCode::InvalidParams, diff --git a/src/services/player.rs b/src/services/player.rs index 32d015d..a15515f 100644 --- a/src/services/player.rs +++ b/src/services/player.rs @@ -87,7 +87,7 @@ impl AudioPlayerService { impl AudioPlayerServiceRpc for AudioPlayerService { fn enqueue_file(&self, path: String) -> RpcResult<()> { let source = FileSource::new(path.as_ref()) - .map_or_else(|| Err(server_error(format!("Could not find or decode file at '{}'", &path))), Ok)?; + .map_err(|e| server_error(format!("Could not find or decode file at '{}': {}", &path, e)))?; let mut graph = self.shared_graph.lock().unwrap(); let src_ref = graph.node_mut(self.src_node).expect("Audio graph has no source node"); diff --git a/src/source/command.rs b/src/source/command.rs new file mode 100644 index 0000000..5421327 --- /dev/null +++ b/src/source/command.rs @@ -0,0 +1,71 @@ +use std::process::{ChildStdout, Command, Stdio}; +use byteorder::{BigEndian, ReadBytesExt}; +use dsp::{Signal, Frame}; +use crate::audioformat::StandardFrame; +use super::AudioSource; + +/// An audio source that reads 32-bit floating +/// point PCM samples in big-endian from a +/// subprocess' stdout. +pub struct CommandSource { + process: ChildStdout, + command: String, + args: Vec, + source_sample_hz: f64, + reached_end: bool +} + +// TODO: Make 'command' an intermediate/processing node + +impl CommandSource { + pub fn new(command: &str, args: &[&str], source_sample_hz: f64) -> Result { + Ok(CommandSource { + process: Command::new(command) + .args(args) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .spawn().map_err(|e| format!("{:?}", e).to_owned())? + .stdout.map_or_else(|| Err("Could not fetch command's stdout".to_owned()), Ok)?, + command: command.to_owned(), + args: args.iter().map(|s| (*s).to_owned()).collect(), + source_sample_hz: source_sample_hz, + reached_end: false + }) + } + + fn read_sample(&mut self) -> Option { + self.process.read_f32::().ok() + } + + fn read_frame(&mut self) -> Option { + let left = self.read_sample()?; + let right = self.read_sample()?; + Some([left, right]) + } + + pub fn command(&self) -> &str { self.command.as_ref() } + + pub fn args(&self) -> &[String] { &self.args } +} + +impl AudioSource for CommandSource { + fn sample_hz(&self) -> f64 { self.source_sample_hz } +} + +impl Signal for CommandSource { + type Frame = StandardFrame; + + fn next(&mut self) -> StandardFrame { + if self.reached_end { + StandardFrame::equilibrium() + } else { + self.read_frame().unwrap_or_else(|| { + self.reached_end = true; + StandardFrame::equilibrium() + }) + } + } + + fn is_exhausted(&self) -> bool { self.reached_end } +} diff --git a/src/source/conv.rs b/src/source/conv.rs index fcf47e3..ae9b2f4 100644 --- a/src/source/conv.rs +++ b/src/source/conv.rs @@ -1,32 +1,48 @@ -use dsp::sample::rate::Converter; -use crate::audioformat::StandardFrame; +use dsp::{Signal, Frame, DuplexSample}; +use dsp::sample::interpolate::{Converter, Linear}; +use crate::util::either::Either; use super::AudioSource; /// An audio source that automatically converts to /// a target sample rate. -pub struct Converting where S: AudioSource { - converter: Converter, +pub struct Converting where S: AudioSource, ::Sample: DuplexSample { + inner: Either>>, target_sample_hz: f64 } -impl Converting where S: AudioSource { - pub fn to_sample_hz(target_sample_hz: f64, wrapped: S) -> Converting { +impl Converting where S: AudioSource, ::Sample: DuplexSample { + pub fn to_sample_hz(target_sample_hz: f64, mut wrapped: S) -> Converting { let source_sample_hz = wrapped.sample_hz(); Converting { - converter: Converter::from_hz_to_hz(wrapped, source_sample_hz, target_sample_hz), + inner: if target_sample_hz == source_sample_hz { + // If the sample rates happen to match exactly + // despite being stored in floating points, we + // do not need a converter + Either::Left(wrapped) + } else { + let interpolator = Linear::from_source(&mut wrapped); + Either::Right(Converter::from_hz_to_hz(wrapped, interpolator, source_sample_hz, target_sample_hz)) + }, target_sample_hz: target_sample_hz } } - pub fn wrapped(&self) -> &S { self.converter.source() } + pub fn wrapped(&self) -> &S { + match self.inner { + Either::Left(ref wrapped) => wrapped, + Either::Right(ref converter) => converter.source() + } + } } -impl AudioSource for Converting where S: AudioSource { +impl AudioSource for Converting where S: AudioSource, ::Sample: DuplexSample { fn sample_hz(&self) -> f64 { self.target_sample_hz } } -impl Iterator for Converting where S: AudioSource { - type Item = StandardFrame; +impl Signal for Converting where S: AudioSource, ::Sample: DuplexSample { + type Frame = S::Frame; + + fn next(&mut self) -> S::Frame { self.inner.next() } - fn next(&mut self) -> Option { self.converter.next() } + fn is_exhausted(&self) -> bool { self.inner.is_exhausted() } } diff --git a/src/source/file.rs b/src/source/file.rs index e7ec29f..9db7d1b 100644 --- a/src/source/file.rs +++ b/src/source/file.rs @@ -1,46 +1,54 @@ use std::io::BufReader; use std::fs::File; +use dsp::Signal; use crate::audioformat::StandardFrame; use super::mp3::Mp3Source; use super::AudioSource; /// An audio source whose format can automatically /// be determined from the file's path upon construction. -enum DecoderSource { +enum FileFormatSource { Mp3(Mp3Source>) // TODO: Other formats } -impl DecoderSource { +impl FileFormatSource { /// Tries to recognize the file's format and /// create an appropriate decoder source for it. /// Otherwise returns None. - fn from(file_path: &str) -> Option { + fn from(file_path: &str) -> Result { let splittable_path = file_path.clone(); let splitter = splittable_path.split("."); - let reader = BufReader::new(File::open(file_path).ok()?); - match splitter.last()?.as_ref() { - "mp3" => Some(DecoderSource::Mp3(Mp3Source::new(reader))), + let reader = BufReader::new(File::open(file_path).map_err(|e| format!("{:?}", e).to_owned())?); + let extension = splitter.last().map_or_else(|| Err("File has no extension"), Ok)?; + match extension.as_ref() { + "mp3" => Ok(FileFormatSource::Mp3(Mp3Source::new(reader))), // TODO: Other formats - _ => None + _ => Err(format!("Unsupported file extension: {}", extension).to_owned()) } } } -impl AudioSource for DecoderSource { +impl AudioSource for FileFormatSource { fn sample_hz(&self) -> f64 { match *self { - DecoderSource::Mp3(ref src) => src.sample_hz() + FileFormatSource::Mp3(ref src) => src.sample_hz() } } } -impl Iterator for DecoderSource { - type Item = StandardFrame; +impl Signal for FileFormatSource { + type Frame = StandardFrame; - fn next(&mut self) -> Option { + fn next(&mut self) -> StandardFrame { match *self { - DecoderSource::Mp3(ref mut src) => src.next() + FileFormatSource::Mp3(ref mut src) => src.next() + } + } + + fn is_exhausted(&self) -> bool { + match *self { + FileFormatSource::Mp3(ref src) => src.is_exhausted() } } } @@ -48,14 +56,14 @@ impl Iterator for DecoderSource { /// A wrapper around a decoder source that maintains /// a path to the file it was created from. pub struct FileSource { - decoder: DecoderSource, + wrapped: FileFormatSource, file_path: String } impl FileSource { - pub fn new(file_path: &str) -> Option { - Some(FileSource { - decoder: DecoderSource::from(file_path)?, + pub fn new(file_path: &str) -> Result { + Ok(FileSource { + wrapped: FileFormatSource::from(file_path)?, file_path: file_path.to_owned() }) } @@ -64,11 +72,13 @@ impl FileSource { } impl AudioSource for FileSource { - fn sample_hz(&self) -> f64 { self.decoder.sample_hz() } + fn sample_hz(&self) -> f64 { self.wrapped.sample_hz() } } -impl Iterator for FileSource { - type Item = StandardFrame; +impl Signal for FileSource { + type Frame = StandardFrame; + + fn next(&mut self) -> StandardFrame { self.wrapped.next() } - fn next(&mut self) -> Option { self.decoder.next() } + fn is_exhausted(&self) -> bool { self.wrapped.is_exhausted() } } diff --git a/src/source/mod.rs b/src/source/mod.rs index 2aa9e73..4aac680 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -2,25 +2,63 @@ pub mod mp3; pub mod file; +pub mod command; pub mod pausable; pub mod conv; -use crate::audioformat::StandardFrame; +use std::marker::PhantomData; +use dsp::{Signal, Frame}; +use crate::util::either::Either; /// A source of audio. -pub trait AudioSource: Iterator { +pub trait AudioSource: Signal { + /// The output sample rate of this + /// audio source in Hertz fn sample_hz(&self) -> f64; } +impl AudioSource for Either where L: AudioSource, R: AudioSource, F: Frame { + fn sample_hz(&self) -> f64 { + match *self { + Either::Left(ref wrapped) => wrapped.sample_hz(), + Either::Right(ref wrapped) => wrapped.sample_hz() + } + } +} + +impl Signal for Either where L: Signal, R: Signal, F: Frame { + type Frame = F; + + fn next(&mut self) -> F { + match *self { + Either::Left(ref mut wrapped) => wrapped.next(), + Either::Right(ref mut wrapped) => wrapped.next() + } + } + + fn is_exhausted(&self) -> bool { + match *self { + Either::Left(ref wrapped) => wrapped.is_exhausted(), + Either::Right(ref wrapped) => wrapped.is_exhausted() + } + } +} + /// A silent source. -pub struct EmptySource; +pub struct EquilibriumSource { + phantom: PhantomData +} + +impl EquilibriumSource { + pub fn new() -> EquilibriumSource { EquilibriumSource { phantom: PhantomData } } +} -impl AudioSource for EmptySource { +impl AudioSource for EquilibriumSource where F: Frame { fn sample_hz(&self) -> f64 { 44_100.0 } } -impl Iterator for EmptySource { - type Item = StandardFrame; +impl Signal for EquilibriumSource where F: Frame { + type Frame = F; - fn next(&mut self) -> Option { None } + fn next(&mut self) -> F { F::equilibrium() } } diff --git a/src/source/mp3.rs b/src/source/mp3.rs index 005c0c5..a23fe15 100644 --- a/src/source/mp3.rs +++ b/src/source/mp3.rs @@ -1,12 +1,13 @@ use super::AudioSource; use crate::audioformat::{StandardFrame, STANDARD_CHANNELS}; use std::io::Read; -use dsp::sample::Sample; +use dsp::{Sample, Frame, Signal}; /// An MP3 decoder. pub struct Mp3Source { decoder: minimp3::Decoder, sample_rate: f64, + eof: bool, mp3_frame_data: Vec, mp3_frame_offset: usize } @@ -24,6 +25,7 @@ impl Mp3Source where R: Read { Mp3Source { decoder: decoder, sample_rate: initial_frame.sample_rate as f64, + eof: false, mp3_frame_data: initial_frame.data, mp3_frame_offset: 0 } @@ -34,27 +36,36 @@ impl AudioSource for Mp3Source where R: Read { fn sample_hz(&self) -> f64 { self.sample_rate } } -impl Iterator for Mp3Source where R: Read { - type Item = StandardFrame; +impl Signal for Mp3Source where R: Read { + type Frame = StandardFrame; - fn next(&mut self) -> Option { - let mp3_frame_size = self.mp3_frame_data.len(); + fn next(&mut self) -> StandardFrame { + if self.eof { + StandardFrame::equilibrium() + } else { + let mp3_frame_size = self.mp3_frame_data.len(); - if self.mp3_frame_offset == mp3_frame_size { - self.mp3_frame_data = match self.decoder.next_frame() { - Ok(minimp3::Frame { data, .. }) => data, // TODO: Deal with custom channel counts - Err(minimp3::Error::Eof) => return None, - Err(e) => panic!("{:?}", e) // TODO: Handle errors - }; - self.mp3_frame_offset = 0 - } - - let frame: StandardFrame = [ - self.mp3_frame_data[self.mp3_frame_offset].to_sample(), - self.mp3_frame_data[self.mp3_frame_offset + 1].to_sample() - ]; + if self.mp3_frame_offset == mp3_frame_size { + self.mp3_frame_data = match self.decoder.next_frame() { + Ok(minimp3::Frame { data, .. }) => data, // TODO: Deal with custom channel counts + Err(minimp3::Error::Eof) => { + self.eof = true; + return StandardFrame::equilibrium(); + }, + Err(e) => panic!("{:?}", e) // TODO: Handle errors + }; + self.mp3_frame_offset = 0 + } + + let frame: StandardFrame = [ + self.mp3_frame_data[self.mp3_frame_offset].to_sample(), + self.mp3_frame_data[self.mp3_frame_offset + 1].to_sample() + ]; - self.mp3_frame_offset += STANDARD_CHANNELS; - Some(frame) + self.mp3_frame_offset += STANDARD_CHANNELS; + frame + } } + + fn is_exhausted(&self) -> bool { self.eof } } diff --git a/src/source/pausable.rs b/src/source/pausable.rs index b5416ee..ebc115d 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -1,3 +1,4 @@ +use dsp::{Signal, Frame}; use crate::audioformat::StandardFrame; use super::AudioSource; @@ -14,7 +15,7 @@ impl Pausable { pub fn paused(wrapped: S) -> Pausable { Pausable { wrapped: wrapped, paused: true } } - pub fn playing(wrapped: S) -> Pausable { Pausable { wrapped: wrapped, paused: true } } + pub fn playing(wrapped: S) -> Pausable { Pausable { wrapped: wrapped, paused: false } } pub fn with(&self, wrapped: T) -> Pausable { Pausable { wrapped: wrapped, paused: self.paused } } } @@ -23,14 +24,16 @@ impl AudioSource for Pausable where S: AudioSource { fn sample_hz(&self) -> f64 { self.wrapped.sample_hz() } } -impl Iterator for Pausable where S: AudioSource { - type Item = StandardFrame; +impl Signal for Pausable where S: Signal { + type Frame = S::Frame; - fn next(&mut self) -> Option { + fn next(&mut self) -> S::Frame { if self.paused { - None + S::Frame::equilibrium() } else { self.wrapped.next() } } + + fn is_exhausted(&self) -> bool { self.wrapped.is_exhausted() } } diff --git a/src/util/either.rs b/src/util/either.rs new file mode 100644 index 0000000..12cef73 --- /dev/null +++ b/src/util/either.rs @@ -0,0 +1,5 @@ +/// A general-purpose sum type with two values. +pub enum Either { + Left(L), + Right(R) +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..de3790a --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod either;