qoa: Add support for Quite OK Audio format

This commit is contained in:
Rafael Caricio 2023-04-06 23:31:31 +02:00
parent 42008fb895
commit d13d488845
Signed by: rafaelcaricio
GPG key ID: 3C86DBCE8E93C947
11 changed files with 701 additions and 0 deletions

View file

@ -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",

46
audio/qoa/Cargo.toml Normal file
View 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
View file

@ -0,0 +1,3 @@
fn main() {
gst_plugin_version_helper::info()
}

32
audio/qoa/src/lib.rs Normal file
View 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
View 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,
],
];

View 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(),
)
}

View 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))
}
}

View 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
View 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(),
);
}
}
},
)
}

View file

@ -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

View file

@ -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')