use super::smoother::Smoother;
const MAX_ROLL_SECS: f64 = 16.0; const SMOOTH_DEPTH_S: f64 = 0.010;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum RollMode {
Roll,
SlipRoll,
RevRoll,
Helix,
}
pub struct RollFx {
pub enabled: bool,
pub mode: RollMode,
bpm: f64,
beat_value: f64,
buf_len_frames: usize,
captured_frames: usize,
buffer: Vec<[f32; 2]>,
max_frames: usize,
write_pos: usize,
captured: bool,
read_frame: usize,
depth_target: f64,
depth_smooth: Smoother,
sample_rate: f64,
}
impl RollFx {
pub fn new(sample_rate: f64) -> Self {
let max_frames = (MAX_ROLL_SECS * sample_rate) as usize;
let initial_buf_len = ((60.0 / 120.0) * sample_rate) as usize; Self {
enabled: false,
mode: RollMode::Roll,
bpm: 120.0,
beat_value: 1.0,
buf_len_frames: initial_buf_len,
captured_frames: 0,
buffer: vec![[0.0; 2]; max_frames],
max_frames,
write_pos: 0,
captured: false,
read_frame: 0,
depth_target: 1.0,
depth_smooth: Smoother::new(sample_rate, SMOOTH_DEPTH_S),
sample_rate,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
if enabled && !self.enabled {
self.write_pos = 0;
self.captured = false;
self.captured_frames = 0;
self.read_frame = 0;
self.recompute_buf_len();
self.depth_smooth.reset(self.depth_target);
}
self.enabled = enabled;
}
pub fn set_mode(&mut self, mode: RollMode) {
self.mode = mode;
}
pub fn set_depth(&mut self, depth: f64) {
self.depth_target = depth.clamp(0.0, 1.0);
}
pub fn set_bpm(&mut self, bpm: f64) {
if bpm > 0.0 && (bpm - self.bpm).abs() > 0.01 {
self.bpm = bpm;
}
}
pub fn set_beat_value(&mut self, beat_value: f64) {
if beat_value <= 0.0 || (beat_value - self.beat_value).abs() < 1e-9 {
return;
}
self.beat_value = beat_value;
match self.mode {
RollMode::SlipRoll => {
self.write_pos = 0;
self.captured = false;
self.captured_frames = 0;
self.read_frame = 0;
self.recompute_buf_len();
}
_ => {
self.recompute_buf_len();
if self.captured {
self.buf_len_frames = self.buf_len_frames.min(self.captured_frames);
if self.buf_len_frames > 0 {
self.read_frame %= self.buf_len_frames;
}
}
}
}
}
fn recompute_buf_len(&mut self) {
let frames = ((60.0 / self.bpm) * self.beat_value * self.sample_rate) as usize;
self.buf_len_frames = frames.clamp(1, self.max_frames);
}
pub fn process_frame(&mut self, live: [f32; 2]) -> [f32; 2] {
if !self.enabled {
return live;
}
let depth = self.depth_smooth.process(self.depth_target) as f32;
if !self.captured {
if self.write_pos < self.buf_len_frames {
self.buffer[self.write_pos] = live;
self.write_pos += 1;
self.captured_frames = self.write_pos;
}
if self.write_pos >= self.buf_len_frames {
self.captured = true;
self.read_frame = 0;
}
return live;
}
let loop_frame = if self.buf_len_frames == 0 {
live
} else {
let buf_pos = self.read_frame % self.buf_len_frames;
match self.mode {
RollMode::RevRoll => {
let rev_pos = self.buf_len_frames.saturating_sub(1) - buf_pos;
self.buffer[rev_pos]
}
_ => self.buffer[buf_pos],
}
};
if self.buf_len_frames > 0 {
self.read_frame = (self.read_frame + 1) % self.buf_len_frames;
}
match self.mode {
RollMode::Helix => {
[
(live[0] + loop_frame[0] * depth).clamp(-1.0, 1.0),
(live[1] + loop_frame[1] * depth).clamp(-1.0, 1.0),
]
}
_ => {
let dry = 1.0 - depth;
[
loop_frame[0] * depth + live[0] * dry,
loop_frame[1] * depth + live[1] * dry,
]
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f64 = 44100.0;
fn armed_roll(mode: RollMode, beat_value: f64, depth: f64) -> RollFx {
let mut r = RollFx::new(SR);
r.set_mode(mode);
r.set_bpm(120.0);
r.set_beat_value(beat_value);
r.set_depth(depth);
r.set_enabled(true);
r
}
fn buf_len_frames(bpm: f64, beat_value: f64, sr: f64) -> usize {
((60.0 / bpm) * beat_value * sr) as usize
}
fn fill(roll: &mut RollFx, frames: usize, value: f32) {
for _ in 0..frames {
roll.process_frame([value, value]);
}
}
#[test]
fn passes_through_during_capture() {
let mut r = armed_roll(RollMode::Roll, 0.25, 1.0);
let out = r.process_frame([0.5, 0.5]);
assert_eq!(out, [0.5, 0.5], "should pass through during capture");
}
#[test]
fn loops_after_capture() {
let n = buf_len_frames(120.0, 0.25, SR);
let mut r = armed_roll(RollMode::Roll, 0.25, 1.0);
fill(&mut r, n, 0.8);
let out = r.process_frame([0.0, 0.0]); assert!(
(out[0] - 0.8).abs() < 1e-5,
"expected loop frame 0.8, got {}", out[0]
);
}
#[test]
fn disabled_passes_through() {
let mut r = RollFx::new(SR);
r.set_depth(1.0);
let out = r.process_frame([0.7, 0.3]);
assert_eq!(out, [0.7, 0.3]);
}
#[test]
fn slip_roll_recaptures_on_beat_value_change() {
let n = buf_len_frames(120.0, 0.25, SR);
let mut r = armed_roll(RollMode::SlipRoll, 0.25, 1.0);
fill(&mut r, n, 0.5);
assert!(r.captured, "should be captured after filling buffer");
r.set_beat_value(0.5);
assert!(!r.captured, "slip roll must re-capture on beat_value change");
assert_eq!(r.write_pos, 0);
}
#[test]
fn roll_does_not_recapture_on_beat_value_change() {
let n = buf_len_frames(120.0, 0.5, SR);
let mut r = armed_roll(RollMode::Roll, 0.5, 1.0);
fill(&mut r, n, 0.5);
assert!(r.captured);
r.set_beat_value(0.25);
assert!(r.captured, "roll must not recapture on beat_value change");
}
#[test]
fn rev_roll_reads_backward() {
let n = buf_len_frames(120.0, 0.25, SR);
let mut r = armed_roll(RollMode::RevRoll, 0.25, 1.0);
for i in 0..n {
let v = ((i as f32 + 1.0) * 0.001).min(1.0);
r.process_frame([v, v]);
}
let last_captured_val = ((n as f32) * 0.001).min(1.0);
let out = r.process_frame([0.0, 0.0]);
assert!(
(out[0] - last_captured_val).abs() < 1e-4,
"rev roll frame 0 should be buffer[n-1]={last_captured_val:.4}, got {:.4}", out[0]
);
}
#[test]
fn helix_adds_loop_to_live() {
let n = buf_len_frames(120.0, 0.25, SR);
let mut r = armed_roll(RollMode::Helix, 0.25, 1.0);
fill(&mut r, n, 0.3);
let out = r.process_frame([0.1, 0.1]); assert!(
out[0] > 0.1,
"helix should add loop on top of live; live=0.1, got {}", out[0]
);
}
#[test]
fn roll_shrinks_loop_window_on_beat_value_change() {
let n = buf_len_frames(120.0, 1.0, SR);
let mut r = armed_roll(RollMode::Roll, 1.0, 1.0);
fill(&mut r, n, 0.5);
assert!(r.captured);
let original_captured = r.captured_frames;
r.set_beat_value(0.5);
let half_n = buf_len_frames(120.0, 0.5, SR);
assert_eq!(r.buf_len_frames, half_n.min(original_captured));
assert!(r.captured, "captured flag must stay true");
}
#[test]
fn re_arm_resets_capture() {
let n = buf_len_frames(120.0, 0.25, SR);
let mut r = armed_roll(RollMode::Roll, 0.25, 1.0);
fill(&mut r, n, 0.5);
assert!(r.captured);
r.set_enabled(false);
r.set_enabled(true);
assert!(!r.captured, "re-arm must reset capture");
assert_eq!(r.write_pos, 0);
}
}