diff --git a/Cargo.toml b/Cargo.toml index ea472bd4ede52ea38dedb6425445769e034be0f6..1e081c08b902f4b5d3f184262eea7a2d906c1fef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "audio/csound", "audio/lewton", "audio/spotify", + "audio/symphonia", "generic/fmp4", "generic/file", "generic/sodium", @@ -43,6 +44,7 @@ default-members = [ "audio/audiofx", "audio/claxon", "audio/lewton", + "audio/symphonia", "generic/file", "generic/threadshare", "net/reqwest", diff --git a/audio/symphonia/Cargo.toml b/audio/symphonia/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..252bfb55f3145413b32fa4d10b5b9ac950c13a22 --- /dev/null +++ b/audio/symphonia/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "gst-plugin-symphonia" +version = "0.9.0" +authors = ["François Laignel "] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +license = "MPL-2.0" +description = "Symphonia audio parsers and decoders Plugin" +edition = "2021" +rust-version = "1.57" + +[dependencies] +atomic_refcell = "0.1" +bytemuck = "1.7.3" +gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +once_cell = "1.0" +symphonia = { git = "https://github.com/fengalin/Symphonia", branch = "borrowed-raw-buffer", default-features = false } +symphonia-utils-xiph = { git = "https://github.com/fengalin/Symphonia", branch = "borrowed-raw-buffer" } +thiserror = "1.0.30" + +[dev-dependencies] +gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } + +[lib] +name = "gstsymphonia" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../version-helper" } + +[features] +default = ["flac", "mp3"] + +allow-planar = ["audio-meta"] +audio-meta = ["gst-audio/v1_16"] + +flac = ["symphonia/flac"] +mp3 = ["symphonia/mp3"] + +# GStreamer 1.14 is required for static linking +static = ["gst/v1_14"] +capi = [] + +[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-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0" diff --git a/audio/symphonia/LICENSE-MPL-2.0 b/audio/symphonia/LICENSE-MPL-2.0 new file mode 120000 index 0000000000000000000000000000000000000000..eb5d24fe91cf96433d7a8685343bd62765b53fbb --- /dev/null +++ b/audio/symphonia/LICENSE-MPL-2.0 @@ -0,0 +1 @@ +../../LICENSE-MPL-2.0 \ No newline at end of file diff --git a/audio/symphonia/build.rs b/audio/symphonia/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..cda12e57e199933e6ce75a6bd48e2ef443392eb2 --- /dev/null +++ b/audio/symphonia/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/audio/symphonia/src/flac/decoder/imp.rs b/audio/symphonia/src/flac/decoder/imp.rs new file mode 100644 index 0000000000000000000000000000000000000000..2b8ba25455dc160b355f62531ed370521d7e0655 --- /dev/null +++ b/audio/symphonia/src/flac/decoder/imp.rs @@ -0,0 +1,555 @@ +// Copyright (C) 2022 François Laignel +// +// 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_audio::audio_decoder_error; +use gst_audio::prelude::*; +use gst_audio::subclass::prelude::*; + +use atomic_refcell::AtomicRefCell; +use once_cell::sync::Lazy; + +use std::boxed::Box; + +use symphonia::core::codecs::{CodecParameters, Decoder}; +use symphonia::core::io::{BufReader, ReadBytes}; + +use crate::decoder; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "symphoniaflacdec", + gst::DebugColorFlags::empty(), + Some("Symphonia FLAC decoder"), + ) +}); + +struct Context { + decoder: Box, + out_audio_info: Option, +} + +impl Context { + fn new(codec_params: CodecParameters, out_audio_info: gst_audio::AudioInfo) -> Self { + use symphonia::core::codecs::DecoderOptions; + + let decoder = match symphonia::default::get_codecs() + .make(&codec_params, &DecoderOptions { verify: false }) + { + Ok(decoder) => decoder, + Err(err) => panic!("Failed to build decoder: {}", err), + }; + + Context { + decoder, + out_audio_info: Some(out_audio_info), + } + } + + fn reset(&mut self) { + self.decoder.reset(); + self.out_audio_info = None; + } +} + +#[derive(Default)] +pub struct SymphoniaFlacDec { + context: AtomicRefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for SymphoniaFlacDec { + const NAME: &'static str = "SymphoniaFlacDec"; + type Type = super::SymphoniaFlacDec; + type ParentType = gst_audio::AudioDecoder; +} + +impl ObjectImpl for SymphoniaFlacDec {} + +impl GstObjectImpl for SymphoniaFlacDec {} + +impl ElementImpl for SymphoniaFlacDec { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "Symphonia FLAC decoder", + "Codec/Decoder/Audio", + "Symphonia FLAC (Free Lossless Audio Codec) decoder", + "François Laignel ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + let sink_caps = gst::Caps::builder("audio/x-flac") + .field("framed", true) + .field("rate", gst::IntRange::new(1i32, 655_350)) + .field("channels", gst::IntRange::new(1i32, 8)) + .build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + + vec![sink_pad_template, decoder::src_caps_template()] + }); + + PAD_TEMPLATES.as_ref() + } +} + +impl AudioDecoderImpl for SymphoniaFlacDec { + fn stop(&self, dec: &Self::Type) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, obj: dec, "Stopping"); + *self.context.borrow_mut() = None; + Ok(()) + } + + fn start(&self, dec: &Self::Type) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, obj: dec, "Starting"); + Ok(()) + } + + fn flush(&self, dec: &Self::Type, _hard: bool) { + gst::debug!(CAT, obj: dec, "Flushing"); + + let mut context_guard = self.context.borrow_mut(); + if let Some(ref mut context) = *context_guard { + context.reset(); + } + } + + fn set_format(&self, dec: &Self::Type, caps: &gst::Caps) -> Result<(), gst::LoggableError> { + gst::debug!(CAT, obj: dec, "Attempting to set format"); + gst::log!(CAT, obj: dec, "sink {:?}", caps); + + let s = caps.structure(0).unwrap(); + if let Ok(Some(streamheaders)) = s.get_optional::("streamheader") { + if streamheaders.len() < 2 { + gst::debug!(CAT, obj: dec, "Not enough streamheaders, trying in-band"); + return Ok(()); + } + + // Stream info block is expected to be the first + if let Ok(Some(header_buf)) = streamheaders[0].get::>() { + let headermap = header_buf.map_readable().unwrap(); + let mut reader = BufReader::new(headermap.as_slice()); + + let mut marker = [0u8; 5]; + if reader.read_buf_exact(&mut marker).is_err() { + gst::debug!(CAT, obj: dec, "Invalid streamheader len, will try in-band"); + return Ok(()); + } + + if marker != *b"\x7FFLAC" { + gst::debug!( + CAT, + obj: dec, + "Invalid streamheader format, will try in-band" + ); + return Ok(()); + } + + // Skip: version (2) + num headers (2) + 'fLaC' (4) + if reader.ignore_bytes(2 + 2 + 4).is_err() { + gst::debug!( + CAT, + obj: dec, + "Invalid streamheader version block, will try in-band" + ); + return Ok(()); + } + + let codec_params = match Self::try_parse_stream_info(&mut reader) { + Ok(Some(codec_params)) => codec_params, + Ok(None) => { + gst::warning!( + CAT, + obj: dec, + "Couldn't find Stream Info in first header, will try in-band", + ); + return Ok(()); + } + Err(err) => { + gst::warning!( + CAT, + obj: dec, + "Couldn't get stream info from headers, will try in-band", + ); + gst::debug!(CAT, obj: dec, "due to: {}", err); + return Ok(()); + } + }; + + let mut context = self.context.borrow_mut(); + self.negotiate_src_format(dec, &mut context, codec_params) + .map_err(|err| { + gst::loggable_error!( + CAT, + "Failed to negotiate src caps from the stream info: {}", + err + ) + })?; + } + } + + Ok(()) + } + + fn handle_frame( + &self, + dec: &Self::Type, + inbuf: Option<&gst::Buffer>, + ) -> Result { + use symphonia::core::formats::Packet; + + gst::log!(CAT, obj: dec, "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, obj: dec, "Failed to map buffer readable"); + gst::FlowError::Error + })?; + + // Ignore empty packets + if inmap.len() == 0 { + return dec.finish_frame(None, 1); + } + + let mut context_guard = self.context.borrow_mut(); + + if context_guard.is_none() { + // Not ready yet, expecting the Stream Info frame + let mut reader = BufReader::new(inmap.as_slice()); + let codec_params = Self::try_parse_stream_info(&mut reader).map_err(|err| { + gst::error!(CAT, obj: dec, "Failed to parse Stream Info: {}", err); + gst::FlowError::NotNegotiated + })?; + let codec_params = match codec_params { + Some(codec_params) => codec_params, + None => { + gst::debug!(CAT, obj: dec, "Skipping x{:02x?}", inmap[0]); + return dec.finish_frame(None, 1); + } + }; + + self.negotiate_src_format(dec, &mut context_guard, codec_params) + .map_err(|err| { + gst::error!(CAT, obj: dec, "Failed to negotiate src pad caps: {}", err); + gst::FlowError::NotNegotiated + })?; + } + + let context = context_guard + .as_mut() + .ok_or(gst::FlowError::NotNegotiated)?; + + if inmap[0] != 0b1111_1111 || inmap[1] & 0b1111_1100 != 0b1111_1000 { + // info about other headers in flacparse and https://xiph.org/flac/format.html + gst::debug!(CAT, obj: dec, "Skipping header buffer x{:02x?}", inmap[0]); + return dec.finish_frame(None, 1); + } + + gst::log!(CAT, obj: dec, "Data buffer received"); + + // FIXME handle gapless parameters + let packet = Packet::new_from_slice(0, 0, 0, inmap.as_ref()); + + use symphonia::core::errors::Error::*; + let decoded = match context.decoder.decode(&packet) { + Ok(decoded) => decoded, + Err(DecodeError(err)) => { + gst::warning!(CAT, obj: dec, "an error occured decoding a packet: {}", err); + return dec.finish_frame(None, 1); + } + Err(ResetRequired) => { + gst::info!(CAT, obj: dec, "stream params changed"); + context.reset(); + // FIXME is this the right way to handle this? + // or is it good enough to call gst_pad_mark_reconfigure? + dec.static_pad("src") + .unwrap() + .push_event(gst::event::Reconfigure::new()); + return dec.finish_frame(None, 1); + } + Err(err) => { + return audio_decoder_error!( + dec, + 1, + gst::StreamError::Decode, + ["an unrecoverable error occured decoding a packet: {}", err] + ); + } + }; + + let out_audio_info = context.out_audio_info.as_ref().unwrap(); + // FIXME handle channels mapping + let outbuf = decoder::to_outbuf(dec, decoded, out_audio_info)?; + + dec.finish_frame(Some(outbuf), 1) + } +} + +#[derive(Debug, thiserror::Error)] +enum ParseStreamInfoError { + #[error("Wrong Stream Info size: {}", 0)] + WrongSize(u64), + #[error("Invalid Stream Info")] + Invalid, + #[error("Header is not Stream Info and was the last one")] + NoMoreHeaders, +} + +impl SymphoniaFlacDec { + fn try_parse_stream_info( + reader: &mut BufReader, + ) -> Result, ParseStreamInfoError> { + use symphonia::core::codecs::{self, VerificationCheck}; + use symphonia::core::io::{FiniteStream, ScopedStream}; + use symphonia::core::units::TimeBase; + + use symphonia_utils_xiph::flac::metadata::{ + MetadataBlockHeader, MetadataBlockType, StreamInfo, + }; + + use ParseStreamInfoError::*; + + let header = match MetadataBlockHeader::read(reader) { + Ok(header) => header, + Err(_) => { + // Most likely not a header + return Ok(None); + } + }; + + let mut block_stream = ScopedStream::new(reader, header.block_len.into()); + + if let MetadataBlockType::StreamInfo = header.block_type { + if !StreamInfo::is_valid_size(block_stream.byte_len()) { + return Err(WrongSize(block_stream.byte_len())); + } + + let extra_data = block_stream + .read_boxed_slice_exact(block_stream.byte_len() as usize) + .unwrap(); + + let info = StreamInfo::read(&mut BufReader::new(&extra_data)).map_err(|_| Invalid)?; + + let mut codec_params = CodecParameters::new(); + codec_params + .for_codec(codecs::CODEC_TYPE_FLAC) + .with_packet_data_integrity(true) + .with_extra_data(extra_data) + .with_sample_rate(info.sample_rate) + .with_time_base(TimeBase::new(1, info.sample_rate)) + .with_bits_per_sample(info.bits_per_sample) + .with_channels(info.channels) + .with_verification_code(VerificationCheck::Md5(info.md5)); + + // Total samples per channel (the total number of frames) is optional. + if let Some(n_frames) = info.n_samples { + codec_params.with_n_frames(n_frames); + } + + Ok(Some(codec_params)) + } else { + // Not the Stream Info header + if header.is_last { + // ... and there won't be any other chance to get one + return Err(NoMoreHeaders); + } + + Ok(None) + } + } + + fn negotiate_src_format( + &self, + dec: &super::SymphoniaFlacDec, + context: &mut Option, + codec_params: CodecParameters, + ) -> Result { + gst::debug!( + CAT, + obj: dec, + "Negotiating src format for {:?}", + codec_params + ); + + // Negotiate output audio format and layout + let src_pad = dec.static_pad("src").unwrap(); + let filter = caps_filter_from_params(&codec_params).map_err(|err| { + gst::error!(CAT, obj: dec, "Couldn't build caps filter: {}", err); + gst::FlowError::NotNegotiated + })?; + + gst::debug!(CAT, obj: dec, "Proposing {:?}", filter); + let mut caps = src_pad.peer_query_caps(Some(&filter)); + gst::debug!(CAT, obj: dec, "Peer refined caps to {:?}", caps); + + caps.fixate(); + if caps.is_empty() { + gst::error!(CAT, obj: dec, "Failed to negotiate src pad caps {:?}", caps); + return Err(gst::FlowError::NotNegotiated); + } + + let audio_info = gst_audio::AudioInfo::from_caps(&caps) + .ok() + .and_then(|audio_info| dec.set_output_format(&audio_info).ok().map(|_| audio_info)) + .ok_or_else(|| { + gst::error!(CAT, obj: dec, "Failed to set output format"); + gst::FlowError::NotSupported + })?; + + gst::info!(CAT, obj: dec, "Using {:?}", caps); + *context = Some(Context::new(codec_params, audio_info)); + + Ok(gst::FlowSuccess::Ok) + } +} + +#[derive(Debug, thiserror::Error)] +enum CapsFilterError { + #[error("invalid sample rate")] + InvalidSampleRate, + #[error("more than 8 channels, not supported yet")] + MoreThan8Channels, + #[error("no channels")] + NoChannels, + #[error("unsupported bits per sample format: {}", 0)] + UnsupportedBitsPerSample(u32), +} + +/// Builds caps suitable to initiate decoder downstream negotiation. +fn caps_filter_from_params(params: &CodecParameters) -> Result { + use CapsFilterError::*; + + let in_format = match params.bits_per_sample.unwrap() { + 8 => gst_audio::AUDIO_FORMAT_S8, + 16 => gst_audio::AUDIO_FORMAT_S16, + 24 => gst_audio::AUDIO_FORMAT_S24, + 32 => gst_audio::AUDIO_FORMAT_F32, + other => return Err(UnsupportedBitsPerSample(other)), + }; + + let sample_rate = params + .sample_rate + .filter(|&rate| rate > 0) + .ok_or(InvalidSampleRate)?; + + let channels = match params.channels.map_or(0, |chans| chans.count()) { + 0 => return Err(NoChannels), + n if n > 8 => return Err(MoreThan8Channels), + n => n, + }; + let positions = &FLAC_CHANNEL_POSITIONS[channels - 1][..channels]; + + Ok(decoder::build_caps_filter( + gst_audio::AudioInfo::builder(in_format, sample_rate, channels as u32) + .positions(positions) + .build() + .unwrap(), + )) +} + +// http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-800004.3.9 +// http://flac.sourceforge.net/format.html#frame_header +const FLAC_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::FrontCenter, + 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::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::FrontCenter, + 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::FrontLeft, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::RearLeft, + gst_audio::AudioChannelPosition::RearRight, + gst_audio::AudioChannelPosition::Lfe1, + gst_audio::AudioChannelPosition::Invalid, + gst_audio::AudioChannelPosition::Invalid, + ], + // FIXME: 7/8 channel layouts are not defined in the FLAC specs + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::SideLeft, + gst_audio::AudioChannelPosition::SideRight, + gst_audio::AudioChannelPosition::RearCenter, + gst_audio::AudioChannelPosition::Lfe1, + gst_audio::AudioChannelPosition::Invalid, + ], + [ + gst_audio::AudioChannelPosition::FrontLeft, + gst_audio::AudioChannelPosition::FrontCenter, + gst_audio::AudioChannelPosition::FrontRight, + gst_audio::AudioChannelPosition::SideLeft, + gst_audio::AudioChannelPosition::SideRight, + gst_audio::AudioChannelPosition::RearLeft, + gst_audio::AudioChannelPosition::RearRight, + gst_audio::AudioChannelPosition::Lfe1, + ], +]; diff --git a/audio/symphonia/src/flac/decoder/mod.rs b/audio/symphonia/src/flac/decoder/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..17359e2977bc7da6133e858e8142659e12d9bece --- /dev/null +++ b/audio/symphonia/src/flac/decoder/mod.rs @@ -0,0 +1,28 @@ +// Copyright (C) 2022 François Laignel +// +// 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::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct SymphoniaFlacDec(ObjectSubclass) @extends gst_audio::AudioDecoder, gst::Element, gst::Object; +} + +unsafe impl Send for SymphoniaFlacDec {} +unsafe impl Sync for SymphoniaFlacDec {} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "symphoniaflacdec", + gst::Rank::Marginal, + SymphoniaFlacDec::static_type(), + ) +} diff --git a/audio/symphonia/src/flac/mod.rs b/audio/symphonia/src/flac/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a69607a457bb0b33464b5d05cbc21c28f35ac31 --- /dev/null +++ b/audio/symphonia/src/flac/mod.rs @@ -0,0 +1,11 @@ +// Copyright (C) 2022 François Laignel +// +// 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 + +//! Symphonia flac parser and decoder elements. + +pub mod decoder; diff --git a/audio/symphonia/src/generic/decoder/mod.rs b/audio/symphonia/src/generic/decoder/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ab04aa71d23fe96a6a909dddc27848dd6a6955f4 --- /dev/null +++ b/audio/symphonia/src/generic/decoder/mod.rs @@ -0,0 +1,176 @@ +// Copyright (C) 2022 François Laignel +// +// 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 + +//! Symphonia audio decoders utilities. + +use gst::glib::IsA; +use symphonia::core::audio::AudioBufferRef; + +/// Supported audio formats. +pub const AUDIO_FORMAT_LIST: [gst_audio::AudioFormat; 10] = [ + gst_audio::AUDIO_FORMAT_F32, + gst_audio::AUDIO_FORMAT_F64, + gst_audio::AUDIO_FORMAT_S32, + gst_audio::AUDIO_FORMAT_U32, + gst_audio::AUDIO_FORMAT_S24, + gst_audio::AUDIO_FORMAT_U24, + gst_audio::AUDIO_FORMAT_S16, + gst_audio::AUDIO_FORMAT_U16, + gst_audio::AUDIO_FORMAT_S8, + gst_audio::AUDIO_FORMAT_U8, +]; + +/// Returns Symphonia compliant audio decoder src pad template. +pub fn src_caps_template() -> gst::PadTemplate { + let src_caps = gst::Caps::builder("audio/x-raw") + .field( + "format", + gst::List::new(crate::decoder::AUDIO_FORMAT_LIST.iter().map(|f| f.to_str())), + ) + .field( + "layout", + gst::List::new(&["interleaved", "non-interleaved"]), + ) + .build(); + + gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap() +} + +pub fn build_caps_filter(audio_info: gst_audio::AudioInfo) -> gst::Caps { + use gst_audio::AudioChannelPosition; + + let mut caps = gst::Caps::builder("audio/x-raw"); + + let in_format = audio_info.format(); + let list_without_in_format = AUDIO_FORMAT_LIST.iter().filter(move |&&f| f != in_format); + let out_formats = std::iter::once(&in_format).chain(list_without_in_format); + caps = caps.field("format", gst::List::new(out_formats.map(|f| f.to_str()))); + + #[cfg(feature = "allow-planar")] + let layout = gst::List::new(&["interleaved", "non-interleaved"]); + #[cfg(not(feature = "allow-planar"))] + let layout = "interleaved"; + caps = caps.field("layout", layout); + + caps = caps.field("rate", &(audio_info.rate() as i32)); + + let channels = audio_info.channels(); + caps = caps.field("channels", channels as i32); + + if channels > 1 { + let channel_mask = audio_info + .positions() + .map(|positions| AudioChannelPosition::positions_to_mask(positions, true).unwrap()) + .unwrap_or_else(|| AudioChannelPosition::fallback_mask(channels)); + + caps = caps.field("channel-mask", gst::Bitmask::new(channel_mask)); + } + + caps.build() +} + +/// Exports decoded data to a `gst::Buffer` complying with `out_audio_info`. +// FIXME handle channels mapping +pub fn to_outbuf( + dec: &D, + decoded: AudioBufferRef, + out_audio_info: &gst_audio::AudioInfo, +) -> Result +where + D: IsA + IsA, +{ + use symphonia::core::audio::{RawSample, RawSampleBuffer}; + use symphonia::core::conv::ConvertibleSample; + use symphonia::core::sample::{i24, u24}; + + fn inner( + dec: &D, + decoded: AudioBufferRef, + out_audio_info: &gst_audio::AudioInfo, + ) -> Result + where + S: RawSample + ConvertibleSample, + D: IsA + IsA, + { + use gst_audio::prelude::*; + + let n_frames = decoded.frames() as u64; + let spec = *decoded.spec(); + let capacity = RawSampleBuffer::::required_byte_capacity(n_frames, spec); + let mut outbuf = dec.allocate_output_buffer(capacity).map_err(|_| { + gst::element_error!( + dec, + gst::StreamError::Decode, + ["Failed to allocate output buffer"] + ); + gst::FlowError::Error + })?; + + { + let outbuf = outbuf.get_mut().unwrap(); + let mut outmap = outbuf.map_writable().map_err(|_| { + gst::element_error!( + dec, + gst::StreamError::Decode, + ["Failed to map output buffer writable"] + ); + gst::FlowError::Error + })?; + + let raw_sample_slice = + bytemuck::try_cast_slice_mut(outmap.as_mut_slice()).map_err(|err| { + gst::element_error!( + dec, + gst::StreamError::Decode, + ["Failed to cast buffer to a raw sample slice: {}", err] + ); + gst::FlowError::Error + })?; + + // FIXME handle channel position mapping + let mut rawbuf = RawSampleBuffer::::from(raw_sample_slice, n_frames, spec); + if out_audio_info.layout() == gst_audio::AudioLayout::Interleaved { + rawbuf.copy_interleaved_ref(decoded); + } else { + #[cfg(feature = "allow-planar")] + rawbuf.copy_planar_ref(decoded); + + #[cfg(not(feature = "allow-planar"))] + panic!("Negotiated non-interleaved layout but the feature `allow-planar` is not activated"); + } + + drop(rawbuf); + drop(outmap); + + #[cfg(feature = "audio-meta")] + gst_audio::AudioMeta::add(outbuf, out_audio_info, n_frames as usize, &[]).unwrap(); + } + + Ok(outbuf) + } + + match out_audio_info.format() { + gst_audio::AUDIO_FORMAT_U8 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_U16 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_U24 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_U32 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_S8 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_S16 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_S24 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_S32 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_F32 => inner::(dec, decoded, out_audio_info), + gst_audio::AUDIO_FORMAT_F64 => inner::(dec, decoded, out_audio_info), + other => panic!("Not a Symphonia compatible audio format: {:?}", other), + } +} diff --git a/audio/symphonia/src/generic/mod.rs b/audio/symphonia/src/generic/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..29ac41f75623d3ced63d45d334e826456fb06756 --- /dev/null +++ b/audio/symphonia/src/generic/mod.rs @@ -0,0 +1,11 @@ +// Copyright (C) 2022 François Laignel +// +// 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 + +//! Symphonia generic audio parsers and decoders. + +pub mod decoder; diff --git a/audio/symphonia/src/lib.rs b/audio/symphonia/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..1cdbb003e61a6d8f37b31bd6de466a9f09abf451 --- /dev/null +++ b/audio/symphonia/src/lib.rs @@ -0,0 +1,41 @@ +// Copyright (C) 2022 François Laignel +// +// 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 + +// FIXME remove when MSRV >= 1.58 +#![allow(clippy::non_send_fields_in_send_ty)] + +use gst::glib; + +mod generic; +pub(crate) use generic::decoder; + +#[cfg(feature = "flac")] +mod flac; + +#[cfg(feature = "mp3")] +mod mp3; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + #[cfg(feature = "flac")] + flac::decoder::register(plugin)?; + #[cfg(feature = "mp3")] + mp3::decoder::register(plugin)?; + Ok(()) +} + +gst::plugin_define!( + symphonia, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MIT/X11", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/audio/symphonia/src/mp3/decoder/imp.rs b/audio/symphonia/src/mp3/decoder/imp.rs new file mode 100644 index 0000000000000000000000000000000000000000..3b57ceeb24aadceb35a72efba79d897b0e35026c --- /dev/null +++ b/audio/symphonia/src/mp3/decoder/imp.rs @@ -0,0 +1,277 @@ +// Copyright (C) 2022 François Laignel +// +// 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_audio::audio_decoder_error; +use gst_audio::prelude::*; +use gst_audio::subclass::prelude::*; + +use atomic_refcell::AtomicRefCell; +use once_cell::sync::Lazy; + +use symphonia::core::audio::AudioBufferRef; +use symphonia::core::codecs::Decoder; + +use crate::decoder; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "symphoniamp3dec", + gst::DebugColorFlags::empty(), + Some("Symphonia MP3 decoder"), + ) +}); + +struct Context { + decoder: Box, + out_audio_info: Option, +} + +impl Default for Context { + fn default() -> Self { + use symphonia::core::codecs::{self, CodecParameters, DecoderOptions}; + + let decoder = match symphonia::default::get_codecs().make( + CodecParameters::new().for_codec(codecs::CODEC_TYPE_MP3), + &DecoderOptions { verify: false }, + ) { + Ok(decoder) => decoder, + Err(err) => panic!("Failed to build decoder: {}", err), + }; + + Context { + decoder, + out_audio_info: None, + } + } +} + +impl Context { + fn reset(&mut self) { + self.decoder.reset(); + self.out_audio_info = None; + } +} + +#[derive(Default)] +pub struct SymphoniaMp3Dec { + context: AtomicRefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for SymphoniaMp3Dec { + const NAME: &'static str = "SymphoniaMp3Dec"; + type Type = super::SymphoniaMp3Dec; + type ParentType = gst_audio::AudioDecoder; +} + +impl ObjectImpl for SymphoniaMp3Dec {} + +impl GstObjectImpl for SymphoniaMp3Dec {} + +impl ElementImpl for SymphoniaMp3Dec { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "Symphonia MP3 decoder", + "Codec/Decoder/Audio", + "Symphonia MP3 (MPEG audio layer 3) decoder", + "François Laignel ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + let sink_caps = gst::Caps::builder("audio/mpeg") + .field("mpegversion", 1i32) + .field("layer", gst::IntRange::new(1i32, 3)) + .field("parsed", true) + .build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + + vec![sink_pad_template, decoder::src_caps_template()] + }); + + PAD_TEMPLATES.as_ref() + } +} + +impl AudioDecoderImpl for SymphoniaMp3Dec { + fn stop(&self, dec: &Self::Type) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, obj: dec, "Stopping"); + *self.context.borrow_mut() = None; + Ok(()) + } + + fn start(&self, dec: &Self::Type) -> Result<(), gst::ErrorMessage> { + gst::debug!(CAT, obj: dec, "Starting"); + *self.context.borrow_mut() = Some(Context::default()); + Ok(()) + } + + fn flush(&self, dec: &Self::Type, _hard: bool) { + gst::debug!(CAT, obj: dec, "Flushing"); + + let mut context_guard = self.context.borrow_mut(); + if let Some(ref mut context) = *context_guard { + context.reset(); + } + } + + fn handle_frame( + &self, + dec: &Self::Type, + inbuf: Option<&gst::Buffer>, + ) -> Result { + use symphonia::core::formats::Packet; + + gst::log!(CAT, obj: dec, "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, obj: dec, "Failed to map buffer readable"); + gst::FlowError::Error + })?; + + // Ignore empty packets + if inmap.len() == 0 { + return dec.finish_frame(None, 1); + } + + let mut context_guard = self.context.borrow_mut(); + + let context = context_guard + .as_mut() + .ok_or(gst::FlowError::NotNegotiated)?; + + // FIXME handle gapless parameters + let packet = Packet::new_from_slice(0, 0, 0, inmap.as_ref()); + + use symphonia::core::errors::Error::*; + let decoded = match context.decoder.decode(&packet) { + Ok(decoded) => decoded, + Err(DecodeError(err)) => { + gst::warning!(CAT, obj: dec, "an error occured decoding a packet: {}", err); + return dec.finish_frame(None, 1); + } + Err(ResetRequired) => { + gst::info!(CAT, obj: dec, "stream params changed"); + context.reset(); + // FIXME is this the right way to handle this? + // or is it good enough to call gst_pad_mark_reconfigure? + dec.static_pad("src") + .unwrap() + .push_event(gst::event::Reconfigure::new()); + return dec.finish_frame(None, 1); + } + Err(err) => { + return audio_decoder_error!( + dec, + 1, + gst::StreamError::Decode, + ["an unrecoverable error occured decoding a packet: {}", err] + ); + } + }; + + if context.out_audio_info.is_none() { + // Negotiate output audio format and layout + let src_pad = dec.static_pad("src").unwrap(); + let filter = caps_filter_from_decoded(&decoded).map_err(|err| { + gst::error!(CAT, obj: dec, "Couldn't build caps filter: {}", err); + gst::FlowError::NotNegotiated + })?; + + gst::debug!(CAT, obj: dec, "Proposing {:?}", filter); + let mut caps = src_pad.peer_query_caps(Some(&filter)); + gst::debug!(CAT, obj: dec, "Peer refined caps to {:?}", caps); + + caps.fixate(); + if caps.is_empty() { + gst::error!(CAT, obj: dec, "Failed to negotiate src pad caps {:?}", caps); + return Err(gst::FlowError::NotNegotiated); + } + + let out_audio_info = gst_audio::AudioInfo::from_caps(&caps) + .ok() + .and_then(|audio_info| dec.set_output_format(&audio_info).ok().map(|_| audio_info)) + .ok_or_else(|| { + gst::error!(CAT, obj: dec, "Failed to set output format"); + gst::FlowError::NotSupported + })?; + + gst::info!(CAT, obj: dec, "Using {:?}", caps); + context.out_audio_info = Some(out_audio_info); + } + + let out_audio_info = context.out_audio_info.as_ref().unwrap(); + let outbuf = decoder::to_outbuf(dec, decoded, out_audio_info)?; + + dec.finish_frame(Some(outbuf), 1) + } +} + +#[derive(Debug, thiserror::Error)] +enum CapsFilterError { + #[error("invalid sample rate")] + InvalidSampleRate, + #[error("more than 2 channels, not supported yet")] + MoreThan2Channels, + #[error("no channels")] + NoChannels, +} + +/// Builds caps suitable to initiate decoder downstream negotiation. +fn caps_filter_from_decoded(decoded: &AudioBufferRef) -> Result { + use symphonia::core::audio::AudioBufferRef::*; + use CapsFilterError::*; + + let in_format = match decoded { + U8(_) => gst_audio::AUDIO_FORMAT_U8, + U16(_) => gst_audio::AUDIO_FORMAT_U16, + U24(_) => gst_audio::AUDIO_FORMAT_U24, + U32(_) => gst_audio::AUDIO_FORMAT_U32, + S8(_) => gst_audio::AUDIO_FORMAT_S8, + S16(_) => gst_audio::AUDIO_FORMAT_S16, + S32(_) => gst_audio::AUDIO_FORMAT_S32, + S24(_) => gst_audio::AUDIO_FORMAT_S24, + F32(_) => gst_audio::AUDIO_FORMAT_F32, + F64(_) => gst_audio::AUDIO_FORMAT_F64, + }; + + let sample_rate = decoded.spec().rate; + if sample_rate == 0 { + return Err(InvalidSampleRate); + } + + let channels = match decoded.spec().channels.count() { + 0 => return Err(NoChannels), + n if n > 2 => return Err(MoreThan2Channels), + n => n, + }; + + Ok(decoder::build_caps_filter( + gst_audio::AudioInfo::builder(in_format, sample_rate, channels as u32) + .build() + .unwrap(), + )) +} diff --git a/audio/symphonia/src/mp3/decoder/mod.rs b/audio/symphonia/src/mp3/decoder/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..4d602690cdc39fe0a2a27d4b4a67cfa44af96668 --- /dev/null +++ b/audio/symphonia/src/mp3/decoder/mod.rs @@ -0,0 +1,28 @@ +// Copyright (C) 2022 François Laignel +// +// 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::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct SymphoniaMp3Dec(ObjectSubclass) @extends gst_audio::AudioDecoder, gst::Element, gst::Object; +} + +unsafe impl Send for SymphoniaMp3Dec {} +unsafe impl Sync for SymphoniaMp3Dec {} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "symphoniamp3dec", + gst::Rank::Marginal, + SymphoniaMp3Dec::static_type(), + ) +} diff --git a/audio/symphonia/src/mp3/mod.rs b/audio/symphonia/src/mp3/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf38c231739dfa14098addef82db6db2a3659528 --- /dev/null +++ b/audio/symphonia/src/mp3/mod.rs @@ -0,0 +1,11 @@ +// Copyright (C) 2022 François Laignel +// +// 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 + +//! Symphonia mp3 parser and decoder elements. + +pub mod decoder; diff --git a/audio/symphonia/tests/symphonia.rs b/audio/symphonia/tests/symphonia.rs new file mode 100644 index 0000000000000000000000000000000000000000..70c3d2fdd316cd68a1cc02fc25dee7c554bc1093 --- /dev/null +++ b/audio/symphonia/tests/symphonia.rs @@ -0,0 +1,232 @@ +// Copyright (C) 2022 François Laignel +// +// 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::prelude::*; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstsymphonia::plugin_register_static().expect("symphonia test"); + }); +} + +#[cfg(feature = "mp3")] +#[derive(PartialEq)] +enum OutFormat { + ForceS16, + SameAsIn, +} + +#[cfg(feature = "flac")] +#[derive(PartialEq)] +enum StreamInfoOrigin { + Caps, + InBand, +} + +#[cfg(feature = "mp3")] +#[test] +fn mp3dec_original_format() { + run_mp3dec(gst_audio::AudioLayout::Interleaved, OutFormat::SameAsIn); +} + +#[cfg(feature = "mp3")] +#[test] +fn mp3dec_force_s16() { + run_mp3dec(gst_audio::AudioLayout::Interleaved, OutFormat::ForceS16); +} + +#[cfg(all(feature = "mp3", feature = "allow-planar"))] +#[test] +fn mp3dec_planar() { + run_mp3dec(gst_audio::AudioLayout::NonInterleaved, OutFormat::SameAsIn); +} + +#[cfg(feature = "mp3")] +fn run_mp3dec(layout: gst_audio::AudioLayout, format: OutFormat) { + let data = include_bytes!("test.mp3"); + let packet_sizes = [731, 182, 522]; + let offsets: Vec<(usize, usize)> = packet_sizes + .iter() + .scan(0, |offset, size| { + let prev = *offset; + *offset += size; + Some((prev, *offset)) + }) + .collect(); + + init(); + + let mut h_sink_caps = gst::Caps::builder("audio/x-raw"); + + let (format_str, sample_size) = match format { + OutFormat::ForceS16 => { + let format_str = gst_audio::AUDIO_FORMAT_S16.to_str(); + h_sink_caps = h_sink_caps.field("format", format_str); + (format_str, std::mem::size_of::()) + } + OutFormat::SameAsIn => { + // Test file format is F32. + // don't change the harness sink pad caps + ( + gst_audio::AUDIO_FORMAT_F32.to_str(), + std::mem::size_of::(), + ) + } + }; + + let layout_str = if layout == gst_audio::AudioLayout::Interleaved { + "interleaved" + } else { + "non-interleaved" + }; + h_sink_caps = h_sink_caps.field("layout", layout_str); + + let mut h = gst_check::Harness::new("symphoniamp3dec"); + h.set_sink_caps(h_sink_caps.build()); + h.play(); + + let caps = gst::Caps::builder("audio/mpeg") + .field("mpegversion", 1i32) + .field("layer", 3i32) + .field("parsed", true) + .build(); + h.set_src_caps(caps); + + for (start, end) in offsets.iter() { + let buffer = gst::Buffer::from_slice(&data[*start..*end]); + h.push(buffer).unwrap(); + } + + h.push_event(gst::event::Eos::new()); + + const CHANNELS: usize = 2; + const RATE: usize = 44_100; + + const DECODED_SAMPLES: usize = 1_152; + for _ in offsets.iter() { + let buffer = h.pull().unwrap(); + assert_eq!(buffer.size(), DECODED_SAMPLES * sample_size * CHANNELS); + + #[cfg(feature = "audio-meta")] + { + let audio_meta = buffer.meta::().unwrap(); + assert_eq!(audio_meta.samples(), DECODED_SAMPLES); + assert_eq!(audio_meta.info().layout(), layout); + if layout == gst_audio::AudioLayout::NonInterleaved { + assert_eq!(audio_meta.offsets(), &[0, DECODED_SAMPLES * sample_size]); + } + } + } + + let expected_caps = gst::Caps::builder("audio/x-raw") + .field("format", format_str) + .field("layout", layout_str) + .field("rate", RATE as i32) + .field("channels", CHANNELS as i32) + .field( + "channel-mask", + gst::Bitmask::new(gst_audio::AudioChannelPosition::fallback_mask( + CHANNELS as u32, + )), + ) + .build(); + + let caps = h + .sinkpad() + .expect("harness has no sinkpad") + .current_caps() + .expect("pad has no caps"); + + assert_eq!(caps, expected_caps); +} + +#[cfg(feature = "flac")] +#[test] +fn flacdec_caps_stream_info() { + run_flacdec(StreamInfoOrigin::Caps); +} + +#[cfg(feature = "flac")] +#[test] +fn flacdec_in_band_stream_info() { + run_flacdec(StreamInfoOrigin::InBand); +} + +#[cfg(feature = "flac")] +fn run_flacdec(stream_info_orig: StreamInfoOrigin) { + let data = include_bytes!("test.flac"); + let packet_sizes = [4, 38, 74, 2058, 2061, 473]; + let offsets: Vec<(usize, usize)> = packet_sizes + .iter() + .scan(0, |offset, &size| { + let prev = *offset; + *offset += size; + Some((prev, *offset)) + }) + .collect(); + let decoded_samples = [4608, 4608, 1024]; + + init(); + + let mut h = gst_check::Harness::new("symphoniaflacdec"); + h.play(); + + let mut caps = gst::Caps::builder("audio/x-flac") + .field("framed", true) + .field("rate", 44_100i32) + .field("channels", 1i32); + + if stream_info_orig == StreamInfoOrigin::Caps { + let stream_info: Vec = b"\x7FFLAC\x01\x00\x00\x02" + .iter() + .chain(data[offsets[0].0..offsets[0].1].iter()) + .chain(data[offsets[1].0..offsets[1].1].iter()) + .copied() + .collect(); + caps = caps.field( + "streamheader", + gst::Array::new([ + gst::Buffer::from_slice(stream_info.into_boxed_slice()), + gst::Buffer::from_slice(&data[offsets[2].0..offsets[2].1]), + ]), + ); + } + + h.set_src_caps(caps.build()); + + for (start, end) in offsets.iter() { + let buffer = gst::Buffer::from_slice(&data[*start..*end]); + h.push(buffer).unwrap(); + } + + h.push_event(gst::event::Eos::new()); + + for samples in decoded_samples { + let buffer = h.pull().unwrap(); + assert_eq!(buffer.size(), samples * std::mem::size_of::()); + } + + let expected_caps = gst::Caps::builder("audio/x-raw") + .field("format", gst_audio::AUDIO_FORMAT_S16.to_str()) + .field("layout", "interleaved") + .field("rate", 44_100i32) + .field("channels", 1i32) + .build(); + + let caps = h + .sinkpad() + .expect("harness has no sinkpad") + .current_caps() + .expect("pad has no caps"); + + assert_eq!(caps, expected_caps); +} diff --git a/audio/symphonia/tests/test.flac b/audio/symphonia/tests/test.flac new file mode 100644 index 0000000000000000000000000000000000000000..2f12c3f7667d59f82036609e3a80d5ddd0886f9c Binary files /dev/null and b/audio/symphonia/tests/test.flac differ diff --git a/audio/symphonia/tests/test.mp3 b/audio/symphonia/tests/test.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b8b9101056680e0ce4eadad1d758d4cbc414bfc5 Binary files /dev/null and b/audio/symphonia/tests/test.mp3 differ diff --git a/meson.build b/meson.build index 75b8a6ecde7e7cf344db904994e7955e5bc37982..9ad14499362775ad0a8a7e6f901c270fda9e92ac 100644 --- a/meson.build +++ b/meson.build @@ -49,6 +49,7 @@ plugins = { 'gst-plugin-rusoto': 'libgstrusoto', 'gst-plugin-textwrap': 'libgstrstextwrap', 'gst-plugin-fmp4': 'libgstfmp4', + 'gst-plugin-symphonia': 'libgstsymphonia', 'gst-plugin-threadshare': 'libgstthreadshare', 'gst-plugin-togglerecord': 'libgsttogglerecord', 'gst-plugin-hsv': 'libgsthsv',