Skip to main content

falcon_ffmpeg/
tempo.rs

1//! Changing the speed of sound without changing its pitch, through FFmpeg's
2//! `atempo` filter: Eagle's main player is a `<video>` element, whose
3//! `preservesPitch` does the same.
4//!
5//! A [`Tempo`] is a small filter graph, `abuffer -> atempo -> abuffersink`,
6//! taking and giving the player's stereo floats at 48 kHz, so no conversion
7//! filter is needed. The filter holds some sound back and its own timestamps
8//! count output time, so [`crate::sync::TempoTimeline`] says which part of
9//! the clip what comes out belongs to. A graph can't be rewound: a seek
10//! makes a new one.
11
12use std::ffi::{CString, c_int, c_uint};
13use std::ptr;
14
15use crate::decode::{OUT_CHANNELS, OUT_RATE, check};
16use crate::ffi::{self, AVERROR_EAGAIN, AVERROR_EOF, AVFilterContext, AVFilterGraph, AVFrame, Api};
17use crate::sync::{self, TempoTimeline};
18use crate::{Error, Result};
19
20/// Sound going through `atempo` at one speed.
21pub struct Tempo {
22    api: &'static Api,
23    graph: *mut AVFilterGraph,
24    src: *mut AVFilterContext,
25    sink: *mut AVFilterContext,
26    frame: *mut AVFrame,
27    rate: f64,
28    timeline: TempoTimeline,
29    /// Sample frames sent in, the timestamps the source wants.
30    frames_in: i64,
31}
32
33impl Tempo {
34    /// A graph for `rate` times speed.
35    pub fn new(rate: f64) -> Result<Tempo> {
36        let api = ffi::api().map_err(|e| Error(e.to_string()))?;
37        Tempo::with_api(api, rate)
38    }
39
40    fn with_api(api: &'static Api, rate: f64) -> Result<Tempo> {
41        // SAFETY: takes nothing, returns an owned frame or null.
42        let frame = unsafe { (api.av_frame_alloc)() };
43        // SAFETY: takes nothing, returns an owned graph or null.
44        let graph = unsafe { (api.avfilter_graph_alloc)() };
45        // Built first, so that its Drop frees both on every way out below.
46        let mut tempo = Tempo {
47            api,
48            graph,
49            src: ptr::null_mut(),
50            sink: ptr::null_mut(),
51            frame,
52            rate,
53            timeline: TempoTimeline::new(rate, OUT_RATE),
54            frames_in: 0,
55        };
56        if frame.is_null() || graph.is_null() {
57            return Err(Error("could not allocate the tempo filter".into()));
58        }
59        let source = format!("sample_rate={OUT_RATE}:sample_fmt=flt:channel_layout=stereo:time_base=1/{OUT_RATE}");
60        tempo.src = tempo.filter("abuffer", "in", Some(&source))?;
61        tempo.sink = tempo.filter("abuffersink", "out", None)?;
62        let mut last = tempo.src;
63        for (i, speed) in sync::tempo_chain(rate).into_iter().enumerate() {
64            let next = tempo.filter("atempo", &format!("tempo{i}"), Some(&format!("tempo={speed}")))?;
65            tempo.link(last, next)?;
66            last = next;
67        }
68        tempo.link(last, tempo.sink)?;
69        // SAFETY: owned; every filter in it is linked; a null log context is allowed.
70        check(api, unsafe { (api.avfilter_graph_config)(tempo.graph, ptr::null_mut()) }, "set up the tempo filter")?;
71        Ok(tempo)
72    }
73
74    /// Adds a filter to the graph.
75    fn filter(&self, kind: &str, name: &str, args: Option<&str>) -> Result<*mut AVFilterContext> {
76        let (kind, name) = (cstring(kind)?, cstring(name)?);
77        let args = args.map(cstring).transpose()?;
78        // SAFETY: a NUL-terminated name; FFmpeg returns a static filter or null.
79        let filter = unsafe { (self.api.avfilter_get_by_name)(kind.as_ptr()) };
80        if filter.is_null() {
81            return Err(Error(format!("this FFmpeg has no {} filter", kind.to_string_lossy())));
82        }
83        let mut ctx = ptr::null_mut();
84        // SAFETY: `ctx` is an out-pointer, the strings live through the call
85        // and FFmpeg copies them; the graph is owned and takes the filter.
86        let r = unsafe { (self.api.avfilter_graph_create_filter)(&mut ctx, filter, name.as_ptr(), args.as_ref().map_or(ptr::null(), |a| a.as_ptr()), ptr::null_mut(), self.graph) };
87        check(self.api, r, "add a tempo filter")?;
88        Ok(ctx)
89    }
90
91    fn link(&self, from: *mut AVFilterContext, to: *mut AVFilterContext) -> Result<()> {
92        const FIRST_PAD: c_uint = 0;
93        // SAFETY: both filters belong to the owned graph, each with one pad on that side.
94        check(self.api, unsafe { (self.api.avfilter_link)(from, FIRST_PAD, to, FIRST_PAD) }, "link the tempo filter")?;
95        Ok(())
96    }
97
98    /// The speed this graph was made for.
99    pub fn rate(&self) -> f64 {
100        self.rate
101    }
102
103    /// Sends in interleaved stereo floats at [`OUT_RATE`], the first of them
104    /// at media time `pts` if it is known.
105    pub fn push(&mut self, samples: &[f32], pts: Option<f64>) -> Result<()> {
106        let frames = samples.len() / OUT_CHANNELS;
107        if frames == 0 {
108            return Ok(());
109        }
110        self.timeline.input(pts);
111        let api = self.api;
112        // SAFETY: the frame is owned and empty (the source moved the last
113        // one out); `av_frame_get_buffer` allocates one interleaved plane of
114        // `frames` stereo floats for the fields set here, which `samples`
115        // fills; the source takes the reference and leaves the frame empty.
116        let r = unsafe {
117            let f = &mut *self.frame;
118            f.format = ffi::AV_SAMPLE_FMT_FLT;
119            f.sample_rate = OUT_RATE as c_int;
120            f.nb_samples = frames as c_int;
121            f.pts = self.frames_in;
122            (api.av_channel_layout_default)(&mut f.ch_layout, OUT_CHANNELS as c_int);
123            let r = (api.av_frame_get_buffer)(self.frame, 0);
124            if r < 0 {
125                (api.av_frame_unref)(self.frame);
126                r
127            } else {
128                ptr::copy_nonoverlapping(samples.as_ptr(), f.data[0].cast::<f32>(), frames * OUT_CHANNELS);
129                let r = (api.av_buffersrc_add_frame)(self.src, self.frame);
130                (api.av_frame_unref)(self.frame);
131                r
132            }
133        };
134        check(api, r, "send sound to the tempo filter")?;
135        self.frames_in += frames as i64;
136        Ok(())
137    }
138
139    /// Says no more sound is coming, so [`Tempo::pull`] gives what is left.
140    pub fn finish(&mut self) -> Result<()> {
141        // SAFETY: owned; a null frame is how the source is told of the end.
142        check(self.api, unsafe { (self.api.av_buffersrc_add_frame)(self.src, ptr::null_mut()) }, "finish the tempo filter")?;
143        Ok(())
144    }
145
146    /// Appends all the sound ready to `out`, and returns the media time of
147    /// the first sample appended (if any was and the time is known).
148    pub fn pull(&mut self, out: &mut Vec<f32>) -> Option<f64> {
149        let mut first = None;
150        loop {
151            // SAFETY: both owned; the sink fills the empty frame or leaves it empty.
152            let r = unsafe { (self.api.av_buffersink_get_frame)(self.sink, self.frame) };
153            if r < 0 {
154                debug_assert!(r == AVERROR_EAGAIN || r == AVERROR_EOF, "{}", ffi::error_text(self.api, r));
155                return first;
156            }
157            // SAFETY: owned, and just filled by the sink.
158            let f = unsafe { &*self.frame };
159            let frames = f.nb_samples.max(0) as usize;
160            if f.format == ffi::AV_SAMPLE_FMT_FLT && frames > 0 {
161                let time = self.timeline.next();
162                // SAFETY: one interleaved plane of `frames` stereo floats,
163                // the format the graph was set up with and just checked.
164                let got = unsafe { std::slice::from_raw_parts(f.data[0].cast::<f32>(), frames * OUT_CHANNELS) };
165                out.extend_from_slice(got);
166                self.timeline.output(frames);
167                first = first.or(time);
168            }
169            // SAFETY: owned.
170            unsafe { (self.api.av_frame_unref)(self.frame) };
171        }
172    }
173}
174
175impl Drop for Tempo {
176    fn drop(&mut self) {
177        // SAFETY: owned; both accept a pointer to null and null it.
178        unsafe {
179            (self.api.av_frame_free)(&mut self.frame);
180            (self.api.avfilter_graph_free)(&mut self.graph);
181        }
182    }
183}
184
185fn cstring(s: &str) -> Result<CString> {
186    CString::new(s).map_err(|e| Error(e.to_string()))
187}
188
189/// The player's stretcher: the graph for the current speed, made when first
190/// needed and forgotten at a seek. No graph at normal speed, or at a speed
191/// the filter refused (a replacement FFmpeg without `atempo`, say); the
192/// player resamples to speed then, as a pitch-moving last resort.
193pub struct Stretch {
194    api: &'static Api,
195    tempo: Option<Tempo>,
196    refused: Option<f64>,
197}
198
199impl Stretch {
200    pub fn new(api: &'static Api) -> Stretch {
201        Stretch { api, tempo: None, refused: None }
202    }
203
204    /// The graph to send sound at `rate` through, if it needs one.
205    pub fn at(&mut self, rate: f64) -> Option<&mut Tempo> {
206        if !sync::needs_tempo(rate) {
207            return None;
208        }
209        if self.tempo.as_ref().is_some_and(|t| t.rate() != rate) {
210            self.tempo = None;
211        }
212        if self.tempo.is_none() && self.refused != Some(rate) {
213            match Tempo::with_api(self.api, rate) {
214                Ok(t) => self.tempo = Some(t),
215                Err(_) => self.refused = Some(rate),
216            }
217        }
218        self.tempo.as_mut()
219    }
220
221    /// Forgets the graph and anything it held back, after a seek.
222    pub fn reset(&mut self) {
223        self.tempo = None;
224    }
225}