diff --git a/src/context.rs b/src/context.rs index bda02b63..3e7e66ec 100644 --- a/src/context.rs +++ b/src/context.rs @@ -80,6 +80,60 @@ pub trait GuiContext: Send + Sync + 'static { unsafe fn raw_default_normalized_param_value(&self, param: ParamPtr) -> f32; } +/// Information about the plugin's transport. Depending on the plugin API and the host not all +/// fields may be available. +pub struct Transport { + /// Whether the transport is currently running. + pub playing: bool, + /// Whether recording is enabled in the project. + pub recording: bool, + /// Whether the pre-roll is currently active, if the plugin API reports this information. + pub preroll_active: Option, + + /// The sample rate in Hertz. Also passed in + /// [`Plugin::initialize()`][crate::prelude::Plugin::initialize()], so if you need this then you + /// can also store that value. + pub sample_rate: f32, + /// The proejct's tempo in beats per minute. + pub tempo: Option, + /// The time signature's numerator. + pub time_sig_numerator: Option, + /// The time signature's denominator. + pub time_sig_denominator: Option, + + // XXX: VST3 also has a continuous time in samples that ignores loops, but we can't reconstruct + // something similar in CLAP so it may be best to just ignore that so you can't rely on it + /// The position in the song in samples. Can be used to calculate the time in seconds if needed. + pub(crate) pos_samples: Option, + /// The position in the song in quarter notes. Can be used to calculate the time in samples if + /// needed. + pub(crate) pos_seconds: Option, + /// The position in the song in quarter notes. Can be calculated from the time in seconds and + /// the tempo if needed. + pub(crate) pos_beats: Option, + /// The last bar's start position in beats. Can be calculated from the beat position and time + /// signature if needed. + pub(crate) bar_start_pos_beats: Option, + /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at + /// the start of the song. Can be calculated from the beat position and time signature if + /// needed. + pub(crate) bar_number: Option, + + /// The loop range in samples, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Can be calulcated from the other loop range information if needed. + pub(crate) loop_range_samples: Option<(i64, i64)>, + /// The loop range in seconds, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Can be calulcated from the other loop range information if needed. + pub(crate) loop_range_seconds: Option<(f64, f64)>, + /// The loop range in quarter notes, if the loop is active and this information is available. + /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume + /// that the end is exclusive. Can be calulcated from the other loop range information if + /// needed. + pub(crate) loop_range_beats: Option<(f64, f64)>, +} + /// A convenience helper for setting parameter values. Any changes made here will be broadcasted to /// the host and reflected in the plugin's [`Params`][crate::param::internals::Params] object. These /// functions should only be called from the main thread. @@ -87,6 +141,170 @@ pub struct ParamSetter<'a> { context: &'a dyn GuiContext, } +// TODO: These conversions have not really been tested yet, there might be an error in there somewhere +impl Transport { + /// The position in the song in samples. Will be calculated from other information if needed. + pub fn pos_samples(&self) -> Option { + match ( + self.pos_samples, + self.pos_seconds, + self.pos_beats, + self.tempo, + ) { + (Some(pos_samples), _, _, _) => Some(pos_samples), + (_, Some(pos_seconds), _, _) => { + Some((pos_seconds * self.sample_rate as f64).round() as i64) + } + (_, _, Some(pos_beats), Some(tempo)) => { + Some((pos_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64) + } + (_, _, _, _) => None, + } + } + + /// The position in the song in quarter notes. Will be calculated from other information if + /// needed. + pub fn pos_seconds(&self) -> Option { + match ( + self.pos_samples, + self.pos_seconds, + self.pos_beats, + self.tempo, + ) { + (_, Some(pos_seconds), _, _) => Some(pos_seconds), + (Some(pos_samples), _, _, _) => Some(pos_samples as f64 / self.sample_rate as f64), + (_, _, Some(pos_beats), Some(tempo)) => Some(pos_beats / tempo * 60.0), + (_, _, _, _) => None, + } + } + + /// The position in the song in quarter notes. Will be calculated from other information if + /// needed. + pub fn pos_beats(&self) -> Option { + match ( + self.pos_samples, + self.pos_seconds, + self.pos_beats, + self.tempo, + ) { + (_, _, Some(pos_beats), _) => Some(pos_beats), + (_, Some(pos_seconds), _, Some(tempo)) => Some(pos_seconds / 60.0 * tempo), + (Some(pos_samples), _, _, Some(tempo)) => { + Some(pos_samples as f64 / self.sample_rate as f64 / 60.0 * tempo) + } + (_, _, _, _) => None, + } + } + + /// The last bar's start position in beats. Will be calculated from other information if needed. + pub fn bar_start_pos_beats(&self) -> Option { + if self.bar_start_pos_beats.is_some() { + return self.bar_start_pos_beats; + } + + match ( + self.time_sig_numerator, + self.time_sig_denominator, + self.pos_beats(), + ) { + (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => { + let quarter_note_bar_length = + time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0; + Some(pos_beats.div_euclid(quarter_note_bar_length)) + } + (_, _, _) => None, + } + } + + /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at + /// the start of the song. Will be calculated from other information if needed. + pub fn bar_number(&self) -> Option { + if self.bar_number.is_some() { + return self.bar_number; + } + + match ( + self.time_sig_numerator, + self.time_sig_denominator, + self.pos_beats(), + ) { + (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => { + let quarter_note_bar_length = + time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0; + Some((pos_beats / quarter_note_bar_length).floor() as i32) + } + (_, _, _) => None, + } + } + + /// The loop range in samples, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Will be calculated from other information if needed. + pub fn loop_range_samples(&self) -> Option<(i64, i64)> { + match ( + self.loop_range_samples, + self.loop_range_seconds, + self.loop_range_beats, + self.tempo, + ) { + (Some(loop_range_samples), _, _, _) => Some(loop_range_samples), + (_, Some((start_seconds, end_seconds)), _, _) => Some(( + ((start_seconds * self.sample_rate as f64).round() as i64), + ((end_seconds * self.sample_rate as f64).round() as i64), + )), + (_, _, Some((start_beats, end_beats)), Some(tempo)) => Some(( + (start_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64, + (end_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64, + )), + (_, _, _, _) => None, + } + } + + /// The loop range in seconds, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Will be calculated from other information if needed. + pub fn loop_range_seconds(&self) -> Option<(f64, f64)> { + match ( + self.loop_range_samples, + self.loop_range_seconds, + self.loop_range_beats, + self.tempo, + ) { + (_, Some(loop_range_seconds), _, _) => Some(loop_range_seconds), + (Some((start_samples, end_samples)), _, _, _) => Some(( + start_samples as f64 / self.sample_rate as f64, + end_samples as f64 / self.sample_rate as f64, + )), + (_, _, Some((start_beats, end_beats)), Some(tempo)) => { + Some((start_beats / tempo * 60.0, end_beats / tempo * 60.0)) + } + (_, _, _, _) => None, + } + } + + /// The loop range in quarter notes, if the loop is active and this information is available. + /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume + /// that the end is exclusive. Will be calculated from other information if needed. + pub fn loop_range_beats(&self) -> Option<(f64, f64)> { + match ( + self.loop_range_samples, + self.loop_range_seconds, + self.loop_range_beats, + self.tempo, + ) { + (_, _, Some(loop_range_beats), _) => Some(loop_range_beats), + (_, Some((start_seconds, end_seconds)), _, Some(tempo)) => { + Some((start_seconds / 60.0 * tempo, end_seconds / 60.0 * tempo)) + } + (Some((start_samples, end_samples)), _, _, Some(tempo)) => Some(( + start_samples as f64 / self.sample_rate as f64 / 60.0 * tempo, + end_samples as f64 / self.sample_rate as f64 / 60.0 * tempo, + )), + (_, _, _, _) => None, + } + } +} + impl<'a> ParamSetter<'a> { pub fn new(context: &'a dyn GuiContext) -> Self { Self { context }