#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
use rand::Rng;
#[derive(Debug, Clone)]
pub struct GlitchConfig {
pub glitch_probability: f32,
pub hold_duration: usize,
pub bit_crush_amount: u8,
pub dropout_probability: f32,
}
impl Default for GlitchConfig {
fn default() -> Self {
Self {
glitch_probability: 0.01,
hold_duration: 512,
bit_crush_amount: 0,
dropout_probability: 0.0,
}
}
}
#[derive(Debug)]
pub struct GlitchEngine {
config: GlitchConfig,
held_sample: f32,
hold_counter: usize,
rng: rand::prelude::ThreadRng,
}
impl GlitchEngine {
#[must_use]
pub fn new(config: GlitchConfig) -> Self {
Self {
config,
held_sample: 0.0,
hold_counter: 0,
rng: rand::rng(),
}
}
pub fn process_sample(&mut self, input: f32) -> f32 {
if self.config.dropout_probability > 0.0
&& self.rng.random::<f32>() < self.config.dropout_probability
{
return 0.0;
}
if self.hold_counter > 0 {
self.hold_counter -= 1;
return bit_crush(self.held_sample, self.config.bit_crush_amount);
}
if self.config.glitch_probability > 0.0
&& self.rng.random::<f32>() < self.config.glitch_probability
{
self.held_sample = input;
self.hold_counter = self.config.hold_duration;
}
bit_crush(input, self.config.bit_crush_amount)
}
pub fn process(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.process_sample(*sample);
}
}
pub fn reset(&mut self) {
self.hold_counter = 0;
self.held_sample = 0.0;
}
pub fn set_config(&mut self, config: GlitchConfig) {
self.config = config;
}
}
#[must_use]
pub fn bit_crush(sample: f32, amount: u8) -> f32 {
if amount == 0 {
return sample;
}
let steps = (1u32 << (16u32.saturating_sub(u32::from(amount)))) as f32;
let scaled = sample * steps;
scaled.round() / steps
}
pub fn stutter(buffer: &mut [f32], segment_len: usize, repeats: usize) {
if segment_len == 0 || repeats == 0 {
return;
}
let mut i = 0;
while i + segment_len <= buffer.len() {
let end = (i + segment_len * (repeats + 1)).min(buffer.len());
for j in i + segment_len..end {
let src = i + (j - (i + segment_len)) % segment_len;
buffer[j] = buffer[src];
}
i = end;
}
}
pub fn reverse_segment(buffer: &mut [f32], offset: usize, len: usize) {
let end = (offset + len).min(buffer.len());
if offset >= end {
return;
}
buffer[offset..end].reverse();
}
pub fn random_sample_hold(buffer: &mut [f32], probability: f32) {
let mut rng = rand::rng();
let mut held = 0.0_f32;
for sample in buffer.iter_mut() {
if rng.random::<f32>() < probability {
*sample = held;
} else {
held = *sample;
}
}
}
pub fn zero_crossing_hold(buffer: &mut [f32], hold_len: usize) {
let mut hold_value = 0.0_f32;
let mut counter = 0usize;
let mut prev = 0.0_f32;
for sample in buffer.iter_mut() {
let current = *sample;
if prev * current < 0.0 {
hold_value = *sample;
counter = hold_len;
}
if counter > 0 {
*sample = hold_value;
counter -= 1;
}
prev = current;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bit_crush_zero_passthrough() {
let sample = 0.7654_f32;
assert!((bit_crush(sample, 0) - sample).abs() < 1e-6);
}
#[test]
fn test_bit_crush_reduces_precision() {
let sample = 0.123456789_f32;
let crushed = bit_crush(sample, 8);
assert!((crushed - sample).abs() < 0.01);
assert!((crushed - sample).abs() > 0.0);
}
#[test]
fn test_bit_crush_max_amount() {
let sample = 0.4_f32;
let crushed = bit_crush(sample, 16);
assert!(crushed == 0.0 || crushed == 1.0);
}
#[test]
fn test_bit_crush_negative_sample() {
let sample = -0.5_f32;
let crushed = bit_crush(sample, 4);
assert!(crushed >= -1.0 && crushed <= 1.0);
}
#[test]
fn test_stutter_repeats() {
let mut buf = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
stutter(&mut buf, 2, 2);
assert_eq!(buf[2], 1.0);
assert_eq!(buf[3], 2.0);
assert_eq!(buf[4], 1.0);
assert_eq!(buf[5], 2.0);
}
#[test]
fn test_stutter_zero_repeats_noop() {
let orig = vec![1.0, 2.0, 3.0];
let mut buf = orig.clone();
stutter(&mut buf, 2, 0);
assert_eq!(buf, orig);
}
#[test]
fn test_reverse_segment() {
let mut buf = vec![1.0, 2.0, 3.0, 4.0, 5.0];
reverse_segment(&mut buf, 1, 3);
assert_eq!(buf, vec![1.0, 4.0, 3.0, 2.0, 5.0]);
}
#[test]
fn test_reverse_segment_out_of_bounds() {
let mut buf = vec![1.0, 2.0, 3.0];
reverse_segment(&mut buf, 1, 100);
assert_eq!(buf[0], 1.0); }
#[test]
fn test_random_sample_hold_probability_zero() {
let orig = vec![0.1, 0.2, 0.3, 0.4];
let mut buf = orig.clone();
random_sample_hold(&mut buf, 0.0);
assert_eq!(buf, orig);
}
#[test]
fn test_random_sample_hold_probability_one() {
let mut buf = vec![0.1, 0.5, 0.9, 0.3];
random_sample_hold(&mut buf, 1.0);
for v in &buf {
assert!(*v == 0.0 || *v == 0.1); }
}
#[test]
fn test_glitch_engine_reset() {
let config = GlitchConfig {
glitch_probability: 1.0,
hold_duration: 10,
..Default::default()
};
let mut engine = GlitchEngine::new(config);
engine.process_sample(0.5);
engine.reset();
assert_eq!(engine.hold_counter, 0);
assert_eq!(engine.held_sample, 0.0);
}
#[test]
fn test_glitch_engine_no_glitch() {
let config = GlitchConfig {
glitch_probability: 0.0,
hold_duration: 100,
bit_crush_amount: 0,
dropout_probability: 0.0,
};
let mut engine = GlitchEngine::new(config);
let result = engine.process_sample(0.42);
assert!((result - 0.42).abs() < 1e-6);
}
#[test]
fn test_glitch_engine_dropout_silences() {
let config = GlitchConfig {
dropout_probability: 1.0,
..Default::default()
};
let mut engine = GlitchEngine::new(config);
let result = engine.process_sample(0.99);
assert_eq!(result, 0.0);
}
#[test]
fn test_zero_crossing_hold_flat_signal() {
let mut buf = vec![0.5_f32; 10];
zero_crossing_hold(&mut buf, 3);
for v in &buf {
assert!((v - 0.5).abs() < 1e-6);
}
}
#[test]
fn test_zero_crossing_hold_detects_crossing() {
let mut buf = vec![0.5, -0.5, 0.3, 0.3, 0.3];
zero_crossing_hold(&mut buf, 2);
assert!(buf[1] == -0.5 || buf[2] == -0.5);
}
}