From d13d488845b37c88353e706308b883335afa3207 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 6 Apr 2023 23:31:31 +0200 Subject: [PATCH] qoa: Add support for Quite OK Audio format --- Cargo.toml | 2 + audio/qoa/Cargo.toml | 46 +++++ audio/qoa/build.rs | 3 + audio/qoa/src/lib.rs | 32 ++++ audio/qoa/src/qoadec/imp.rs | 334 ++++++++++++++++++++++++++++++++++ audio/qoa/src/qoadec/mod.rs | 37 ++++ audio/qoa/src/qoaparse/imp.rs | 186 +++++++++++++++++++ audio/qoa/src/qoaparse/mod.rs | 37 ++++ audio/qoa/src/typefind.rs | 22 +++ meson.build | 1 + meson_options.txt | 1 + 11 files changed, 701 insertions(+) create mode 100644 audio/qoa/Cargo.toml create mode 100644 audio/qoa/build.rs create mode 100644 audio/qoa/src/lib.rs create mode 100644 audio/qoa/src/qoadec/imp.rs create mode 100644 audio/qoa/src/qoadec/mod.rs create mode 100644 audio/qoa/src/qoaparse/imp.rs create mode 100644 audio/qoa/src/qoaparse/mod.rs create mode 100644 audio/qoa/src/typefind.rs diff --git a/Cargo.toml b/Cargo.toml index 279f43e0..d247e6ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "audio/csound", "audio/lewton", "audio/spotify", + "audio/qoa", "generic/file", "generic/sodium", @@ -63,6 +64,7 @@ default-members = [ "audio/audiofx", "audio/claxon", "audio/lewton", + "audio/qoa", "generic/threadshare", "generic/inter", diff --git a/audio/qoa/Cargo.toml b/audio/qoa/Cargo.toml new file mode 100644 index 00000000..4279e953 --- /dev/null +++ b/audio/qoa/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "gst-plugin-qoa" +version = "0.11.0-alpha.1" +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +license = "MPL-2.0" +edition = "2021" +rust-version = "1.66" +description = "GStreamer QOA (Quite OK Audio) Decoder Plugin" + +[dependencies] +gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +qoaudio = "0.5.0" +byte-slice-cast = "1.0" +atomic_refcell = "0.1" +once_cell = "1.0" + +[dev-dependencies] +gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } + +[lib] +name = "gstqoa" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../version-helper" } + +[features] +static = [] +capi = [] +doc = ["gst/v1_18"] + +[package.metadata.capi] +min_version = "0.8.0" + +[package.metadata.capi.header] +enabled = false + +[package.metadata.capi.library] +install_subdir = "gstreamer-1.0" +versioning = false + +[package.metadata.capi.pkg_config] +requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0" diff --git a/audio/qoa/build.rs b/audio/qoa/build.rs new file mode 100644 index 00000000..cda12e57 --- /dev/null +++ b/audio/qoa/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/audio/qoa/src/lib.rs b/audio/qoa/src/lib.rs new file mode 100644 index 00000000..ded771e8 --- /dev/null +++ b/audio/qoa/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright (C) 2023 Rafael Caricio +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; + +mod qoadec; +mod qoaparse; +mod typefind; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + qoadec::register(plugin)?; + qoaparse::register(plugin)?; + typefind::register(plugin) +} + +gst::plugin_define!( + qoa, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + // FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known) + "MPL", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/audio/qoa/src/qoadec/imp.rs b/audio/qoa/src/qoadec/imp.rs new file mode 100644 index 00000000..b358536c --- /dev/null +++ b/audio/qoa/src/qoadec/imp.rs @@ -0,0 +1,334 @@ +// Copyright (C) 2023 Rafael Caricio +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use atomic_refcell::AtomicRefCell; +use byte_slice_cast::*; +use gst::glib; +use gst::subclass::prelude::*; +use gst_audio::prelude::*; +use gst_audio::subclass::prelude::*; +use once_cell::sync::Lazy; +use qoaudio::{DecodedAudio, QoaDecoder}; + +#[derive(Default)] +struct State { + decoder: Option, + audio_info: Option, +} + +#[derive(Default)] +pub struct QoaDec { + state: AtomicRefCell>, +} + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "qoadec", + gst::DebugColorFlags::empty(), + Some("Quite OK Audio decoder"), + ) +}); + +#[glib::object_subclass] +impl ObjectSubclass for QoaDec { + const NAME: &'static str = "GstQoaDec"; + type Type = super::QoaDec; + type ParentType = gst_audio::AudioDecoder; +} + +impl ObjectImpl for QoaDec {} + +impl GstObjectImpl for QoaDec {} + +impl ElementImpl for QoaDec { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "QOA decoder", + "Decoder/Audio", + "Quite OK Audio decoder", + "Rafael Caricio ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + let sink_caps = gst::Caps::builder("audio/x-qoa") + .field("parsed", true) + .build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + + let src_caps = gst_audio::AudioCapsBuilder::new_interleaved() + .format(gst_audio::AUDIO_FORMAT_S16) + .rate_range(1..16_777_215) + .channels_range(1..8) + .build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + + vec![sink_pad_template, src_pad_template] + }); + + PAD_TEMPLATES.as_ref() + } +} + +impl AudioDecoderImpl for QoaDec { + fn start(&self) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, imp: self, "Starting..."); + + *self.state.borrow_mut() = Some(State::default()); + Ok(()) + } + + fn stop(&self) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, imp: self, "Stopping..."); + + *self.state.borrow_mut() = None; + Ok(()) + } + + fn set_format(&self, caps: &gst::Caps) -> Result<(), gst::LoggableError> { + gst::debug!(CAT, imp: self, "Setting format {:?}", caps); + Ok(()) + } + + fn handle_frame( + &self, + inbuf: Option<&gst::Buffer>, + ) -> Result { + gst::debug!(CAT, imp: self, "Handling buffer {:?}", inbuf); + + let inbuf = match inbuf { + None => return Ok(gst::FlowSuccess::Ok), + Some(inbuf) => inbuf, + }; + + let inmap = inbuf.map_readable().map_err(|_| { + gst::error!(CAT, imp: self, "Failed to buffer readable"); + gst::FlowError::Error + })?; + + let mut state_guard = self.state.borrow_mut(); + let state = state_guard.as_mut().ok_or_else(|| { + gst::error!(CAT, imp: self, "Failed to get state"); + gst::FlowError::NotNegotiated + })?; + + self.handle_buffer(state, &inmap) + } +} + +impl QoaDec { + fn handle_buffer( + &self, + state: &mut State, + indata: &[u8], + ) -> Result { + let decoder = match state.decoder { + Some(ref mut decoder) => decoder, + None => { + let decoder = QoaDecoder::decode_header(indata).unwrap_or_else(|err| { + gst::debug!( + CAT, + imp: self, + "Using decoder in streaming mode, cause: {}", + err + ); + QoaDecoder::streaming() + }); + state.decoder = Some(decoder); + state.decoder.as_mut().unwrap() + } + }; + + gst::trace!( + CAT, + imp: self, + "Trying to decode frame... indata: {}", + indata.len() + ); + + let audio: DecodedAudio = decoder + .decode_frames(indata) + .and_then(|frames| frames.try_into()) + .map_err(|err| { + gst::element_error!( + self.obj(), + gst::CoreError::Negotiation, + ["Failed to decode frames: {}", err] + ); + gst::FlowError::Error + })?; + + gst::trace!(CAT, imp: self, "Decoded audio: {:?}", audio.duration()); + + // On new buffers the audio configuration might change, if so we need to request renegotiation + // and reconfigure the audio info + if state.audio_info.is_none() + || state.audio_info.as_ref().unwrap().channels() != audio.channels() + || state.audio_info.as_ref().unwrap().rate() != audio.sample_rate() + { + let audio_info = get_audio_info(&audio).map_err(|e| { + gst::element_error!( + self.obj(), + gst::CoreError::Negotiation, + ["Failed to get audio info: {}", e] + ); + gst::FlowError::Error + })?; + + gst::debug!( + CAT, + imp: self, + "Successfully parsed headers: {:?}", + audio_info + ); + + self.obj().set_output_format(&audio_info)?; + self.obj().negotiate()?; + + state.audio_info = Some(audio_info); + } + + let samples = audio.collect::>(); + + struct CastVec(Vec); + impl AsRef<[u8]> for CastVec { + fn as_ref(&self) -> &[u8] { + self.0.as_byte_slice() + } + } + impl AsMut<[u8]> for CastVec { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut_byte_slice() + } + } + + let outbuf = gst::Buffer::from_mut_slice(CastVec(samples)); + self.obj().finish_frame(Some(outbuf), 1) + } +} + +fn get_audio_info(audio: &DecodedAudio) -> Result { + let index = match audio.channels() as usize { + 0 => return Err("no channels".to_string()), + n if n > 8 => return Err("more than 8 channels, not supported yet".to_string()), + n => n, + }; + let to = &QOA_CHANNEL_POSITIONS[index - 1][..index]; + let info_builder = gst_audio::AudioInfo::builder( + gst_audio::AUDIO_FORMAT_S16, + audio.sample_rate(), + audio.channels(), + ) + .positions(to); + + let audio_info = info_builder + .build() + .map_err(|e| format!("failed to build audio info: {e}"))?; + + Ok(audio_info) +} + +const QOA_CHANNEL_POSITIONS: [[gst_audio::AudioChannelPosition; 8]; 8] = [ + [ + gst_audio::AudioChannelPosition::Mono, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::RearLeft, + gst_audio::AudioChannelPosition::RearRight, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::RearLeft, + gst_audio::AudioChannelPosition::RearRight, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::Lfe1, + gst_audio::AudioChannelPosition::RearLeft, + gst_audio::AudioChannelPosition::RearRight, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::Lfe1, + gst_audio::AudioChannelPosition::RearCenter, + gst_audio::AudioChannelPosition::SideLeft, + gst_audio::AudioChannelPosition::SideRight, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::Lfe1, + gst_audio::AudioChannelPosition::RearLeft, + gst_audio::AudioChannelPosition::RearRight, + gst_audio::AudioChannelPosition::SideLeft, + gst_audio::AudioChannelPosition::SideRight, + ], +]; diff --git a/audio/qoa/src/qoadec/mod.rs b/audio/qoa/src/qoadec/mod.rs new file mode 100644 index 00000000..abe58774 --- /dev/null +++ b/audio/qoa/src/qoadec/mod.rs @@ -0,0 +1,37 @@ +// Copyright (C) 2023 Rafael Caricio +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 +/** + * element-qoadec: + * @short_description: Decode audio encoded in QOA format. + * + * Decoder for the Quite OK Audio format. Supports file and streaming modes. + * + * ## Example pipeline + * ```bash + * gst-launch-1.0 filesrc location=audio.qoa ! qoaparse ! qoadec ! autoaudiosink + * ``` + * + * Since: plugins-rs-0.11.0-alpha.1 + */ +use gst::glib; +use gst::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct QoaDec(ObjectSubclass) @extends gst_audio::AudioDecoder, gst::Element, gst::Object; +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "qoadec", + gst::Rank::Marginal, + QoaDec::static_type(), + ) +} diff --git a/audio/qoa/src/qoaparse/imp.rs b/audio/qoa/src/qoaparse/imp.rs new file mode 100644 index 00000000..3e12224e --- /dev/null +++ b/audio/qoa/src/qoaparse/imp.rs @@ -0,0 +1,186 @@ +// Copyright (C) 2023 Rafael Caricio +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; +use gst::subclass::prelude::*; +use gst_base::prelude::*; +use gst_base::subclass::prelude::*; +use once_cell::sync::Lazy; +use qoaudio::{QOA_HEADER_SIZE, QOA_MAGIC, QOA_MIN_FILESIZE}; + +#[derive(Default)] +pub struct QoaParse; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "qoaparse", + gst::DebugColorFlags::empty(), + Some("Quite OK Audio parser"), + ) +}); + +#[glib::object_subclass] +impl ObjectSubclass for QoaParse { + const NAME: &'static str = "GstQoaParse"; + type Type = super::QoaParse; + type ParentType = gst_base::BaseParse; +} + +impl ObjectImpl for QoaParse {} + +impl GstObjectImpl for QoaParse {} + +impl ElementImpl for QoaParse { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "QOA parser", + "Codec/Parser/Audio", + "Quite OK Audio parser", + "Rafael Caricio ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + let sink_caps = gst::Caps::builder("audio/x-qoa").build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + + let src_caps = gst::Caps::builder("audio/x-qoa") + .field("parsed", true) + .build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + + vec![src_pad_template, sink_pad_template] + }); + + PAD_TEMPLATES.as_ref() + } +} + +impl BaseParseImpl for QoaParse { + fn start(&self) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, imp: self, "Starting..."); + + self.obj().set_min_frame_size(QOA_MIN_FILESIZE as u32); + Ok(()) + } + + fn handle_frame( + &self, + mut frame: gst_base::BaseParseFrame, + ) -> Result<(gst::FlowSuccess, u32), gst::FlowError> { + gst::trace!(CAT, imp: self, "Handling frame..."); + + if self.obj().src_pad().current_caps().is_none() { + // Set src pad caps + let src_caps = gst::Caps::builder("audio/x-qoa") + .field("parsed", true) + .build(); + + gst::debug!(CAT, imp: self, "Setting src pad caps {:?}", src_caps); + + self.obj() + .src_pad() + .push_event(gst::event::Caps::new(&src_caps)); + } + + let input = frame.buffer().unwrap(); + let map = input.map_readable().map_err(|_| { + gst::element_imp_error!( + self, + gst::CoreError::Failed, + ["Failed to map input buffer readable"] + ); + gst::FlowError::Error + })?; + let data = map.as_slice(); + + let file_header_size = { + if data.len() >= QOA_MIN_FILESIZE { + let magic = u32::from_be_bytes(data[0..4].try_into().unwrap()); + if magic == QOA_MAGIC { + QOA_HEADER_SIZE + } else { + 0 + } + } else { + 0 + } + }; + + if data.len() < (file_header_size + QOA_HEADER_SIZE) { + // Error with not enough bytes to read the frame header + gst::element_imp_error!( + self, + gst::CoreError::Failed, + ["Not enough bytes to read the frame header"] + ); + return Err(gst::FlowError::Error); + } + + let frame_header = u64::from_be_bytes( + data[file_header_size..(file_header_size + QOA_HEADER_SIZE)] + .try_into() + .unwrap(), + ); + let channels = ((frame_header >> 56) & 0x0000ff) as u64; + let sample_rate = ((frame_header >> 32) & 0xffffff) as u64; + let total_samples = ((frame_header >> 16) & 0x00ffff) as u64; + let frame_size = (frame_header & 0x00ffff) as usize; + + if data.len() < (file_header_size + frame_size) { + gst::trace!( + CAT, + imp: self, + "Not enough bytes to read the frame, need {} bytes, have {}. Waiting for more data...", + file_header_size + frame_size, + data.len() + ); + return Ok((gst::FlowSuccess::Ok, 0)); + } + + drop(map); + + let duration = total_samples + .mul_div_floor(*gst::ClockTime::SECOND, sample_rate) + .map(gst::ClockTime::from_nseconds); + + let buffer = frame.buffer_mut().unwrap(); + buffer.set_duration(duration); + if file_header_size > 0 { + buffer.set_flags(gst::BufferFlags::HEADER); + } + + gst::trace!( + CAT, + imp: self, + "Found frame channels={channels}, sample_rate={sample_rate}, total_samples={total_samples}, size={frame_size}, duration={duration:?}", + ); + + self.obj() + .finish_frame(frame, (file_header_size + frame_size) as u32)?; + + Ok((gst::FlowSuccess::Ok, 0)) + } +} diff --git a/audio/qoa/src/qoaparse/mod.rs b/audio/qoa/src/qoaparse/mod.rs new file mode 100644 index 00000000..c17b34e1 --- /dev/null +++ b/audio/qoa/src/qoaparse/mod.rs @@ -0,0 +1,37 @@ +// Copyright (C) 2023 Rafael Caricio +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 +/** + * element-qoaparse: + * @short_description: Parser for audio encoded in QOA format. + * + * Parser for the Quite OK Audio format. Supports file and streaming modes. + * + * ## Example pipeline + * ```bash + * gst-launch-1.0 filesrc location=audio.qoa ! qoaparse ! qoadec ! autoaudiosink + * ``` + * + * Since: plugins-rs-0.11.0-alpha.1 + */ +use gst::glib; +use gst::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct QoaParse(ObjectSubclass) @extends gst_base::BaseParse, gst::Element, gst::Object; +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "qoaparse", + gst::Rank::Primary, + QoaParse::static_type(), + ) +} diff --git a/audio/qoa/src/typefind.rs b/audio/qoa/src/typefind.rs new file mode 100644 index 00000000..45608308 --- /dev/null +++ b/audio/qoa/src/typefind.rs @@ -0,0 +1,22 @@ +use gst::{TypeFind, TypeFindProbability}; +use gst::glib; + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + TypeFind::register( + Some(plugin), + "qoa_typefind", + gst::Rank::None, + Some("qoa"), + Some(&gst::Caps::builder("audio/x-qoa").build()), + |typefind| { + if let Some(data) = typefind.peek(0, qoaudio::QOA_MIN_FILESIZE as u32) { + if qoaudio::QoaDecoder::decode_header(data).is_ok() { + typefind.suggest( + TypeFindProbability::Maximum, + &gst::Caps::builder("audio/x-qoa").build(), + ); + } + } + }, + ) +} diff --git a/meson.build b/meson.build index 162da11d..d3667f28 100644 --- a/meson.build +++ b/meson.build @@ -111,6 +111,7 @@ plugins = { # csound has a non-trivial external dependency, see below 'lewton': {'library': 'libgstlewton'}, 'spotify': {'library': 'libgstspotify'}, + 'qoa': {'library': 'libgstqoa'}, 'file': {'library': 'libgstrsfile'}, # sodium can have an external dependency, see below diff --git a/meson_options.txt b/meson_options.txt index fbe8034d..c11314c2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,6 +6,7 @@ option('claxon', type: 'feature', value: 'auto', description: 'Build claxon plug option('csound', type: 'feature', value: 'auto', description: 'Build csound plugin') option('lewton', type: 'feature', value: 'auto', description: 'Build lewton plugin') option('spotify', type: 'feature', value: 'auto', description: 'Build spotify plugin') +option('qoa', type: 'feature', value: 'auto', description: 'Build QOA plugin') # generic option('file', type: 'feature', value: 'auto', description: 'Build file plugin')