From 0ec7b2608c07ef209252be650118892c4dca07c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Sun, 3 Dec 2023 15:01:53 +0200 Subject: [PATCH] examples: Add an example that shows how to write subclasses with virtual methods Part-of: --- Cargo.lock | 1 + examples/Cargo.toml | 1 + .../src/bin/subclass_vfuncs/iirfilter/imp.rs | 186 ++++++++++++++++++ .../src/bin/subclass_vfuncs/iirfilter/mod.rs | 82 ++++++++ .../src/bin/subclass_vfuncs/lowpass/imp.rs | 165 ++++++++++++++++ .../src/bin/subclass_vfuncs/lowpass/mod.rs | 15 ++ examples/src/bin/subclass_vfuncs/main.rs | 66 +++++++ 7 files changed, 516 insertions(+) create mode 100644 examples/src/bin/subclass_vfuncs/iirfilter/imp.rs create mode 100644 examples/src/bin/subclass_vfuncs/iirfilter/mod.rs create mode 100644 examples/src/bin/subclass_vfuncs/lowpass/imp.rs create mode 100644 examples/src/bin/subclass_vfuncs/lowpass/mod.rs create mode 100644 examples/src/bin/subclass_vfuncs/main.rs diff --git a/Cargo.lock b/Cargo.lock index 31e353323..53bfa4ab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,7 @@ name = "examples" version = "0.22.0" dependencies = [ "anyhow", + "atomic_refcell", "byte-slice-cast", "cairo-rs", "cocoa", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 58b3b480b..5254f9df9 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -40,6 +40,7 @@ pangocairo = { git = "https://github.com/gtk-rs/gtk-rs-core", optional = true } raw-window-handle = { version = "0.5", optional = true } uds = { version = "0.4", optional = true } winit = { version = "0.29", optional = true, default-features = false, features = ["rwh_05"] } +atomic_refcell = "0.1" [target.'cfg(windows)'.dependencies] windows = { version = "0.52", features=["Win32_Graphics_Direct3D11", diff --git a/examples/src/bin/subclass_vfuncs/iirfilter/imp.rs b/examples/src/bin/subclass_vfuncs/iirfilter/imp.rs new file mode 100644 index 000000000..89ff47d32 --- /dev/null +++ b/examples/src/bin/subclass_vfuncs/iirfilter/imp.rs @@ -0,0 +1,186 @@ +// In the imp submodule we include the actual implementation + +use std::{collections::VecDeque, sync::Mutex}; + +use glib::{once_cell::sync::Lazy, prelude::*}; +use gst_audio::subclass::prelude::*; + +use byte_slice_cast::*; + +use atomic_refcell::AtomicRefCell; + +// The debug category we use below for our filter +pub static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "rsiirfilter", + gst::DebugColorFlags::empty(), + Some("Rust IIR Filter"), + ) +}); + +#[derive(Default)] +// This is the state of our filter +struct State { + a: Vec, + b: Vec, + x: VecDeque, + y: VecDeque, +} + +// This is the private data of our filter +#[derive(Default)] +pub struct IirFilter { + coeffs: Mutex, Vec)>>, + state: AtomicRefCell, +} + +// This trait registers our type with the GObject object system and +// provides the entry points for creating a new instance and setting +// up the class data +#[glib::object_subclass] +impl ObjectSubclass for IirFilter { + const NAME: &'static str = "RsIirFilter"; + const ABSTRACT: bool = true; + type Type = super::IirFilter; + type ParentType = gst_audio::AudioFilter; + type Class = super::Class; + + fn class_init(class: &mut Self::Class) { + class.set_rate = |obj, rate| obj.imp().set_rate_default(rate); + } +} + +// Implementation of glib::Object virtual methods +impl ObjectImpl for IirFilter {} + +impl GstObjectImpl for IirFilter {} + +// Implementation of gst::Element virtual methods +impl ElementImpl for IirFilter {} + +// Implementation of gst_base::BaseTransform virtual methods +impl BaseTransformImpl for IirFilter { + // Configure basetransform so that we are always running in-place, + // don't passthrough on same caps and also never call transform_ip + // in passthrough mode (which does not matter for us here). + // + // The way how our processing is implemented, in-place transformation + // is simpler. + const MODE: gst_base::subclass::BaseTransformMode = + gst_base::subclass::BaseTransformMode::AlwaysInPlace; + const PASSTHROUGH_ON_SAME_CAPS: bool = false; + const TRANSFORM_IP_ON_PASSTHROUGH: bool = false; + + fn start(&self) -> Result<(), gst::ErrorMessage> { + self.parent_start()?; + + *self.state.borrow_mut() = State::default(); + + Ok(()) + } + + fn stop(&self) -> Result<(), gst::ErrorMessage> { + self.parent_stop()?; + + *self.state.borrow_mut() = State::default(); + + Ok(()) + } + + fn transform_ip(&self, buf: &mut gst::BufferRef) -> Result { + let mut state = self.state.borrow_mut(); + + // Update coefficients if new coefficients were set + { + let mut coeffs = self.coeffs.lock().unwrap(); + + if let Some((a, b)) = coeffs.take() { + state.x.clear(); + state.y.clear(); + if !a.is_empty() { + state.y.resize(a.len() - 1, 0.0); + } + if !b.is_empty() { + state.x.resize(b.len() - 1, 0.0); + } + state.a = a; + state.b = b; + } + } + + if state.a.is_empty() | state.b.is_empty() { + return Ok(gst::FlowSuccess::Ok); + } + + let mut map = buf.map_writable().map_err(|_| { + gst::error!(CAT, imp: self, "Failed to map buffer writable"); + gst::FlowError::Error + })?; + + let samples = map.as_mut_slice_of::().unwrap(); + + assert!(state.b.len() - 1 == state.x.len()); + assert!(state.a.len() - 1 == state.y.len()); + + for sample in samples.iter_mut() { + let mut val = state.b[0] * *sample as f64; + + for (b, x) in Iterator::zip(state.b.iter().skip(1), state.x.iter()) { + val += b * x; + } + + for (a, y) in Iterator::zip(state.a.iter().skip(1), state.y.iter()) { + val -= a * y; + } + + val /= state.a[0]; + + let _ = state.x.pop_back().unwrap(); + state.x.push_front(*sample as f64); + + let _ = state.y.pop_back().unwrap(); + state.y.push_front(val); + + *sample = val as f32; + } + + Ok(gst::FlowSuccess::Ok) + } +} + +impl AudioFilterImpl for IirFilter { + fn allowed_caps() -> &'static gst::Caps { + static CAPS: Lazy = Lazy::new(|| { + // On both of pads we can only handle F32 mono at any sample rate. + gst_audio::AudioCapsBuilder::new_interleaved() + .format(gst_audio::AUDIO_FORMAT_F32) + .channels(1) + .build() + }); + + &CAPS + } + + fn setup(&self, info: &gst_audio::AudioInfo) -> Result<(), gst::LoggableError> { + self.parent_setup(info)?; + + gst::debug!(CAT, imp: self, "Rate changed to {}", info.rate()); + let obj = self.obj(); + (obj.class().as_ref().set_rate)(&obj, info.rate()); + + Ok(()) + } +} + +/// Wrappers for public methods and associated helper functions. +impl IirFilter { + pub(super) fn set_coeffs(&self, a: Vec, b: Vec) { + gst::debug!(CAT, imp: self, "Setting coefficients a: {a:?}, b: {b:?}"); + *self.coeffs.lock().unwrap() = Some((a, b)); + } +} + +/// Default virtual method implementations. +impl IirFilter { + fn set_rate_default(&self, _rate: u32) {} +} diff --git a/examples/src/bin/subclass_vfuncs/iirfilter/mod.rs b/examples/src/bin/subclass_vfuncs/iirfilter/mod.rs new file mode 100644 index 000000000..babb6c742 --- /dev/null +++ b/examples/src/bin/subclass_vfuncs/iirfilter/mod.rs @@ -0,0 +1,82 @@ +use gst::{prelude::*, subclass::prelude::*}; +use gst_audio::subclass::prelude::*; + +mod imp; + +// This here defines the public interface of our element and implements +// the corresponding traits so that it behaves like any other gst::Element +// +// GObject +// ╰──GstObject +// ╰──GstElement +// ╰──GstBaseTransform +// ╰──GstAudioFilter +// ╰──IirFilter +glib::wrapper! { + pub struct IirFilter(ObjectSubclass) @extends gst_audio::AudioFilter, gst_base::BaseTransform, gst::Element, gst::Object; +} + +/// Trait containing extension methods for `IirFilter`. +pub trait IirFilterExt: IsA { + // Sets the coefficients by getting access to the private struct and simply setting them + fn set_coeffs(&self, a: Vec, b: Vec) { + self.upcast_ref::().imp().set_coeffs(a, b) + } +} + +impl> IirFilterExt for O {} + +/// Trait to implement in `IirFilter` subclasses. +pub trait IirFilterImpl: AudioFilterImpl { + /// Called whenever the sample rate is changing. + fn set_rate(&self, rate: u32) { + self.parent_set_rate(rate); + } +} + +/// Trait containing extension methods for `IirFilterImpl`, specifically methods for chaining +/// up to the parent implementation of virtual methods. +pub trait IirFilterImplExt: IirFilterImpl { + fn parent_set_rate(&self, rate: u32) { + unsafe { + let data = Self::type_data(); + let parent_class = &*(data.as_ref().parent_class() as *mut Class); + (parent_class.set_rate)(self.obj().unsafe_cast_ref(), rate) + } + } +} + +impl IirFilterImplExt for T {} + +/// Class struct for `IirFilter`. +#[repr(C)] +pub struct Class { + parent: <::ParentType as glib::ObjectType>::GlibClassType, + + set_rate: fn(&IirFilter, rate: u32), +} + +unsafe impl ClassStruct for Class { + type Type = imp::IirFilter; +} + +impl std::ops::Deref for Class { + type Target = glib::Class<<::Type as ObjectSubclass>::ParentType>; + + fn deref(&self) -> &Self::Target { + unsafe { &*(&self.parent as *const _ as *const _) } + } +} + +unsafe impl IsSubclassable for IirFilter { + fn class_init(class: &mut glib::Class) { + Self::parent_class_init::(class); + + let class = class.as_mut(); + + class.set_rate = |obj, rate| unsafe { + let imp = obj.unsafe_cast_ref::().imp(); + imp.set_rate(rate); + }; + } +} diff --git a/examples/src/bin/subclass_vfuncs/lowpass/imp.rs b/examples/src/bin/subclass_vfuncs/lowpass/imp.rs new file mode 100644 index 000000000..280e91e95 --- /dev/null +++ b/examples/src/bin/subclass_vfuncs/lowpass/imp.rs @@ -0,0 +1,165 @@ +// In the imp submodule we include the actual implementation + +use std::sync::Mutex; + +use glib::{once_cell::sync::Lazy, prelude::*}; +use gst::prelude::*; +use gst_audio::subclass::prelude::*; + +use crate::iirfilter::{IirFilterExt, IirFilterImpl}; + +// These are the property values of our filter +pub struct Settings { + cutoff: f32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { cutoff: 0.0 } + } +} + +// This is the state of our filter +#[derive(Default)] +pub struct State { + rate: Option, +} + +// This is the private data of our filter +#[derive(Default)] +pub struct Lowpass { + settings: Mutex, + state: Mutex, +} + +// This trait registers our type with the GObject object system and +// provides the entry points for creating a new instance and setting +// up the class data +#[glib::object_subclass] +impl ObjectSubclass for Lowpass { + const NAME: &'static str = "RsLowpass"; + type Type = super::Lowpass; + type ParentType = crate::iirfilter::IirFilter; +} + +// Implementation of glib::Object virtual methods +impl ObjectImpl for Lowpass { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpecFloat::builder("cutoff") + .nick("Cutoff") + .blurb("Cutoff frequency in Hz") + .default_value(Settings::default().cutoff) + .minimum(0.0) + .mutable_playing() + .build()] + }); + + PROPERTIES.as_ref() + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "cutoff" => { + self.settings.lock().unwrap().cutoff = value.get().unwrap(); + self.calculate_coeffs(); + } + _ => unimplemented!(), + }; + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "cutoff" => self.settings.lock().unwrap().cutoff.to_value(), + _ => unimplemented!(), + } + } +} + +impl GstObjectImpl for Lowpass {} + +// Implementation of gst::Element virtual methods +impl ElementImpl for Lowpass { + // The element specific metadata. This information is what is visible from + // gst-inspect-1.0 and can also be programmatically retrieved from the gst::Registry + // after initial registration without having to load the plugin in memory. + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "Lowpass Filter", + "Filter/Effect/Audio", + "A Lowpass audio filter", + "Sebastian Dröge ", + ) + }); + + Some(&*ELEMENT_METADATA) + } +} + +// Implementation of gst_base::BaseTransform virtual methods +impl BaseTransformImpl for Lowpass { + const MODE: gst_base::subclass::BaseTransformMode = + <::Subclass>::MODE; + const PASSTHROUGH_ON_SAME_CAPS: bool = + <::Subclass>::PASSTHROUGH_ON_SAME_CAPS; + const TRANSFORM_IP_ON_PASSTHROUGH: bool = + <::Subclass>::TRANSFORM_IP_ON_PASSTHROUGH; + + fn start(&self) -> Result<(), gst::ErrorMessage> { + self.parent_start()?; + + *self.state.lock().unwrap() = State::default(); + + Ok(()) + } +} + +// Implement of gst_audio::AudioFilter virtual methods +impl AudioFilterImpl for Lowpass {} + +// Implement of IirFilter virtual methods +impl IirFilterImpl for Lowpass { + fn set_rate(&self, rate: u32) { + self.state.lock().unwrap().rate = Some(rate); + self.calculate_coeffs(); + } +} + +impl Lowpass { + fn calculate_coeffs(&self) { + use std::f64; + + let Some(rate) = self.state.lock().unwrap().rate else { + return; + }; + let cutoff = self.settings.lock().unwrap().cutoff; + + // See Audio EQ Cookbook + // https://www.w3.org/TR/audio-eq-cookbook + let cutoff = cutoff as f64 / rate as f64; + + let omega = 2.0 * f64::consts::PI * cutoff; + let q = 1.0; + + let alpha = f64::sin(omega) / (2.0 * q); + + let mut b = vec![ + (1.0 - f64::cos(omega)) / 2.0, + 1.0 - f64::cos(omega), + (1.0 - f64::cos(omega) / 2.0), + ]; + + let mut a = vec![1.0 + alpha, -2.0 * f64::cos(omega), 1.0 - alpha]; + + let a0 = a[0]; + for a in &mut a { + *a /= a0; + } + for b in &mut b { + *b /= a0; + } + + self.obj().set_coeffs(a, b); + } +} diff --git a/examples/src/bin/subclass_vfuncs/lowpass/mod.rs b/examples/src/bin/subclass_vfuncs/lowpass/mod.rs new file mode 100644 index 000000000..230a44170 --- /dev/null +++ b/examples/src/bin/subclass_vfuncs/lowpass/mod.rs @@ -0,0 +1,15 @@ +mod imp; + +// This here defines the public interface of our element and implements +// the corresponding traits so that it behaves like any other gst::Element +// +// GObject +// ╰──GstObject +// ╰──GstElement +// ╰──GstBaseTransform +// ╰──GstAudioFilter +// ╰──IirFilter +// ╰──Lowpass +glib::wrapper! { + pub struct Lowpass(ObjectSubclass) @extends crate::iirfilter::IirFilter, gst_audio::AudioFilter, gst_base::BaseTransform, gst::Element, gst::Object; +} diff --git a/examples/src/bin/subclass_vfuncs/main.rs b/examples/src/bin/subclass_vfuncs/main.rs new file mode 100644 index 000000000..fa5e36544 --- /dev/null +++ b/examples/src/bin/subclass_vfuncs/main.rs @@ -0,0 +1,66 @@ +// This example implements a baseclass IirFilter, and a subclass Lowpass of that. +// +// The example shows how to provide and implement virtual methods, and how to provide non-virtual +// methods on the base class. + +use gst::prelude::*; + +mod iirfilter; +mod lowpass; + +#[path = "../../examples-common.rs"] +mod examples_common; + +fn example_main() { + gst::init().unwrap(); + + let pipeline = gst::Pipeline::new(); + let src = gst::ElementFactory::make("audiotestsrc") + .property_from_str("wave", "white-noise") + .build() + .unwrap(); + let filter = glib::Object::builder::() + .property("cutoff", 4000.0f32) + .build(); + let conv = gst::ElementFactory::make("audioconvert").build().unwrap(); + let sink = gst::ElementFactory::make("autoaudiosink").build().unwrap(); + + pipeline + .add_many([&src, filter.as_ref(), &conv, &sink]) + .unwrap(); + gst::Element::link_many([&src, filter.as_ref(), &conv, &sink]).unwrap(); + + let bus = pipeline.bus().unwrap(); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + for msg in bus.iter_timed(gst::ClockTime::NONE) { + use gst::MessageView; + + match msg.view() { + MessageView::Eos(..) => break, + MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + err.src().map(|s| s.path_string()), + err.error(), + err.debug() + ); + break; + } + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); +} + +fn main() { + // tutorials_common::run is only required to set up the application environment on macOS + // (but not necessary in normal Cocoa applications where this is set up automatically) + examples_common::run(example_main); +}