mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-09-29 15:22:07 +00:00
qoa: Add support for Quite OK Audio format
This commit is contained in:
parent
42008fb895
commit
d13d488845
11 changed files with 701 additions and 0 deletions
|
@ -10,6 +10,7 @@ members = [
|
||||||
"audio/csound",
|
"audio/csound",
|
||||||
"audio/lewton",
|
"audio/lewton",
|
||||||
"audio/spotify",
|
"audio/spotify",
|
||||||
|
"audio/qoa",
|
||||||
|
|
||||||
"generic/file",
|
"generic/file",
|
||||||
"generic/sodium",
|
"generic/sodium",
|
||||||
|
@ -63,6 +64,7 @@ default-members = [
|
||||||
"audio/audiofx",
|
"audio/audiofx",
|
||||||
"audio/claxon",
|
"audio/claxon",
|
||||||
"audio/lewton",
|
"audio/lewton",
|
||||||
|
"audio/qoa",
|
||||||
|
|
||||||
"generic/threadshare",
|
"generic/threadshare",
|
||||||
"generic/inter",
|
"generic/inter",
|
||||||
|
|
46
audio/qoa/Cargo.toml
Normal file
46
audio/qoa/Cargo.toml
Normal file
|
@ -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"
|
3
audio/qoa/build.rs
Normal file
3
audio/qoa/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
gst_plugin_version_helper::info()
|
||||||
|
}
|
32
audio/qoa/src/lib.rs
Normal file
32
audio/qoa/src/lib.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (C) 2023 Rafael Caricio <rafael@caricio.com>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// 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")
|
||||||
|
);
|
334
audio/qoa/src/qoadec/imp.rs
Normal file
334
audio/qoa/src/qoadec/imp.rs
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
// Copyright (C) 2023 Rafael Caricio <rafael@caricio.com>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// 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<QoaDecoder>,
|
||||||
|
audio_info: Option<gst_audio::AudioInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct QoaDec {
|
||||||
|
state: AtomicRefCell<Option<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static CAT: Lazy<gst::DebugCategory> = 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<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"QOA decoder",
|
||||||
|
"Decoder/Audio",
|
||||||
|
"Quite OK Audio decoder",
|
||||||
|
"Rafael Caricio <rafael@caricio.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = 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::FlowSuccess, gst::FlowError> {
|
||||||
|
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<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
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::<Vec<i16>>();
|
||||||
|
|
||||||
|
struct CastVec(Vec<i16>);
|
||||||
|
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<gst_audio::AudioInfo, String> {
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
];
|
37
audio/qoa/src/qoadec/mod.rs
Normal file
37
audio/qoa/src/qoadec/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright (C) 2023 Rafael Caricio <rafael@caricio.com>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// 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<imp::QoaDec>) @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(),
|
||||||
|
)
|
||||||
|
}
|
186
audio/qoa/src/qoaparse/imp.rs
Normal file
186
audio/qoa/src/qoaparse/imp.rs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
// Copyright (C) 2023 Rafael Caricio <rafael@caricio.com>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// 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<gst::DebugCategory> = 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<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"QOA parser",
|
||||||
|
"Codec/Parser/Audio",
|
||||||
|
"Quite OK Audio parser",
|
||||||
|
"Rafael Caricio <rafael@caricio.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = 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))
|
||||||
|
}
|
||||||
|
}
|
37
audio/qoa/src/qoaparse/mod.rs
Normal file
37
audio/qoa/src/qoaparse/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright (C) 2023 Rafael Caricio <rafael@caricio.com>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// 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<imp::QoaParse>) @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(),
|
||||||
|
)
|
||||||
|
}
|
22
audio/qoa/src/typefind.rs
Normal file
22
audio/qoa/src/typefind.rs
Normal file
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -111,6 +111,7 @@ plugins = {
|
||||||
# csound has a non-trivial external dependency, see below
|
# csound has a non-trivial external dependency, see below
|
||||||
'lewton': {'library': 'libgstlewton'},
|
'lewton': {'library': 'libgstlewton'},
|
||||||
'spotify': {'library': 'libgstspotify'},
|
'spotify': {'library': 'libgstspotify'},
|
||||||
|
'qoa': {'library': 'libgstqoa'},
|
||||||
|
|
||||||
'file': {'library': 'libgstrsfile'},
|
'file': {'library': 'libgstrsfile'},
|
||||||
# sodium can have an external dependency, see below
|
# sodium can have an external dependency, see below
|
||||||
|
|
|
@ -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('csound', type: 'feature', value: 'auto', description: 'Build csound plugin')
|
||||||
option('lewton', type: 'feature', value: 'auto', description: 'Build lewton plugin')
|
option('lewton', type: 'feature', value: 'auto', description: 'Build lewton plugin')
|
||||||
option('spotify', type: 'feature', value: 'auto', description: 'Build spotify plugin')
|
option('spotify', type: 'feature', value: 'auto', description: 'Build spotify plugin')
|
||||||
|
option('qoa', type: 'feature', value: 'auto', description: 'Build QOA plugin')
|
||||||
|
|
||||||
# generic
|
# generic
|
||||||
option('file', type: 'feature', value: 'auto', description: 'Build file plugin')
|
option('file', type: 'feature', value: 'auto', description: 'Build file plugin')
|
||||||
|
|
Loading…
Reference in a new issue