soundcraft/crates/model/src/track.rs

373 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Tracks and playlists.
use crate::{AutoParam, AutomationLane, ChannelFormat, Clip, ClipId, Mixer, TrackId};
use soundcraft_time::{Range, Samples};
/// Track types (Track › New).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
pub enum TrackKind {
#[default]
Audio,
Aux,
Master,
Midi,
Instrument,
Vca,
Folder,
/// Picture: clips reference a movie ([`crate::VideoSource`]); no audio path.
Video,
}
impl TrackKind {
pub const ALL: [TrackKind; 8] = [
TrackKind::Audio,
TrackKind::Aux,
TrackKind::Master,
TrackKind::Midi,
TrackKind::Instrument,
TrackKind::Vca,
TrackKind::Folder,
TrackKind::Video,
];
pub fn label(self) -> &'static str {
match self {
TrackKind::Audio => "Audio Track",
TrackKind::Aux => "Aux Input",
TrackKind::Master => "Master Fader",
TrackKind::Midi => "MIDI Track",
TrackKind::Instrument => "Instrument Track",
TrackKind::Vca => "VCA Master",
TrackKind::Folder => "Routing Folder Track",
TrackKind::Video => "Video Track",
}
}
pub fn default_name(self) -> &'static str {
match self {
TrackKind::Audio => "Audio",
TrackKind::Aux => "Aux",
TrackKind::Master => "Master",
TrackKind::Midi => "MIDI",
TrackKind::Instrument => "Inst",
TrackKind::Vca => "VCA",
TrackKind::Folder => "Folder",
TrackKind::Video => "Video",
}
}
pub fn id(self) -> &'static str {
match self {
TrackKind::Audio => "audio",
TrackKind::Aux => "aux",
TrackKind::Master => "master",
TrackKind::Midi => "midi",
TrackKind::Instrument => "instrument",
TrackKind::Vca => "vca",
TrackKind::Folder => "folder",
TrackKind::Video => "video",
}
}
pub fn from_id(s: &str) -> Option<TrackKind> {
let s = s.to_ascii_lowercase();
TrackKind::ALL.into_iter().find(|k| k.id() == s || k.label().eq_ignore_ascii_case(&s))
}
/// Track kinds that hold clips on a timeline.
pub fn has_playlist(self) -> bool {
matches!(self, TrackKind::Audio | TrackKind::Midi | TrackKind::Instrument | TrackKind::Video)
}
pub fn is_midi(self) -> bool {
matches!(self, TrackKind::Midi | TrackKind::Instrument)
}
/// Kinds that pass audio through the mixer.
pub fn is_audio_path(self) -> bool {
matches!(self, TrackKind::Audio | TrackKind::Aux | TrackKind::Master | TrackKind::Instrument | TrackKind::Folder)
}
}
/// Track heights (View › Track Height presets).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
pub enum TrackHeight {
/// Any height from Micro to Extreme, in points.
Custom(u16),
Micro,
Mini,
Small,
#[default]
Medium,
Large,
Jumbo,
Extreme,
Fit,
}
impl TrackHeight {
pub const ALL: [TrackHeight; 7] = [
TrackHeight::Micro,
TrackHeight::Mini,
TrackHeight::Small,
TrackHeight::Medium,
TrackHeight::Large,
TrackHeight::Jumbo,
TrackHeight::Extreme,
];
pub fn points(self) -> f32 {
match self {
TrackHeight::Custom(points) => f32::from(points).clamp(TrackHeight::Micro.points(), TrackHeight::Extreme.points()),
TrackHeight::Micro => 16.0,
TrackHeight::Mini => 24.0,
TrackHeight::Small => 46.0,
TrackHeight::Medium => 88.0,
TrackHeight::Large => 132.0,
TrackHeight::Jumbo => 220.0,
TrackHeight::Extreme => 440.0,
TrackHeight::Fit => 88.0,
}
}
pub fn label(self) -> &'static str {
match self {
TrackHeight::Custom(_) => "custom",
TrackHeight::Micro => "micro",
TrackHeight::Mini => "mini",
TrackHeight::Small => "small",
TrackHeight::Medium => "medium",
TrackHeight::Large => "large",
TrackHeight::Jumbo => "jumbo",
TrackHeight::Extreme => "extreme",
TrackHeight::Fit => "fit to window",
}
}
pub fn from_id(s: &str) -> Option<TrackHeight> {
TrackHeight::ALL.into_iter().find(|h| h.label().eq_ignore_ascii_case(s))
}
/// The preset this many points tall, else a custom height clamped to Micro..Extreme.
pub fn from_points(points: f32) -> TrackHeight {
let p = points.max(TrackHeight::Micro.points()).min(TrackHeight::Extreme.points()).round() as u16;
TrackHeight::ALL.into_iter().find(|h| h.points() == f32::from(p)).unwrap_or(TrackHeight::Custom(p))
}
/// The next preset taller (or shorter) than this height; Extreme (or Micro) stays.
pub fn step(self, taller: bool) -> TrackHeight {
let p = self.points();
let next =
if taller { TrackHeight::ALL.into_iter().find(|h| h.points() > p) } else { TrackHeight::ALL.into_iter().rev().find(|h| h.points() < p) };
next.unwrap_or(self)
}
}
/// The default track colour palette (original, chosen for SoundCraft).
pub const TRACK_COLORS: [[u8; 3]; 16] = [
[86, 132, 214],
[94, 170, 214],
[80, 190, 168],
[104, 186, 98],
[168, 196, 76],
[222, 196, 72],
[230, 152, 62],
[222, 104, 72],
[214, 82, 110],
[196, 86, 168],
[150, 98, 206],
[110, 104, 214],
[130, 150, 170],
[178, 140, 110],
[120, 170, 140],
[190, 120, 150],
];
/// A playlist: an alternative take of a track's clips.
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct Playlist {
pub name: String,
/// Clips sorted by start; non-overlapping except at crossfades.
pub clips: Vec<Clip>,
}
impl Playlist {
pub fn new(name: impl Into<String>) -> Self {
Playlist { name: name.into(), clips: Vec::new() }
}
pub fn sort(&mut self) {
self.clips.sort_by_key(|c| (c.start, c.id.0));
}
pub fn clip(&self, id: ClipId) -> Option<&Clip> {
self.clips.iter().find(|c| c.id == id)
}
pub fn clip_mut(&mut self, id: ClipId) -> Option<&mut Clip> {
self.clips.iter_mut().find(|c| c.id == id)
}
pub fn clips_in(&self, r: Range) -> impl Iterator<Item = &Clip> {
self.clips.iter().filter(move |c| c.range().overlaps(&r))
}
pub fn clip_at(&self, at: Samples) -> Option<&Clip> {
self.clips.iter().rev().find(|c| c.range().contains(at))
}
pub fn end(&self) -> Samples {
self.clips.iter().map(Clip::end).max().unwrap_or(0)
}
}
/// A track.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Track {
pub id: TrackId,
pub name: String,
pub kind: TrackKind,
pub format: ChannelFormat,
pub color: [u8; 3],
pub height: TrackHeight,
pub playlists: Vec<Playlist>,
pub active_playlist: usize,
pub mixer: Mixer,
pub automation: Vec<AutomationLane>,
/// Which lane the track shows: `waveform`/`blocks`/`notes`/`regions` or an automation param id.
pub view: String,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub inactive: bool,
#[serde(default)]
pub frozen: bool,
#[serde(default)]
pub comments: String,
/// Folder that contains this track.
#[serde(default)]
pub folder: Option<TrackId>,
#[serde(default = "yes")]
pub folder_open: bool,
/// Instrument plugin (instrument tracks) — registry id.
#[serde(default)]
pub instrument: Option<crate::Insert>,
/// Ticks (musical) or samples timebase for clips.
#[serde(default)]
pub ticks_timebase: bool,
/// Elastic Audio algorithm, if enabled.
#[serde(default)]
pub elastic: Option<String>,
/// Pinned to top in the Edit window.
#[serde(default)]
pub pinned: bool,
}
fn yes() -> bool {
true
}
impl Track {
pub fn new(id: TrackId, name: impl Into<String>, kind: TrackKind, format: ChannelFormat, color: [u8; 3]) -> Self {
let name = name.into();
let format = if kind.is_midi() && kind == TrackKind::Midi { ChannelFormat::Mono } else { format };
let view = if kind.is_midi() {
"notes"
} else if kind.has_playlist() {
"waveform"
} else {
"volume"
};
Track {
id,
playlists: vec![Playlist::new(name.clone())],
name,
kind,
format,
color,
height: TrackHeight::Medium,
active_playlist: 0,
mixer: Mixer::new(format),
automation: Vec::new(),
view: view.into(),
hidden: false,
inactive: false,
frozen: false,
comments: String::new(),
folder: None,
folder_open: true,
instrument: None,
ticks_timebase: kind.is_midi(),
elastic: None,
pinned: false,
}
}
pub fn channels(&self) -> usize {
self.format.channels()
}
pub fn playlist(&self) -> Option<&Playlist> {
self.playlists.get(self.active_playlist)
}
pub fn playlist_mut(&mut self) -> Option<&mut Playlist> {
self.playlists.get_mut(self.active_playlist)
}
pub fn clips(&self) -> &[Clip] {
self.playlist().map_or(&[], |p| p.clips.as_slice())
}
pub fn lane(&self, p: &AutoParam) -> Option<&AutomationLane> {
self.automation.iter().find(|l| &l.param == p)
}
pub fn lane_mut(&mut self, p: &AutoParam) -> &mut AutomationLane {
let idx = match self.automation.iter().position(|l| &l.param == p) {
Some(i) => i,
None => {
self.automation.push(AutomationLane::new(p.clone()));
self.automation.len() - 1
}
};
let len = self.automation.len();
// `idx` is in range: it came from `position` or is the index of the element just pushed.
&mut self.automation[idx.min(len - 1)]
}
pub fn is_folder(&self) -> bool {
self.kind == TrackKind::Folder
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_track_defaults() {
let t = Track::new(TrackId(1), "Gtr", TrackKind::Audio, ChannelFormat::Stereo, TRACK_COLORS[0]);
assert_eq!(t.channels(), 2);
assert_eq!(t.view, "waveform");
assert_eq!(t.playlists.len(), 1);
let m = Track::new(TrackId(2), "Keys", TrackKind::Midi, ChannelFormat::Stereo, TRACK_COLORS[0]);
assert_eq!(m.format, ChannelFormat::Mono);
assert_eq!(m.view, "notes");
}
#[test]
fn lane_mut_creates_once() {
let mut t = Track::new(TrackId(1), "Gtr", TrackKind::Audio, ChannelFormat::Mono, TRACK_COLORS[0]);
t.lane_mut(&AutoParam::Volume).set_point(0, -3.0);
t.lane_mut(&AutoParam::Volume).set_point(10, -6.0);
assert_eq!(t.automation.len(), 1);
assert_eq!(t.lane(&AutoParam::Volume).map(|l| l.points.len()), Some(2));
}
#[test]
fn kind_ids() {
for k in TrackKind::ALL {
assert_eq!(TrackKind::from_id(k.id()), Some(k));
}
}
#[test]
fn custom_heights_round_to_presets_and_step_through_them() {
assert_eq!(TrackHeight::from_points(88.0), TrackHeight::Medium);
assert_eq!(TrackHeight::from_points(57.4), TrackHeight::Custom(57));
assert_eq!(TrackHeight::from_points(9999.0), TrackHeight::Extreme);
assert_eq!(TrackHeight::from_points(f32::NAN), TrackHeight::Micro);
assert_eq!(TrackHeight::Custom(u16::MAX).points(), 440.0);
assert_eq!(TrackHeight::Custom(57).step(true), TrackHeight::Medium);
assert_eq!(TrackHeight::Custom(57).step(false), TrackHeight::Small);
assert_eq!(TrackHeight::Extreme.step(true), TrackHeight::Extreme);
assert_eq!(TrackHeight::Micro.step(false), TrackHeight::Micro);
let json = serde_json::to_string(&TrackHeight::Custom(57)).unwrap();
assert_eq!(serde_json::from_str::<TrackHeight>(&json).unwrap(), TrackHeight::Custom(57));
assert_eq!(serde_json::from_str::<TrackHeight>("\"Large\"").unwrap(), TrackHeight::Large);
}
}