diff --git a/.github/workflows/dasp.yml b/.github/workflows/dasp.yml index 398aad21..30ac1d8e 100644 --- a/.github/workflows/dasp.yml +++ b/.github/workflows/dasp.yml @@ -63,6 +63,11 @@ jobs: with: command: test args: --manifest-path dasp_envelope/Cargo.toml --no-default-features --verbose + - name: cargo test dasp_filter (no default features) + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path dasp_filter/Cargo.toml --no-default-features --verbose - name: cargo test dasp_frame (no default features) uses: actions-rs/cargo@v1 with: @@ -270,6 +275,9 @@ jobs: - name: cargo publish dasp_graph continue-on-error: true run: cargo publish --token $CRATESIO_TOKEN --manifest-path dasp_graph/Cargo.toml + - name: cargo publish dasp_filter + continue-on-error: true + run: cargo publish --token $CRATESIO_TOKEN --manifest-path dasp_filter/Cargo.toml - name: wait for crates.io run: sleep 5 - name: cargo publish dasp diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4b39ff..7a9d531a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Unreleased -- Renamed `window-hanning` to `window-hann` +- Renamed `window-hanning` to `window-hann`. +- Add `dasp_filter` crate (#145). + - Allows for filtering of samples using a digital biquad filter. + - Add `filter` feature gate to `dasp_signal`. + - Add `signal-filter` feature gate to `dasp`. --- diff --git a/Cargo.toml b/Cargo.toml index 920f3cdf..a0a1cf97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "dasp", "dasp_envelope", + "dasp_filter", "dasp_frame", "dasp_graph", "dasp_interpolate", diff --git a/dasp/Cargo.toml b/dasp/Cargo.toml index 0e780e4c..46388838 100644 --- a/dasp/Cargo.toml +++ b/dasp/Cargo.toml @@ -12,6 +12,7 @@ edition = "2018" [dependencies] dasp_envelope = { version = "0.11", path = "../dasp_envelope", default-features = false, optional = true } +dasp_filter = { version = "0.11", path = "../dasp_filter", default-features = false, optional = true } dasp_frame = { version = "0.11", path = "../dasp_frame", default-features = false } dasp_graph = { version = "0.11", path = "../dasp_graph", default-features = false, optional = true } dasp_interpolate = { version = "0.11", path = "../dasp_interpolate", default-features = false, optional = true } @@ -36,6 +37,7 @@ all-no-std = [ "envelope", "envelope-peak", "envelope-rms", + "filter", "interpolate", "interpolate-floor", "interpolate-linear", @@ -47,6 +49,7 @@ all-no-std = [ "signal-boxed", "signal-bus", "signal-envelope", + "signal-filter", "signal-rms", "signal-window", "signal-window-hann", @@ -59,6 +62,7 @@ all-no-std = [ ] std = [ "dasp_envelope/std", + "dasp_filter/std", "dasp_frame/std", "dasp_interpolate/std", "dasp_peak/std", @@ -72,6 +76,7 @@ std = [ envelope = ["dasp_envelope"] envelope-peak = ["dasp_envelope/peak"] envelope-rms = ["dasp_envelope/rms"] +filter = ["dasp_filter"] graph = ["dasp_graph"] graph-all-nodes = ["dasp_graph/all-nodes"] graph-node-boxed = ["dasp_graph/node-boxed"] @@ -90,6 +95,7 @@ signal = ["dasp_signal"] signal-boxed = ["dasp_signal/boxed"] signal-bus = ["dasp_signal/bus"] signal-envelope = ["dasp_signal/envelope", "envelope"] +signal-filter = ["dasp_signal/filter", "filter"] signal-rms = ["dasp_signal/rms", "rms"] signal-window = ["dasp_signal/window", "window"] signal-window-hann = ["dasp_signal/window-hann", "window-hann"] diff --git a/dasp/src/lib.rs b/dasp/src/lib.rs index 191dc7f0..535ade1f 100644 --- a/dasp/src/lib.rs +++ b/dasp/src/lib.rs @@ -104,6 +104,9 @@ #[cfg(feature = "envelope")] #[doc(inline)] pub use dasp_envelope as envelope; +#[cfg(feature = "filter")] +#[doc(inline)] +pub use dasp_filter as filter; #[doc(inline)] pub use dasp_frame::{self as frame, Frame}; // TODO: Remove `std` requirement once `dasp_graph` gains `no_std` support. diff --git a/dasp_filter/Cargo.toml b/dasp_filter/Cargo.toml new file mode 100644 index 00000000..d425e783 --- /dev/null +++ b/dasp_filter/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dasp_filter" +description = "Filters for digital audio signals." +version = "0.11.0" +authors = ["mitchmindtree ", "Mark LeMoine "] +readme = "../README.md" +keywords = ["dsp", "filter", "biquad"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rustaudio/dasp.git" +homepage = "https://github.com/rustaudio/dasp" +edition = "2018" + +[dependencies] +dasp_frame = { version = "0.11", path = "../dasp_frame", default-features = false } +dasp_sample = { version = "0.11", path = "../dasp_sample", default-features = false } + +[features] +default = ["std"] +std = [ + "dasp_frame/std", + "dasp_sample/std", +] + +[package.metadata.docs.rs] +all-features = true diff --git a/dasp_filter/src/lib.rs b/dasp_filter/src/lib.rs new file mode 100644 index 00000000..23700f21 --- /dev/null +++ b/dasp_filter/src/lib.rs @@ -0,0 +1,117 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(not(feature = "std"), feature(core_intrinsics))] + +use dasp_frame::Frame; +use dasp_sample::{Duplex, FloatSample, FromSample, ToSample}; + +/// Coefficients for a digital biquad filter. +/// It is assumed that the `a0` coefficient is always normalized to 1.0, +/// and thus not included. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Coefficients +where + S: FloatSample, +{ + // Transfer function numerator coefficients. + pub b0: S, + pub b1: S, + pub b2: S, + + // Transfer function denominator coefficients. + pub a1: S, + pub a2: S, +} + +/// An implementation of a digital biquad filter, using the Direct Form 2 +/// Transposed (DF2T) representation. +pub struct Biquad +where + F: Frame, + F::Sample: FloatSample, +{ + pub coeff: Coefficients, + + // Since biquad filters are second-order, we require two historical buffers. + // This state is updated each time the filter is applied to a `Frame`. + t0: F, + t1: F, +} + +impl Biquad +where + F: Frame, + F::Sample: FloatSample, +{ + pub fn new(coeff: Coefficients) -> Self { + Self { + coeff, + t0: F::EQUILIBRIUM, + t1: F::EQUILIBRIUM, + } + } + + /// Performs a single iteration of this filter, calculating a new filtered + /// `Frame` from an input `Frame`. + /// + /// ```rust + /// use dasp_filter::{Coefficients, Biquad}; + /// + /// fn main() { + /// // Notch boost filter. + /// let co = Coefficients { + /// b0: 1.0469127398708575f64, + /// b1: -0.27732002669854483, + /// b2: 0.8588151488168104, + /// a1: -0.27732002669854483, + /// a2: 0.9057278886876682, + /// }; + /// + /// // Note that this type argument defines the format of the temporary + /// // values, as well as the number of channels required for input + /// // `Frame`s. + /// let mut b = Biquad::<[f64; 2]>::new(co); + /// + /// assert_eq!(b.apply([32i8, -64]), [33, -67]); + /// assert_eq!(b.apply([0.1f32, -0.3]), [0.107943736, -0.32057875]); + /// } + /// ``` + pub fn apply(&mut self, input: I) -> I + where + I: Frame, + I::Sample: Duplex, + { + // Convert into floating point representation. + let input: F = input.map(ToSample::to_sample_); + + // Calculate scaled inputs. + let input_by_b0 = input.scale_amp(self.coeff.b0); + let input_by_b1 = input.scale_amp(self.coeff.b1); + let input_by_b2 = input.scale_amp(self.coeff.b2); + + // This is the new filtered `Frame`. + let output: F = self.t0.add_amp(input_by_b0); + + // Calculate scaled outputs. + // NOTE: Negative signs on the scaling factors for these. + let output_by_neg_a1 = output.scale_amp(-self.coeff.a1); + let output_by_neg_a2 = output.scale_amp(-self.coeff.a2); + + // Update buffers. + self.t0 = self.t1.add_amp(input_by_b1).add_amp(output_by_neg_a1); + self.t1 = input_by_b2.add_amp(output_by_neg_a2); + + // Convert back into the original `Frame` format. + output.map(FromSample::from_sample_) + } +} + +impl From> for Biquad +where + F: Frame, + F::Sample: FloatSample, +{ + // Same as `new()`, but adding this for the blanket `Into` impl. + fn from(coeff: Coefficients) -> Self { + Self::new(coeff) + } +} diff --git a/dasp_signal/Cargo.toml b/dasp_signal/Cargo.toml index 139b1b71..95764383 100644 --- a/dasp_signal/Cargo.toml +++ b/dasp_signal/Cargo.toml @@ -12,6 +12,7 @@ edition = "2018" [dependencies] dasp_envelope = { version = "0.11", path = "../dasp_envelope", default-features = false, optional = true } +dasp_filter = { version = "0.11", path = "../dasp_filter", default-features = false, optional = true } dasp_frame = { version = "0.11", path = "../dasp_frame", default-features = false } dasp_interpolate = { version = "0.11", path = "../dasp_interpolate", default-features = false } dasp_peak = { version = "0.11", path = "../dasp_peak", default-features = false } @@ -32,6 +33,7 @@ all-no-std = [ "boxed", "bus", "envelope", + "filter", "rms", "window", "window-hann", @@ -39,6 +41,7 @@ all-no-std = [ ] std = [ "dasp_envelope/std", + "dasp_filter/std", "dasp_frame/std", "dasp_interpolate/std", "dasp_peak/std", @@ -50,6 +53,7 @@ std = [ boxed = [] bus = [] envelope = ["dasp_envelope"] +filter = ["dasp_filter"] rms = ["dasp_rms"] window = ["dasp_window"] window-hann = ["dasp_window/hann"] diff --git a/dasp_signal/src/filter.rs b/dasp_signal/src/filter.rs new file mode 100644 index 00000000..7dd45e1e --- /dev/null +++ b/dasp_signal/src/filter.rs @@ -0,0 +1,96 @@ +//! An extension to the **Signal** trait that enables iterative filtering. +//! +//! ### Required Features +//! +//! - When using `dasp_signal`, this item requires the **filter** feature to be enabled. +//! - When using `dasp`, this item requires the **signal-filter** feature to be enabled. + +use crate::Signal; +use dasp_filter as filter; +use dasp_frame::Frame; +use dasp_sample::{FromSample, Sample}; + +/// An extension to the **Signal** trait that enables iterative filtering. +/// +/// # Example +/// +/// ``` +/// use dasp_filter::{self as filter, Coefficients}; +/// use dasp_signal::{self as signal, Signal}; +/// use dasp_signal::filter::SignalFilter; +/// +/// fn main() { +/// let signal = signal::rate(48000.0).const_hz(997.0).sine(); +/// // Notch filter to attenuate 997 Hz. +/// let coeff = Coefficients { +/// b0: 0.9157328640471359f64, +/// b1: -1.8158910212730535, +/// b2: 0.9157328640471359, +/// a1: -1.8158910212730535, +/// a2: 0.831465728094272, +/// }; +/// let mut filtered = signal.filtered(coeff); +/// assert_eq!( +/// filtered.take(4).collect::>(), +/// vec![0.0, 0.11917058366454024, 0.21640079287630784, 0.2938740006664008] +/// ); +/// } +/// ``` +/// +/// ### Required Features +/// +/// - When using `dasp_signal`, this item requires the **filter** feature to be enabled. +/// - When using `dasp`, this item requires the **signal-filter** feature to be enabled. +pub trait SignalFilter: Signal { + fn filtered( + self, + coeff: filter::Coefficients<<::Sample as Sample>::Float>, + ) -> FilteredSignal + where + Self: Sized, + ::Sample: + FromSample<<::Sample as Sample>::Float>, + { + let biquad = filter::Biquad::from(coeff); + + FilteredSignal { + signal: self, + biquad, + } + } +} + +/// An adaptor that calculates and yields a filtered signal. +/// +/// ### Required Features +/// +/// - When using `dasp_signal`, this item requires the **filter** feature to be enabled. +/// - When using `dasp`, this item requires the **signal-filter** feature to be enabled. +pub struct FilteredSignal +where + S: Signal, + ::Sample: FromSample<<::Sample as Sample>::Float>, +{ + signal: S, + biquad: filter::Biquad<::Float>, +} + +impl Signal for FilteredSignal +where + S: Signal, + ::Sample: FromSample<<::Sample as Sample>::Float>, +{ + // Output is the same type as the input. + type Frame = S::Frame; + + fn next(&mut self) -> Self::Frame { + self.biquad.apply(self.signal.next()) + } + + fn is_exhausted(&self) -> bool { + self.signal.is_exhausted() + } +} + +// Impl this for all `Signal`s. +impl SignalFilter for T where T: Signal {} diff --git a/dasp_signal/src/lib.rs b/dasp_signal/src/lib.rs index da36d9f2..19db379b 100644 --- a/dasp_signal/src/lib.rs +++ b/dasp_signal/src/lib.rs @@ -63,6 +63,8 @@ mod boxed; pub mod bus; #[cfg(feature = "envelope")] pub mod envelope; +#[cfg(feature = "filter")] +pub mod filter; #[cfg(feature = "rms")] pub mod rms; #[cfg(feature = "window")]