// Oak Video Editor - Non-Linear Video Editor // Copyright (C) 2026 Oak Team // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! M12 P4: timeline audio waveforms. //! //! The [`WaveformCache`] holds per-clip min/max peak data extracted //! through `oakengine_waveform_extract` (the real FFmpeg-backed //! extraction); the engine refreshes it when the timeline rebuilds. The //! [`OakClipDecorator`] draws the peaks into the timeline's clip body. //! //! The extraction is keyed by `(clip id, filename)` and cached for the //! engine lifetime; the plan's disk cache is a later refinement (the //! extractor itself is the real implementation). use std::collections::HashMap; use std::ffi::{c_char, c_int}; use std::sync::{Arc, Mutex}; use gpui::timeline::clip::ClipDecorator; use gpui::timeline::ClipId; use gpui::timeline::FrameRange; use gpui::{Bounds, Hsla, Pixels, Window}; /// One min/max amplitude pair (mirror of the oakaudio waveform C ABI /// `MinMax`: two f32s). #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct MinMax { /// Minimum amplitude in the window. pub min: f32, /// Maximum amplitude in the window. pub max: f32, } /// The cached waveform of one clip. #[derive(Debug, Clone)] pub struct ClipWaveform { /// Per-point min/max (first channel). pub peaks: Vec, /// Channel count of the media. pub channel_count: i32, /// Media sample rate. pub sample_rate: i32, /// Samples per point at extraction time. pub samples_per_point: i32, /// The clip's length in timeline frames. pub duration_frames: i64, } /// Per-clip waveform cache (thread-safe; filled by the engine). pub struct WaveformCache { /// Peaks by clip id. map: Mutex>>, /// Timeline frame rate (frames per second) for frame→time mapping. fps: f32, } impl WaveformCache { /// Create an empty cache at the given frame rate. pub fn new(fps: f32) -> Arc { Arc::new(WaveformCache { map: Mutex::new(HashMap::new()), fps: if fps > 0.0 { fps } else { 25.0 }, }) } /// The cached waveform of `clip`, if extracted. pub fn get(&self, clip: u64) -> Option> { self.map.lock().unwrap_or_else(|e| e.into_inner()).get(&clip).cloned() } /// Ensure `clip` (with media `filename`, `duration_frames` long) has a /// waveform, extracting it through the real codec-backed extractor /// when missing. Failures (missing media, no audio stream) leave the /// clip silent (no waveform drawn). pub fn refresh(&self, clip: u64, filename: &str, duration_frames: i64) { if self.get(clip).is_some() { return; } let Some(waveform) = extract(filename, duration_frames, self.fps) else { return; }; let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner()); if !map.contains_key(&clip) { map.insert(clip, Arc::new(waveform)); } } } /// Extract a clip's waveform (first channel) via the facade's waveform /// C ABI. fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option { let cname = std::ffi::CString::new(filename).ok()?; const SAMPLES_PER_POINT: c_int = 256; unsafe { let mut channels: c_int = 0; let needed = crate::oakui::ffi::oakengine_waveform_extract( cname.as_ptr(), 0, // audio-stream index (probe numbering) SAMPLES_PER_POINT, std::ptr::null_mut(), 0, &mut channels, ); if needed <= 0 || channels <= 0 { return None; } // The extractor's `out_pairs` receives `point_count * // channel_count` channel-interleaved pairs (capacity is in points), // so the buffer is sized for the media's channel count. let channel_count = channels.max(1) as usize; let mut raw = vec![MinMax::default(); needed as usize * channel_count]; let rc = crate::oakui::ffi::oakengine_waveform_extract( cname.as_ptr(), 0, SAMPLES_PER_POINT, raw.as_mut_ptr(), needed, &mut channels, ); if rc < 0 { return None; } // Keep only the first channel: the pairs are interleaved per point // (point i channel c at index i * channels + c). let count = rc.min(needed) as usize; let mut peaks = Vec::with_capacity(count); peaks.extend((0..count).map(|i| raw[i * channel_count])); Some(ClipWaveform { peaks, channel_count: channels.max(1), sample_rate: 48000, samples_per_point: SAMPLES_PER_POINT, duration_frames, }) } } /// The timeline's clip decorator: draws extracted waveforms into audio /// clips (M12 P4). Reads the engine-populated cache. pub struct OakClipDecorator { /// The waveform cache. pub cache: Arc, } impl ClipDecorator for OakClipDecorator { fn paint_waveform( &mut self, window: &mut Window, clip: ClipId, _visible_range: FrameRange, bounds: Bounds, ) { let Some(waveform) = self.cache.get(clip.0) else { return; }; if waveform.peaks.is_empty() || bounds.size.width.as_f32() <= 0.0 { return; } let width = bounds.size.width.as_f32() as usize; if width == 0 { return; } let height = bounds.size.height.as_f32(); let mid = bounds.origin.y.as_f32() + height / 2.0; let half = (height / 2.0).max(1.0) * 0.9; // Media seconds covered by one waveform point. let secs_per_point = waveform.samples_per_point.max(1) as f32 / waveform.sample_rate.max(1) as f32; let frames_per_point = secs_per_point * self.cache.fps; // Sample one peak per horizontal pixel column. let color = Hsla { h: 0.55, s: 0.5, l: 0.55, a: 0.85, }; for x in 0..width { let t = x as f32 / width as f32; let frame = (t * waveform.duration_frames as f32) as usize; let point_index = (frame as f32 / frames_per_point.max(0.0001)) as usize; let point_index = point_index.min(waveform.peaks.len() - 1); let p = waveform.peaks[point_index]; let y_top = mid - p.max.max(0.0) * half; let y_bot = mid - p.min.min(0.0) * half; let x_px = bounds.origin.x.as_f32() + x as f32; window.paint_quad(gpui::fill( gpui::Bounds::from_corners( gpui::point(Pixels::from(x_px), Pixels::from(y_top)), gpui::point(Pixels::from(x_px + 1.0), Pixels::from(y_bot.max(y_top))), ), color, )); } } }