mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-05-13 13:52:43 +00:00
Merge branch 'gst-qoa-plugin' into 'main'
qoa: Add support for Quite OK Audio format See merge request gstreamer/gst-plugins-rs!1163
This commit is contained in:
commit
4dea2283fd
|
@ -10,6 +10,7 @@ members = [
|
|||
"audio/csound",
|
||||
"audio/lewton",
|
||||
"audio/spotify",
|
||||
"audio/qoa",
|
||||
|
||||
"generic/file",
|
||||
"generic/originalbuffer",
|
||||
|
@ -66,6 +67,7 @@ default-members = [
|
|||
"audio/audiofx",
|
||||
"audio/claxon",
|
||||
"audio/lewton",
|
||||
"audio/qoa",
|
||||
|
||||
"generic/originalbuffer",
|
||||
"generic/threadshare",
|
||||
|
|
45
audio/qoa/Cargo.toml
Normal file
45
audio/qoa/Cargo.toml
Normal file
|
@ -0,0 +1,45 @@
|
|||
[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) 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.7.0"
|
||||
byte-slice-cast = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
|
||||
gst-app = { package = "gstreamer-app", 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()
|
||||
}
|
33
audio/qoa/src/lib.rs
Normal file
33
audio/qoa/src/lib.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
// 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 qoa;
|
||||
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")
|
||||
);
|
98
audio/qoa/src/qoa.rs
Normal file
98
audio/qoa/src/qoa.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use qoaudio::{QOA_HEADER_SIZE, QOA_LMS_LEN};
|
||||
use std::fmt;
|
||||
|
||||
pub const QOA_MIN_FILESIZE: usize = 16;
|
||||
|
||||
pub const MAX_SLICES_PER_CHANNEL_PER_FRAME: usize = 256;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct InvalidFrameHeader;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct FrameHeader {
|
||||
/// Number of channels in this frame
|
||||
pub channels: u8,
|
||||
/// Sample rate in HZ for this frame
|
||||
pub sample_rate: u32,
|
||||
/// Samples per channel in this frame
|
||||
pub num_samples_per_channel: u16,
|
||||
/// Total size of the frame (includes header size itself)
|
||||
pub frame_size: usize,
|
||||
}
|
||||
|
||||
impl FrameHeader {
|
||||
/// Parse and validate various traits of a valid frame header.
|
||||
pub fn parse(frame_header: u64) -> Result<Self, InvalidFrameHeader> {
|
||||
let channels = ((frame_header >> 56) & 0x0000ff) as u8;
|
||||
let sample_rate = ((frame_header >> 32) & 0xffffff) as u32;
|
||||
let num_samples_per_channel = ((frame_header >> 16) & 0x00ffff) as u16;
|
||||
let frame_size = (frame_header & 0x00ffff) as usize;
|
||||
|
||||
if channels == 0 || sample_rate == 0 {
|
||||
return Err(InvalidFrameHeader);
|
||||
}
|
||||
|
||||
const LMS_SIZE: usize = 4;
|
||||
let non_sample_data_size = QOA_HEADER_SIZE + QOA_LMS_LEN * LMS_SIZE * channels as usize;
|
||||
if frame_size <= non_sample_data_size {
|
||||
return Err(InvalidFrameHeader);
|
||||
}
|
||||
let data_size = frame_size - non_sample_data_size;
|
||||
let num_slices = data_size / 8;
|
||||
|
||||
if num_slices % channels as usize != 0 {
|
||||
return Err(InvalidFrameHeader);
|
||||
}
|
||||
if num_slices / channels as usize > MAX_SLICES_PER_CHANNEL_PER_FRAME {
|
||||
return Err(InvalidFrameHeader);
|
||||
}
|
||||
|
||||
Ok(FrameHeader {
|
||||
channels,
|
||||
sample_rate,
|
||||
num_samples_per_channel,
|
||||
frame_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for FrameHeader {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.channels == other.channels && self.sample_rate == other.sample_rate
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FrameHeader {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{{channels={}, sample_rate={}, num_samples_per_channel={}, frame_size={}}}",
|
||||
self.channels, self.sample_rate, self.num_samples_per_channel, self.frame_size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_can_parse_valid_frame_header() {
|
||||
let frame_header = FrameHeader::parse(0x0200a028000003e8).unwrap();
|
||||
assert_eq!(
|
||||
frame_header,
|
||||
FrameHeader {
|
||||
channels: 2,
|
||||
sample_rate: 41000,
|
||||
num_samples_per_channel: 100,
|
||||
frame_size: 1000
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_frame_header() {
|
||||
assert!(FrameHeader::parse(0x0000000000000000).is_err());
|
||||
assert!(FrameHeader::parse(0x0420420420420420).is_err());
|
||||
}
|
||||
}
|
311
audio/qoa/src/qoadec/imp.rs
Normal file
311
audio/qoa/src/qoadec/imp.rs
Normal file
|
@ -0,0 +1,311 @@
|
|||
// 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 crate::qoa::QOA_MIN_FILESIZE;
|
||||
use byte_slice_cast::*;
|
||||
use gst::glib;
|
||||
use gst::glib::once_cell::sync::Lazy;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_audio::prelude::*;
|
||||
use gst_audio::subclass::prelude::*;
|
||||
use qoaudio::{QoaDecoder, QOA_HEADER_SIZE, QOA_MAGIC};
|
||||
use std::io::Cursor;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
struct State {
|
||||
decoder: QoaDecoder<Cursor<Vec<u8>>>,
|
||||
audio_info: Option<gst_audio::AudioInfo>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
State {
|
||||
decoder: QoaDecoder::new_streaming().expect("Decoder creation failed"),
|
||||
audio_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct QoaDec {
|
||||
state: Arc<Mutex<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 {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
self.obj().set_drainable(false);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.field("rate", gst::IntRange::<i32>::new(1, 16777215))
|
||||
.field("channels", gst::IntRange::<i32>::new(1, 255))
|
||||
.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...");
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
*state = Some(State::default());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
gst::debug!(CAT, imp: self, "Stopping...");
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
*state = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_format(&self, caps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||
gst::debug!(CAT, imp: self, "Setting format {:?}", caps);
|
||||
|
||||
let s = caps.structure(0).unwrap();
|
||||
let channels = s.get::<i32>("channels").unwrap();
|
||||
let rate = s.get::<i32>("rate").unwrap();
|
||||
|
||||
if let Ok(audio_info) = get_audio_info(rate as u32, channels as u32) {
|
||||
if self.obj().set_output_format(&audio_info).is_err() || self.obj().negotiate().is_err()
|
||||
{
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Error to negotiate output from based on in-caps parameters"
|
||||
);
|
||||
}
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
let state = state_guard.as_mut().unwrap();
|
||||
state.audio_info = Some(audio_info);
|
||||
} else {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to get audio info"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_frame(
|
||||
&self,
|
||||
inbuf: Option<&gst::Buffer>,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
gst::debug!(CAT, imp: self, "Handling buffer {:?}", inbuf);
|
||||
|
||||
let inbuf = inbuf.expect("Non-drainable should never receive empty buffer");
|
||||
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.lock().unwrap();
|
||||
let state = state_guard.as_mut().ok_or_else(|| {
|
||||
gst::error!(CAT, imp: self, "Failed to get state");
|
||||
gst::FlowError::NotNegotiated
|
||||
})?;
|
||||
|
||||
// Skip file header, if present
|
||||
let file_header_size = {
|
||||
if inmap.len() >= QOA_MIN_FILESIZE {
|
||||
let magic = u32::from_be_bytes(inmap[0..4].try_into().unwrap());
|
||||
if magic == QOA_MAGIC {
|
||||
QOA_HEADER_SIZE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let samples = state
|
||||
.decoder
|
||||
.decode_frame(&inmap[file_header_size..])
|
||||
.map_err(|err| {
|
||||
gst::element_error!(
|
||||
self.obj(),
|
||||
gst::CoreError::Negotiation,
|
||||
["Failed to decode frames: {}", err]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
gst::trace!(CAT, imp: self, "Successfully decoded {} audio samples from frame", samples.len());
|
||||
|
||||
let outbuf = gst::Buffer::from_slice(samples.as_byte_slice().to_vec());
|
||||
self.obj().finish_frame(Some(outbuf), 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_audio_info(sample_rate: u32, channels: u32) -> Result<gst_audio::AudioInfo, String> {
|
||||
let index = match 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, sample_rate, 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 ! 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(),
|
||||
)
|
||||
}
|
213
audio/qoa/src/qoaparse/imp.rs
Normal file
213
audio/qoa/src/qoaparse/imp.rs
Normal file
|
@ -0,0 +1,213 @@
|
|||
// 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 crate::qoa::{FrameHeader, QOA_MIN_FILESIZE};
|
||||
use glib::once_cell::sync::Lazy;
|
||||
use gst::glib;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_base::prelude::*;
|
||||
use gst_base::subclass::prelude::*;
|
||||
use qoaudio::{QOA_HEADER_SIZE, QOA_MAGIC};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
struct State {
|
||||
last_header: Option<FrameHeader>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct QoaParse {
|
||||
state: Arc<Mutex<State>>,
|
||||
}
|
||||
|
||||
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)
|
||||
.field("rate", gst::IntRange::<i32>::new(1, 16777215))
|
||||
.field("channels", gst::IntRange::<i32>::new(1, 255))
|
||||
.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...");
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
// We need at least a full frame header size to proceed
|
||||
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);
|
||||
}
|
||||
|
||||
// We want to skip the file header (if content is coming
|
||||
// from QOA file) so we ignore the first bytes. We want to
|
||||
// read the frame header.
|
||||
let raw_header = u64::from_be_bytes(
|
||||
data[file_header_size..(file_header_size + QOA_HEADER_SIZE)]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// We don't know where in the byte stream we are reading from. We need to figure out where
|
||||
// the frame header starts and sync to it. We try to parse and validate some traits of
|
||||
// a valid QOA frame header.
|
||||
let frame_header = match FrameHeader::parse(raw_header) {
|
||||
Ok(header) => header,
|
||||
Err(_) => {
|
||||
// maybe the next byte will sync with a valid frame header
|
||||
return Ok((gst::FlowSuccess::Ok, 1));
|
||||
}
|
||||
};
|
||||
|
||||
if data.len() < (file_header_size + frame_header.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_header.frame_size,
|
||||
data.len()
|
||||
);
|
||||
return Ok((gst::FlowSuccess::Ok, 0));
|
||||
}
|
||||
|
||||
drop(map);
|
||||
|
||||
let duration = (frame_header.num_samples_per_channel as u64)
|
||||
.mul_div_floor(*gst::ClockTime::SECOND, frame_header.sample_rate as u64)
|
||||
.map(gst::ClockTime::from_nseconds);
|
||||
|
||||
let buffer = frame.buffer_mut().unwrap();
|
||||
buffer.set_duration(duration);
|
||||
// all buffers are valid QOA frames which contain header
|
||||
buffer.set_flags(gst::BufferFlags::HEADER);
|
||||
|
||||
gst::trace!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Found frame {frame_header} with duration of {duration:?}",
|
||||
);
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if self.obj().src_pad().current_caps().is_none() || state.last_header != Some(frame_header)
|
||||
{
|
||||
// Set src pad caps
|
||||
let src_caps = gst::Caps::builder("audio/x-qoa")
|
||||
.field("parsed", true)
|
||||
.field("rate", frame_header.sample_rate as i32)
|
||||
.field("channels", frame_header.channels as i32)
|
||||
.build();
|
||||
|
||||
gst::debug!(CAT, imp: self, "Setting src pad caps {:?}", src_caps);
|
||||
|
||||
self.obj()
|
||||
.src_pad()
|
||||
.push_event(gst::event::Caps::new(&src_caps));
|
||||
|
||||
state.last_header = Some(frame_header);
|
||||
}
|
||||
|
||||
self.obj()
|
||||
.finish_frame(frame, (file_header_size + frame_header.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(),
|
||||
)
|
||||
}
|
34
audio/qoa/src/typefind.rs
Normal file
34
audio/qoa/src/typefind.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use crate::qoa::{FrameHeader, QOA_MIN_FILESIZE};
|
||||
use gst::glib;
|
||||
use gst::{TypeFind, TypeFindProbability};
|
||||
use qoaudio::QOA_HEADER_SIZE;
|
||||
|
||||
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, QOA_MIN_FILESIZE as u32) {
|
||||
let magic = u32::from_be_bytes(data[0..4].try_into().unwrap());
|
||||
if magic == qoaudio::QOA_MAGIC {
|
||||
typefind.suggest(
|
||||
TypeFindProbability::Maximum,
|
||||
&gst::Caps::builder("audio/x-qoa").build(),
|
||||
);
|
||||
} else {
|
||||
let raw_header =
|
||||
u64::from_be_bytes(data[0..QOA_HEADER_SIZE].try_into().unwrap());
|
||||
if let Ok(_header) = FrameHeader::parse(raw_header) {
|
||||
typefind.suggest(
|
||||
TypeFindProbability::Likely,
|
||||
&gst::Caps::builder("audio/x-qoa").build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
66
audio/qoa/tests/typefind.rs
Normal file
66
audio/qoa/tests/typefind.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use gst::prelude::*;
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstqoa::plugin_register_static().expect("Failed to register csound plugin");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_the_type_of_qoa_data() {
|
||||
init();
|
||||
|
||||
let src = gst_app::AppSrc::builder().is_live(true).build();
|
||||
|
||||
let typefind = gst::ElementFactory::make("typefind").build().unwrap();
|
||||
let fakesink = gst::ElementFactory::make("fakesink").build().unwrap();
|
||||
|
||||
let pipeline = gst::Pipeline::new();
|
||||
pipeline
|
||||
.add_many(&[src.upcast_ref(), &typefind, &fakesink])
|
||||
.unwrap();
|
||||
gst::Element::link_many(&[src.upcast_ref(), &typefind, &fakesink]).unwrap();
|
||||
|
||||
pipeline.set_state(gst::State::Playing).unwrap();
|
||||
|
||||
// Create some fake QOA data
|
||||
let mut qoa_data = Vec::new();
|
||||
qoa_data.extend(qoaudio::QOA_MAGIC.to_be_bytes());
|
||||
qoa_data.extend(&[0u8; 20]);
|
||||
src.push_buffer(gst::Buffer::from_slice(qoa_data)).unwrap();
|
||||
|
||||
src.end_of_stream().unwrap();
|
||||
|
||||
let bus = pipeline.bus().unwrap();
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
use gst::MessageView;
|
||||
match msg.view() {
|
||||
MessageView::Error(err) => {
|
||||
eprintln!(
|
||||
"Error received from element {:?}: {}",
|
||||
err.src().map(|s| s.path_string()),
|
||||
err.error()
|
||||
);
|
||||
eprintln!("Debugging information: {:?}", err.debug());
|
||||
pipeline.set_state(gst::State::Null).unwrap();
|
||||
unreachable!();
|
||||
}
|
||||
MessageView::Eos(..) => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let pad = typefind.static_pad("src").unwrap();
|
||||
let caps = pad.current_caps().unwrap();
|
||||
assert_eq!(caps.to_string(), "audio/x-qoa");
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn when_data_is_not_qoa() {
|
||||
// init();
|
||||
//
|
||||
// }
|
|
@ -116,6 +116,7 @@ plugins = {
|
|||
# csound has a non-trivial external dependency, see below
|
||||
'lewton': {'library': 'libgstlewton'},
|
||||
'spotify': {'library': 'libgstspotify'},
|
||||
'qoa': {'library': 'libgstqoa'},
|
||||
|
||||
'file': {'library': 'libgstrsfile'},
|
||||
'originalbuffer': {'library': 'libgstoriginalbuffer'},
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue