From 7d3ab342d26f13df89aa16ea03c1cdefb40bbdb9 Mon Sep 17 00:00:00 2001 From: Ruben Gonzalez Date: Sun, 13 Oct 2019 12:21:09 +0200 Subject: [PATCH] Add claxon based FLAC decoder. Audio decoder element structure is based in `gst-plugin-lewton` (a lewton based Vorbis decoder created by @slomo) The element assumes correctly parsed input from `flacparse`. Implementation details: * Claxon returned frames does not contain audio channels interleaved, the reorganization of the channels is done by the element. * Claxon output buffers are always Vec, mapping to the correct type (depending on the audio format) is also done by the element. Mono s16 and stereo_s32 test are provided. Complete pipelines to test are: ``` gst-launch-1.0 -v souphttpsrc location=https://archive.org/download/MLKDream/MLKDream.flac ! queue2 ! flacparse ! flacdec ! audioconvert ! autoaudiosink gst-launch-1.0 -v audiotestsrc ! audio/x-raw, format=S16LE, layout=interleaved, rate=44100, channels=1 ! audioconvert ! flacenc ! flacparse ! claxondec ! autoaudiosink ``` Fixes #71 --- Cargo.toml | 1 + gst-plugin-claxon/Cargo.toml | 25 + gst-plugin-claxon/build.rs | 5 + gst-plugin-claxon/src/claxondec.rs | 465 +++++++++++++++++++ gst-plugin-claxon/src/lib.rs | 34 ++ gst-plugin-claxon/tests/claxondec.rs | 116 +++++ gst-plugin-claxon/tests/test_mono_s16.flac | Bin 0 -> 126 bytes gst-plugin-claxon/tests/test_stereo_s32.flac | Bin 0 -> 17507 bytes 8 files changed, 646 insertions(+) create mode 100644 gst-plugin-claxon/Cargo.toml create mode 100644 gst-plugin-claxon/build.rs create mode 100644 gst-plugin-claxon/src/claxondec.rs create mode 100644 gst-plugin-claxon/src/lib.rs create mode 100644 gst-plugin-claxon/tests/claxondec.rs create mode 100644 gst-plugin-claxon/tests/test_mono_s16.flac create mode 100644 gst-plugin-claxon/tests/test_stereo_s32.flac diff --git a/Cargo.toml b/Cargo.toml index c84fd62d..6315198a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "gst-plugin-rusoto", "gst-plugin-fallbackswitch", "gst-plugin-lewton", + "gst-plugin-claxon", ] [profile.release] diff --git a/gst-plugin-claxon/Cargo.toml b/gst-plugin-claxon/Cargo.toml new file mode 100644 index 00000000..46551581 --- /dev/null +++ b/gst-plugin-claxon/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "gst-plugin-claxon" +version = "0.1.0" +authors = ["Ruben Gonzalez "] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugin-rs" +license = "MIT/Apache-2.0" +description = "Claxon FLAC Decoder Plugin" +edition = "2018" + +[dependencies] +glib = { git = "https://github.com/gtk-rs/glib" } +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gstreamer-audio = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gstreamer-check = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +claxon = { version = "0.4" } +byte-slice-cast = "~0.3.4" +atomic_refcell = "0.1" + +[lib] +name = "gstclaxon" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../gst-plugin-version-helper" } diff --git a/gst-plugin-claxon/build.rs b/gst-plugin-claxon/build.rs new file mode 100644 index 00000000..0d1ddb61 --- /dev/null +++ b/gst-plugin-claxon/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/gst-plugin-claxon/src/claxondec.rs b/gst-plugin-claxon/src/claxondec.rs new file mode 100644 index 00000000..6262feb1 --- /dev/null +++ b/gst-plugin-claxon/src/claxondec.rs @@ -0,0 +1,465 @@ +// Copyright (C) 2019 Ruben Gonzalez +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::subclass::prelude::*; +use gst_audio; +use gst_audio::prelude::*; +use gst_audio::subclass::prelude::*; + +use std::io::Cursor; + +use atomic_refcell::AtomicRefCell; + +use byte_slice_cast::*; + +struct State { + streaminfo: Option, + audio_info: Option, +} + +struct ClaxonDec { + cat: gst::DebugCategory, + state: AtomicRefCell>, +} + +impl ObjectSubclass for ClaxonDec { + const NAME: &'static str = "ClaxonDec"; + type ParentType = gst_audio::AudioDecoder; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn new() -> Self { + Self { + cat: gst::DebugCategory::new( + "claxondec", + gst::DebugColorFlags::empty(), + Some("Claxon FLAC decoder"), + ), + state: AtomicRefCell::new(None), + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + klass.set_metadata( + "Claxon FLAC decoder", + "Decoder/Audio", + "Claxon FLAC decoder", + "Ruben Gonzalez ", + ); + + let sink_caps = gst::Caps::new_simple("audio/x-flac", &[("framed", &true)]); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let src_caps = gst::Caps::new_simple( + "audio/x-raw", + &[ + ( + "format", + &gst::List::new(&[ + &gst_audio::AudioFormat::S8.to_str(), + &gst_audio::AUDIO_FORMAT_S16.to_str(), + &gst_audio::AUDIO_FORMAT_S2432.to_str(), + &gst_audio::AUDIO_FORMAT_S32.to_str(), + ]), + ), + ("rate", &gst::IntRange::::new(1, 655_350)), + ("channels", &gst::IntRange::::new(1, 8)), + ("layout", &"interleaved"), + ], + ); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + } +} + +impl ObjectImpl for ClaxonDec { + glib_object_impl!(); +} + +impl ElementImpl for ClaxonDec {} + +impl AudioDecoderImpl for ClaxonDec { + fn stop(&self, _element: &gst_audio::AudioDecoder) -> Result<(), gst::ErrorMessage> { + *self.state.borrow_mut() = None; + + Ok(()) + } + + fn start(&self, _element: &gst_audio::AudioDecoder) -> Result<(), gst::ErrorMessage> { + *self.state.borrow_mut() = Some(State { + streaminfo: None, + audio_info: None, + }); + + Ok(()) + } + + fn set_format( + &self, + element: &gst_audio::AudioDecoder, + caps: &gst::Caps, + ) -> Result<(), gst::LoggableError> { + gst_debug!(self.cat, obj: element, "Setting format {:?}", caps); + + let mut streaminfo: Option = None; + let mut audio_info: Option = None; + + let s = caps.get_structure(0).unwrap(); + if let Ok(Some(streamheaders)) = s.get_optional::("streamheader") { + let streamheaders = streamheaders.as_slice(); + + if streamheaders.len() < 2 { + gst_debug!( + self.cat, + obj: element, + "Not enough streamheaders, trying in-band" + ); + } else { + let ident_buf = streamheaders[0].get::(); + if let Ok(Some(ident_buf)) = ident_buf { + gst_debug!(self.cat, obj: element, "Got streamheader buffers"); + let inmap = ident_buf.map_readable().unwrap(); + + if inmap[0..7] != [0x7f, b'F', b'L', b'A', b'C', 0x01, 0x00] { + gst_debug!(self.cat, obj: element, "Unknown streamheader format"); + } else { + if let Ok(tstreaminfo) = get_claxon_streaminfo(&inmap[13..]) { + if let Ok(taudio_info) = get_gstaudioinfo(tstreaminfo) { + // To speed up negotiation + if element.set_output_format(&taudio_info).is_err() + || element.negotiate().is_err() + { + gst_debug!( + self.cat, + obj: element, + "Error to negotiate output from based on in-caps streaminfo" + ); + } + + audio_info = Some(taudio_info); + streaminfo = Some(tstreaminfo); + } + } + } + } + } + } + + let mut state_guard = self.state.borrow_mut(); + *state_guard = Some(State { + streaminfo, + audio_info, + }); + + Ok(()) + } + + fn handle_frame( + &self, + element: &gst_audio::AudioDecoder, + inbuf: Option<&gst::Buffer>, + ) -> Result { + gst_debug!(self.cat, obj: element, "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!(self.cat, obj: element, "Failed to buffer readable"); + gst::FlowError::Error + })?; + + let mut state_guard = self.state.borrow_mut(); + let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?; + + if inmap.as_slice() == b"fLaC" { + gst_debug!(self.cat, obj: element, "fLaC buffer received"); + } else if inmap[0] & 0x7F == 0x00 { + gst_debug!(self.cat, obj: element, "Streaminfo header buffer received"); + return self.handle_streaminfo_header(element, state, inmap.as_ref()); + } else if inmap[0] == 0b1111_1111 && inmap[1] & 0b1111_1100 == 0b1111_1000 { + gst_debug!(self.cat, obj: element, "Data buffer received"); + return self.handle_data(element, state, inmap.as_ref()); + } else { + // info about other headers in flacparse and https://xiph.org/flac/format.html + gst_debug!( + self.cat, + obj: element, + "Other header buffer received {:?}", + inmap[0] & 0x7F + ); + } + + element.finish_frame(None, 1) + } +} + +impl ClaxonDec { + fn handle_streaminfo_header( + &self, + element: &gst_audio::AudioDecoder, + state: &mut State, + indata: &[u8], + ) -> Result { + let streaminfo = match get_claxon_streaminfo(indata) { + Ok(v) => v, + Err(error) => { + gst_element_error!(element, gst::StreamError::Decode, [error]); + return Err(gst::FlowError::Error); + } + }; + + let audio_info = match get_gstaudioinfo(streaminfo) { + Ok(v) => v, + Err(error) => { + gst_element_error!(element, gst::StreamError::Decode, [error]); + return Err(gst::FlowError::Error); + } + }; + + gst_debug!( + self.cat, + obj: element, + "Successfully parsed headers: {:?}", + audio_info + ); + + element.set_output_format(&audio_info)?; + element.negotiate()?; + + state.streaminfo = Some(streaminfo); + state.audio_info = Some(audio_info); + + element.finish_frame(None, 1) + } + + fn handle_data( + &self, + element: &gst_audio::AudioDecoder, + state: &mut State, + indata: &[u8], + ) -> Result { + // TODO It's valid for FLAC to not have any streaminfo header at all, for a small subset + // of possible FLAC configurations. (claxon does not actually support that) + let audio_info = state + .audio_info + .as_ref() + .ok_or(gst::FlowError::NotNegotiated)?; + let channels = audio_info.channels() as usize; + + if channels > 8 { + unreachable!( + "FLAC only supports from 1 to 8 channels (audio contains {} channels)", + channels + ); + } + + if ![8, 16, 24, 32].contains(&audio_info.depth()) { + unreachable!( + "claxondec doesn't supports {}bits audio", + audio_info.depth() + ); + } + + let buffer = Vec::new(); + let mut cursor = Cursor::new(indata); + let mut reader = claxon::frame::FrameReader::new(&mut cursor); + let result = match reader.read_next_or_eof(buffer) { + Ok(Some(result)) => result, + Ok(None) => return element.finish_frame(None, 1), + Err(err) => { + return gst_audio_decoder_error!( + element, + 1, + gst::StreamError::Decode, + ["Failed to decode packet: {:?}", err] + ); + } + }; + + assert_eq!(cursor.position(), indata.len() as u64); + + let v = if channels != 1 { + let mut v: Vec = vec![0; result.len() as usize]; + + for (o, i) in v.chunks_exact_mut(channels).enumerate() { + for c in 0..channels { + i[c] = result.sample(c as u32, o as u32); + } + } + v + } else { + result.into_buffer() + }; + + let outbuf = if audio_info.depth() == 8 { + let v = v.iter().map(|e| *e as i8).collect::>(); + gst::Buffer::from_slice(v.into_byte_vec()) + } else if audio_info.depth() == 16 { + let v = v.iter().map(|e| *e as i16).collect::>(); + gst::Buffer::from_slice(v.into_byte_vec()) + } else { + gst::Buffer::from_slice(v.into_byte_vec()) + }; + + element.finish_frame(Some(outbuf), 1) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "claxondec", + gst::Rank::Marginal, + ClaxonDec::get_type(), + ) +} + +fn get_claxon_streaminfo(indata: &[u8]) -> Result { + let mut cursor = Cursor::new(indata); + let mut metadata_iter = claxon::metadata::MetadataBlockReader::new(&mut cursor); + let streaminfo = match metadata_iter.next() { + Some(Ok(claxon::metadata::MetadataBlock::StreamInfo(info))) => info, + _ => return Err("Failed to decode STREAMINFO"), + }; + + assert_eq!(cursor.position(), indata.len() as u64); + + Ok(streaminfo) +} + +fn get_gstaudioinfo( + streaminfo: claxon::metadata::StreamInfo, +) -> Result { + let format = match streaminfo.bits_per_sample { + 8 => gst_audio::AudioFormat::S8, + 16 => gst_audio::AUDIO_FORMAT_S16, + 24 => gst_audio::AUDIO_FORMAT_S2432, + 32 => gst_audio::AUDIO_FORMAT_S32, + _ => return Err("format not supported"), + }; + + if streaminfo.channels > 8 { + return Err("more than 8 channels not supported yet"); + } + let mut audio_info = + gst_audio::AudioInfo::new(format, streaminfo.sample_rate, streaminfo.channels); + + let index = streaminfo.channels as usize; + let to = &FLAC_CHANNEL_POSITIONS[index - 1][..index]; + audio_info = audio_info.positions(to); + + Ok(audio_info.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/gst-plugin-claxon/src/lib.rs b/gst-plugin-claxon/src/lib.rs new file mode 100644 index 00000000..d6058bd7 --- /dev/null +++ b/gst-plugin-claxon/src/lib.rs @@ -0,0 +1,34 @@ +// Copyright (C) 2019 Ruben Gonzalez +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![crate_type = "cdylib"] + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gstreamer as gst; +#[macro_use] +extern crate gstreamer_audio as gst_audio; + +mod claxondec; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + claxondec::register(plugin) +} + +gst_plugin_define!( + claxon, + 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/gst-plugin-claxon/tests/claxondec.rs b/gst-plugin-claxon/tests/claxondec.rs new file mode 100644 index 00000000..83b4fb41 --- /dev/null +++ b/gst-plugin-claxon/tests/claxondec.rs @@ -0,0 +1,116 @@ +// Copyright (C) 2019 Ruben Gonzalez +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +extern crate glib; +extern crate gstreamer as gst; +use gst::prelude::*; +extern crate gstreamer_audio as gst_audio; +extern crate gstreamer_check as gst_check; + +extern crate gstclaxon; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstclaxon::plugin_register_static().expect("claxon test"); + }); +} + +#[test] +fn test_mono_s16() { + let data = include_bytes!("test_mono_s16.flac"); + // 4 fLaC header, 38 streaminfo_header, 66 other header, 18 data + let packet_sizes = [4, 38, 66, 18]; + let decoded_samples = [0usize, 0usize, 0usize, 2]; + + let caps = do_test(data, &packet_sizes, &decoded_samples); + + assert_eq!( + caps, + gst::Caps::builder("audio/x-raw") + .field("format", &gst_audio::AUDIO_FORMAT_S16.to_str()) + .field("rate", &44_100i32) + .field("channels", &1i32) + .field("layout", &"interleaved") + .build() + ); +} + +#[test] +fn test_stereo_s32() { + let data = include_bytes!("test_stereo_s32.flac"); + // 4 fLaC header, 38 streaminfo_header, 17465 data + let packet_sizes = [4, 38, 17465]; + let decoded_samples = [0usize, 0usize, 8192]; + + let caps = do_test(data, &packet_sizes, &decoded_samples); + + assert_eq!( + caps, + gst::Caps::builder("audio/x-raw") + .field("format", &gst_audio::AUDIO_FORMAT_S2432.to_str()) + .field("rate", &44_100i32) + .field("channels", &2i32) + .field("layout", &"interleaved") + .field("channel-mask", &gst::Bitmask::new(0x3)) + .build() + ); +} + +fn do_test(data: &'static [u8], packet_sizes: &[usize], decoded_samples: &[usize]) -> gst::Caps { + let packet_offsets = packet_sizes + .iter() + .scan(0, |state, &size| { + *state += size; + Some(*state) + }) + .collect::>(); + + init(); + + let mut h = gst_check::Harness::new("claxondec"); + h.play(); + + let caps = gst::Caps::builder("audio/x-flac") + .field("framed", &true) + .build(); + h.set_src_caps(caps); + + let packet_offsets_iter = std::iter::once(&0).chain(packet_offsets.iter()); + let skip = 0; + + for (offset_start, offset_end) in packet_offsets_iter + .clone() + .skip(skip) + .zip(packet_offsets_iter.clone().skip(skip + 1)) + { + let buffer = gst::Buffer::from_slice(&data[*offset_start..*offset_end]); + h.push(buffer).unwrap(); + } + + h.push_event(gst::Event::new_eos().build()); + + for samples in decoded_samples { + if *samples == 0 { + continue; + } + let buffer = h.pull().unwrap(); + assert_eq!(buffer.get_size(), 4 * samples); + } + + let caps = h + .get_sinkpad() + .expect("harness has no sinkpad") + .get_current_caps() + .expect("pad has no caps"); + + caps +} diff --git a/gst-plugin-claxon/tests/test_mono_s16.flac b/gst-plugin-claxon/tests/test_mono_s16.flac new file mode 100644 index 0000000000000000000000000000000000000000..7312dd6365389b9a0f5fa67f35fbe0bb8f3490df GIT binary patch literal 126 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 literal 0 HcmV?d00001