mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-06 18:34:31 +00:00
Video (new standalone crate soundcraft-video): - Pure-Rust H.264 (Baseline/Main/High), ProRes (all flavours) and Motion JPEG decoding with ISO-BMFF demux, adapted from FilmCraft's crates; fuzzed with mutated/truncated movies - TrackKind::Video with ClipContent::Video referencing Session.videos; file.import_video adds the picture track and the movie's audio; relinking on open; video.offset/online/list - Video and Video Universe windows, thumbnails on the timeline, background decoding Third-party plugins: - Plugin state (CLAP state ext, VST3 component + controller state) saved in sessions - Hosted instances created, state-loaded and dropped on the UI thread, never the audio thread - Plugin editors: floating CLAP GUIs and VST3 views on macOS (verified with Melodyne) - New isolated FFI crate soundcraft-au-host: Audio Units on macOS (effects and instruments, parameters, latency, MIDI, ClassInfo state), wired into the mixer and plugin browser UI: Window > Renderer (speaker layout with live levels and object positions), Window > UI Customization (interface scale, reduce motion), Window > Configurations > Auto-Update. Fix: saturating sample-index math in clip rendering (hostile stretch values overflowed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
3.4 KiB
Rust
113 lines
3.4 KiB
Rust
//! Audio sources (files in the session's Audio Files folder) and their decoded data, and the
|
|
//! movies video clips reference.
|
|
|
|
use crate::SourceId;
|
|
use soundcraft_audio_io::{AudioBuffer, FileFormat, peaks::Peaks};
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
|
|
/// Metadata for an audio file used by the session (serialised).
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct Source {
|
|
pub id: SourceId,
|
|
pub name: String,
|
|
/// Path relative to the session folder (or absolute for referenced media).
|
|
pub path: String,
|
|
pub channels: u16,
|
|
pub frames: u64,
|
|
pub sample_rate: u32,
|
|
pub format: FileFormat,
|
|
/// Original BWF time stamp (samples), if any.
|
|
#[serde(default)]
|
|
pub time_reference: u64,
|
|
/// True when the file has not been written to disk yet (recorded/rendered this session).
|
|
#[serde(default)]
|
|
pub unsaved: bool,
|
|
}
|
|
|
|
/// A movie used by Video track clips (serialised). The pictures themselves are never stored in
|
|
/// the session: the UI opens the movie from `path` and decodes frames on demand.
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct VideoSource {
|
|
pub id: SourceId,
|
|
pub name: String,
|
|
/// Absolute path of the movie (referenced, never copied into the session folder).
|
|
pub path: String,
|
|
#[serde(default)]
|
|
pub width: u32,
|
|
#[serde(default)]
|
|
pub height: u32,
|
|
/// Frames per second (0 = unknown).
|
|
#[serde(default)]
|
|
pub frame_rate: f64,
|
|
/// Picture length in seconds.
|
|
#[serde(default)]
|
|
pub duration: f64,
|
|
/// Codec id (`h264`, `prores`, `mjpeg`, `hevc`, …).
|
|
#[serde(default)]
|
|
pub codec: String,
|
|
}
|
|
|
|
impl VideoSource {
|
|
/// Display aspect ratio (width / height), 16:9 when unknown.
|
|
pub fn aspect(&self) -> f64 {
|
|
if self.width == 0 || self.height == 0 { 16.0 / 9.0 } else { f64::from(self.width) / f64::from(self.height) }
|
|
}
|
|
}
|
|
|
|
/// Decoded audio plus waveform overviews (not serialised).
|
|
#[derive(Debug)]
|
|
pub struct SourceAudio {
|
|
pub buffer: AudioBuffer,
|
|
pub peaks: Vec<Peaks>,
|
|
}
|
|
|
|
impl SourceAudio {
|
|
pub fn new(buffer: AudioBuffer) -> Self {
|
|
let peaks = buffer.channels.iter().map(|c| Peaks::build(c)).collect();
|
|
SourceAudio { buffer, peaks }
|
|
}
|
|
pub fn frames(&self) -> usize {
|
|
self.buffer.frames()
|
|
}
|
|
pub fn channel(&self, ch: usize) -> Option<&[f32]> {
|
|
self.buffer.channels.get(ch).map(Vec::as_slice)
|
|
}
|
|
}
|
|
|
|
/// Shared decoded audio, keyed by source id.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SourcePool {
|
|
map: BTreeMap<SourceId, Arc<SourceAudio>>,
|
|
}
|
|
|
|
impl SourcePool {
|
|
pub fn insert(&mut self, id: SourceId, audio: Arc<SourceAudio>) {
|
|
self.map.insert(id, audio);
|
|
}
|
|
pub fn get(&self, id: SourceId) -> Option<&Arc<SourceAudio>> {
|
|
self.map.get(&id)
|
|
}
|
|
pub fn remove(&mut self, id: SourceId) -> Option<Arc<SourceAudio>> {
|
|
self.map.remove(&id)
|
|
}
|
|
pub fn contains(&self, id: SourceId) -> bool {
|
|
self.map.contains_key(&id)
|
|
}
|
|
pub fn len(&self) -> usize {
|
|
self.map.len()
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
self.map.is_empty()
|
|
}
|
|
pub fn ids(&self) -> impl Iterator<Item = SourceId> + '_ {
|
|
self.map.keys().copied()
|
|
}
|
|
}
|
|
|
|
impl PartialEq for SourcePool {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.map.len() == other.map.len() && self.map.iter().zip(other.map.iter()).all(|((a, x), (b, y))| a == b && Arc::ptr_eq(x, y))
|
|
}
|
|
}
|