use super::audio_iter::SampleIter;
use super::audio_options::Channels;
use super::audio_resolve::{resolve_and_build_desc, AudioResolvePlan};
use super::audio_sink::SampleSink;
use super::error::FrameExportError;
use crate::core::context::demuxer::Demuxer;
use crate::core::context::ffmpeg_context::FfmpegContext;
use crate::core::context::filter_complex::FilterComplex;
use crate::core::context::input::Input;
use crate::core::context::output::Output;
use crate::core::filter::frame_pipeline_builder::FramePipelineBuilder;
use crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler;
use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_AUDIO;
const WHISPER_SAMPLE_RATE: u32 = 16_000;
pub struct SampleExtractor {
input: Input,
audio_stream_index: Option<usize>,
sample_rate: Option<u32>,
channels: Option<Channels>,
start_time_us: Option<i64>,
duration_us: Option<i64>,
channel_capacity: usize,
}
impl SampleExtractor {
pub fn new(input: impl Into<Input>) -> Self {
Self {
input: input.into(),
audio_stream_index: None,
sample_rate: None,
channels: None,
start_time_us: None,
duration_us: None,
channel_capacity: 4,
}
}
pub fn for_whisper(input: impl Into<Input>) -> Self {
Self::new(input)
.sample_rate(WHISPER_SAMPLE_RATE)
.channels(Channels::Mono)
}
pub fn audio_stream_index(mut self, index: usize) -> Self {
self.audio_stream_index = Some(index);
self
}
pub fn sample_rate(mut self, hz: u32) -> Self {
self.sample_rate = Some(hz);
self
}
pub fn channels(mut self, channels: Channels) -> Self {
self.channels = Some(channels);
self
}
pub fn start_time_us(mut self, us: i64) -> Self {
self.start_time_us = Some(us);
self
}
pub fn duration_us(mut self, us: i64) -> Self {
self.duration_us = Some(us);
self
}
pub fn channel_capacity(mut self, capacity: usize) -> Self {
self.channel_capacity = capacity;
self
}
pub fn samples(self) -> crate::error::Result<SampleIter> {
self.validate()?;
let capacity = self.channel_capacity.max(1);
let (tx, rx) = crossbeam_channel::bounded(capacity);
let sink = SampleSink::new(tx);
let sink_pipeline = FramePipelineBuilder::new(AVMEDIA_TYPE_AUDIO)
.filter("frame_export_sample_sink", Box::new(sink))
.set_stream_index(0)
.build();
let mut input = self.input;
if let Some(start) = self.start_time_us {
input = input.set_start_time_us(start);
}
if let Some(dur) = self.duration_us {
input = input.set_recording_time_us(dur);
}
let output = Output::from("-")
.set_format("null")
.set_audio_codec("pcm_f32le")
.add_stream_map("[export]")
.add_frame_pipeline(sink_pipeline);
let resolve_plan = AudioResolvePlan {
stream_index: self.audio_stream_index,
sample_rate: self.sample_rate,
channels: self.channels,
};
let resolver = move |demuxs: &[Demuxer]| -> crate::error::Result<FilterComplex> {
let demux = demuxs.first().ok_or(FrameExportError::NoAudioStream)?;
let desc = unsafe { resolve_and_build_desc(demux.in_fmt_ctx_ptr(), &resolve_plan)? };
Ok(desc.into())
};
let context = FfmpegContext::builder()
.input(input)
.output(output)
.add_deferred_filter_desc(Box::new(resolver))
.build()?;
let scheduler = FfmpegScheduler::new(context).start()?;
Ok(SampleIter::new(rx, scheduler))
}
pub fn collect_samples(self) -> crate::error::Result<Vec<f32>> {
let mut out: Vec<f32> = Vec::new();
for chunk in self.samples()? {
let chunk = chunk?;
if out.is_empty() {
out = chunk.into_vec();
} else {
out.extend_from_slice(chunk.as_slice());
}
}
Ok(out)
}
fn validate(&self) -> crate::error::Result<()> {
if self.channel_capacity == 0 {
return Err(invalid("channel_capacity must be >= 1"));
}
if self.sample_rate == Some(0) {
return Err(invalid("sample_rate must be > 0"));
}
if let Some(d) = self.duration_us.or(self.input.recording_time_us) {
if d <= 0 {
return Err(invalid(
"duration_us (or the Input's recording time) must be > 0",
));
}
}
Ok(())
}
}
fn invalid(msg: &str) -> crate::error::Error {
FrameExportError::InvalidOption(msg.to_string()).into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_capacity_is_rejected() {
let r = SampleExtractor::new("x.mp4").channel_capacity(0).validate();
assert!(matches!(
r,
Err(crate::error::Error::FrameExport(
FrameExportError::InvalidOption(_)
))
));
}
#[test]
fn zero_sample_rate_is_rejected() {
assert!(SampleExtractor::new("x.mp4")
.sample_rate(0)
.validate()
.is_err());
}
#[test]
fn non_positive_duration_is_rejected() {
assert!(SampleExtractor::new("x.mp4")
.duration_us(0)
.validate()
.is_err());
assert!(SampleExtractor::new("x.mp4")
.duration_us(-1)
.validate()
.is_err());
}
#[test]
fn defaults_validate() {
assert!(SampleExtractor::new("x.mp4").validate().is_ok());
assert!(SampleExtractor::new("x.mp4")
.sample_rate(16000)
.channels(Channels::Mono)
.duration_us(250_000)
.validate()
.is_ok());
}
#[test]
fn whisper_preset_sets_rate_and_mono() {
let e = SampleExtractor::for_whisper("x.mp4");
assert_eq!(e.sample_rate, Some(WHISPER_SAMPLE_RATE));
assert_eq!(e.channels, Some(Channels::Mono));
assert!(e.validate().is_ok());
}
}