231 lines
6.8 KiB
Rust
231 lines
6.8 KiB
Rust
use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType};
|
|
use crate::audio::midi::MidiEvent;
|
|
|
|
const PARAM_RESOLUTION: u32 = 0;
|
|
|
|
const DEFAULT_BPM: f32 = 120.0;
|
|
const DEFAULT_BEATS_PER_BAR: u32 = 4;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum BeatResolution {
|
|
Whole = 0, // 1/1
|
|
Half = 1, // 1/2
|
|
Quarter = 2, // 1/4
|
|
Eighth = 3, // 1/8
|
|
Sixteenth = 4, // 1/16
|
|
QuarterT = 5, // 1/4 triplet
|
|
EighthT = 6, // 1/8 triplet
|
|
}
|
|
|
|
impl BeatResolution {
|
|
fn from_f32(value: f32) -> Self {
|
|
match value.round() as i32 {
|
|
0 => BeatResolution::Whole,
|
|
1 => BeatResolution::Half,
|
|
2 => BeatResolution::Quarter,
|
|
3 => BeatResolution::Eighth,
|
|
4 => BeatResolution::Sixteenth,
|
|
5 => BeatResolution::QuarterT,
|
|
6 => BeatResolution::EighthT,
|
|
_ => BeatResolution::Quarter,
|
|
}
|
|
}
|
|
|
|
/// How many subdivisions per quarter note beat
|
|
fn subdivisions_per_beat(&self) -> f64 {
|
|
match self {
|
|
BeatResolution::Whole => 0.25, // 1 per 4 beats
|
|
BeatResolution::Half => 0.5, // 1 per 2 beats
|
|
BeatResolution::Quarter => 1.0, // 1 per beat
|
|
BeatResolution::Eighth => 2.0, // 2 per beat
|
|
BeatResolution::Sixteenth => 4.0, // 4 per beat
|
|
BeatResolution::QuarterT => 1.5, // 3 per 2 beats (triplet)
|
|
BeatResolution::EighthT => 3.0, // 3 per beat (triplet)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Beat clock node — generates tempo-synced CV signals.
|
|
///
|
|
/// BPM and time signature are synced from the project document via SetTempo.
|
|
/// When playing: synced to timeline position.
|
|
/// When stopped: free-runs continuously at the project BPM.
|
|
///
|
|
/// Outputs:
|
|
/// - BPM: constant CV proportional to tempo (bpm / 240)
|
|
/// - Beat Phase: sawtooth 0→1 per beat subdivision
|
|
/// - Bar Phase: sawtooth 0→1 per bar (uses project time signature)
|
|
/// - Gate: 1.0 for first half of each subdivision, 0.0 otherwise
|
|
pub struct BeatNode {
|
|
name: String,
|
|
bpm: f32,
|
|
beats_per_bar: u32,
|
|
resolution: BeatResolution,
|
|
/// Playback time in seconds, set by the graph before process()
|
|
playback_time: f64,
|
|
/// Previous playback_time to detect paused state
|
|
prev_playback_time: f64,
|
|
/// Free-running time accumulator for when playback is stopped
|
|
free_run_time: f64,
|
|
inputs: Vec<NodePort>,
|
|
outputs: Vec<NodePort>,
|
|
parameters: Vec<Parameter>,
|
|
}
|
|
|
|
impl BeatNode {
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
let inputs = vec![];
|
|
|
|
let outputs = vec![
|
|
NodePort::new("BPM", SignalType::CV, 0),
|
|
NodePort::new("Beat Phase", SignalType::CV, 1),
|
|
NodePort::new("Bar Phase", SignalType::CV, 2),
|
|
NodePort::new("Gate", SignalType::CV, 3),
|
|
];
|
|
|
|
let parameters = vec![
|
|
Parameter::new(PARAM_RESOLUTION, "Resolution", 0.0, 6.0, 2.0, ParameterUnit::Generic),
|
|
];
|
|
|
|
Self {
|
|
name: name.into(),
|
|
bpm: DEFAULT_BPM,
|
|
beats_per_bar: DEFAULT_BEATS_PER_BAR,
|
|
resolution: BeatResolution::Quarter,
|
|
playback_time: 0.0,
|
|
prev_playback_time: -1.0,
|
|
free_run_time: 0.0,
|
|
inputs,
|
|
outputs,
|
|
parameters,
|
|
}
|
|
}
|
|
|
|
pub fn set_playback_time(&mut self, time: f64) {
|
|
self.playback_time = time;
|
|
}
|
|
|
|
pub fn set_tempo(&mut self, bpm: f32, beats_per_bar: u32) {
|
|
self.bpm = bpm;
|
|
self.beats_per_bar = beats_per_bar;
|
|
}
|
|
}
|
|
|
|
impl AudioNode for BeatNode {
|
|
fn category(&self) -> NodeCategory {
|
|
NodeCategory::Utility
|
|
}
|
|
|
|
fn inputs(&self) -> &[NodePort] {
|
|
&self.inputs
|
|
}
|
|
|
|
fn outputs(&self) -> &[NodePort] {
|
|
&self.outputs
|
|
}
|
|
|
|
fn parameters(&self) -> &[Parameter] {
|
|
&self.parameters
|
|
}
|
|
|
|
fn set_parameter(&mut self, id: u32, value: f32) {
|
|
match id {
|
|
PARAM_RESOLUTION => self.resolution = BeatResolution::from_f32(value),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn get_parameter(&self, id: u32) -> f32 {
|
|
match id {
|
|
PARAM_RESOLUTION => self.resolution as i32 as f32,
|
|
_ => 0.0,
|
|
}
|
|
}
|
|
|
|
fn process(
|
|
&mut self,
|
|
_inputs: &[&[f32]],
|
|
outputs: &mut [&mut [f32]],
|
|
_midi_inputs: &[&[MidiEvent]],
|
|
_midi_outputs: &mut [&mut Vec<MidiEvent>],
|
|
sample_rate: u32,
|
|
) {
|
|
if outputs.len() < 4 {
|
|
return;
|
|
}
|
|
|
|
let bpm_cv = (self.bpm / 240.0).clamp(0.0, 1.0);
|
|
let len = outputs[0].len();
|
|
let sample_period = 1.0 / sample_rate as f64;
|
|
|
|
// Detect paused: playback_time hasn't changed since last process()
|
|
let paused = self.playback_time == self.prev_playback_time;
|
|
self.prev_playback_time = self.playback_time;
|
|
|
|
let beats_per_second = self.bpm as f64 / 60.0;
|
|
let subs_per_beat = self.resolution.subdivisions_per_beat();
|
|
|
|
// Choose time source: timeline when playing, free-running when stopped
|
|
let base_time = if paused { self.free_run_time } else { self.playback_time };
|
|
|
|
for i in 0..len {
|
|
let time = base_time + i as f64 * sample_period;
|
|
let beat_pos = time * beats_per_second;
|
|
|
|
// Beat subdivision phase: 0→1 sawtooth
|
|
let sub_phase = ((beat_pos * subs_per_beat) % 1.0) as f32;
|
|
|
|
// Bar phase: 0→1 over one bar (beats_per_bar beats)
|
|
let bar_phase = ((beat_pos / self.beats_per_bar as f64) % 1.0) as f32;
|
|
|
|
// Gate: high for first half of each subdivision
|
|
let gate = if sub_phase < 0.5 { 1.0f32 } else { 0.0 };
|
|
|
|
outputs[0][i] = bpm_cv;
|
|
outputs[1][i] = sub_phase;
|
|
outputs[2][i] = bar_phase;
|
|
outputs[3][i] = gate;
|
|
}
|
|
|
|
// Advance free-run time (always ticks, so it's ready when playback stops)
|
|
self.free_run_time += len as f64 * sample_period;
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.playback_time = 0.0;
|
|
self.prev_playback_time = -1.0;
|
|
self.free_run_time = 0.0;
|
|
}
|
|
|
|
fn node_type(&self) -> &str {
|
|
"Beat"
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
fn clone_node(&self) -> Box<dyn AudioNode> {
|
|
Box::new(Self {
|
|
name: self.name.clone(),
|
|
bpm: self.bpm,
|
|
beats_per_bar: self.beats_per_bar,
|
|
resolution: self.resolution,
|
|
playback_time: 0.0,
|
|
prev_playback_time: -1.0,
|
|
free_run_time: 0.0,
|
|
inputs: self.inputs.clone(),
|
|
outputs: self.outputs.clone(),
|
|
parameters: self.parameters.clone(),
|
|
})
|
|
}
|
|
|
|
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
}
|