Lightningbeam/daw-backend/src/audio/clip.rs

141 lines
4.4 KiB
Rust

use serde::{Serialize, Deserialize};
/// Audio clip instance ID type
pub type AudioClipInstanceId = u32;
/// Type alias for backwards compatibility
pub type ClipId = AudioClipInstanceId;
/// Audio clip instance that references content in the AudioClipPool
///
/// This represents a placed instance of audio content on the timeline.
/// The actual audio data is stored in the AudioClipPool and referenced by `audio_pool_index`.
///
/// ## Timing Model
/// - `internal_start` / `internal_end`: Define the region of the source audio to play (trimming)
/// - `external_start` / `external_duration`: Define where the clip appears on the timeline and how long
///
/// ## Looping
/// If `external_duration` is greater than `internal_end - internal_start`,
/// the clip will seamlessly loop back to `internal_start` when it reaches `internal_end`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioClipInstance {
pub id: AudioClipInstanceId,
pub audio_pool_index: usize,
/// Start position within the audio content (seconds)
pub internal_start: f64,
/// End position within the audio content (seconds)
pub internal_end: f64,
/// Start position on the timeline (seconds)
pub external_start: f64,
/// Duration on the timeline (seconds) - can be longer than internal duration for looping
pub external_duration: f64,
/// Clip-level gain
pub gain: f32,
}
/// Type alias for backwards compatibility
pub type Clip = AudioClipInstance;
impl AudioClipInstance {
/// Create a new audio clip instance
pub fn new(
id: AudioClipInstanceId,
audio_pool_index: usize,
internal_start: f64,
internal_end: f64,
external_start: f64,
external_duration: f64,
) -> Self {
Self {
id,
audio_pool_index,
internal_start,
internal_end,
external_start,
external_duration,
gain: 1.0,
}
}
/// Create a clip instance from legacy parameters (for backwards compatibility)
/// Maps old start_time/duration/offset to new timing model
pub fn from_legacy(
id: AudioClipInstanceId,
audio_pool_index: usize,
start_time: f64,
duration: f64,
offset: f64,
) -> Self {
Self {
id,
audio_pool_index,
internal_start: offset,
internal_end: offset + duration,
external_start: start_time,
external_duration: duration,
gain: 1.0,
}
}
/// Check if this clip instance is active at a given timeline position
pub fn is_active_at(&self, time_seconds: f64) -> bool {
time_seconds >= self.external_start && time_seconds < self.external_end()
}
/// Get the end time of this clip instance on the timeline
pub fn external_end(&self) -> f64 {
self.external_start + self.external_duration
}
/// Get the end time of this clip instance on the timeline
/// (Alias for external_end(), for backwards compatibility)
pub fn end_time(&self) -> f64 {
self.external_end()
}
/// Get the start time on the timeline
/// (Alias for external_start, for backwards compatibility)
pub fn start_time(&self) -> f64 {
self.external_start
}
/// Get the internal (content) duration
pub fn internal_duration(&self) -> f64 {
self.internal_end - self.internal_start
}
/// Check if this clip instance loops
pub fn is_looping(&self) -> bool {
self.external_duration > self.internal_duration()
}
/// Get the position within the audio content for a given timeline position
/// Returns None if the timeline position is outside this clip instance
/// Handles looping automatically
pub fn get_content_position(&self, timeline_pos: f64) -> Option<f64> {
if timeline_pos < self.external_start || timeline_pos >= self.external_end() {
return None;
}
let relative_pos = timeline_pos - self.external_start;
let internal_duration = self.internal_duration();
if internal_duration <= 0.0 {
return None;
}
// Wrap around for looping
let content_offset = relative_pos % internal_duration;
Some(self.internal_start + content_offset)
}
/// Set clip gain
pub fn set_gain(&mut self, gain: f32) {
self.gain = gain.max(0.0);
}
}