use crate::AudioEffect;
#[derive(Debug, Clone)]
pub struct BitCrusherConfig {
pub bit_depth: u8,
pub downsample: u32,
}
impl Default for BitCrusherConfig {
fn default() -> Self {
Self {
bit_depth: 8,
downsample: 4,
}
}
}
pub struct BitCrusher {
config: BitCrusherConfig,
hold_sample: f32,
sample_counter: u32,
}
impl BitCrusher {
#[must_use]
pub fn new(config: BitCrusherConfig) -> Self {
Self {
config,
hold_sample: 0.0,
sample_counter: 0,
}
}
pub fn set_bit_depth(&mut self, bit_depth: u8) {
self.config.bit_depth = bit_depth.clamp(1, 16);
}
pub fn set_downsample(&mut self, downsample: u32) {
self.config.downsample = downsample.max(1);
}
}
impl AudioEffect for BitCrusher {
const EFFECT_ID: &'static str = "bit_crusher";
fn process_sample(&mut self, input: f32) -> f32 {
self.sample_counter += 1;
if self.sample_counter >= self.config.downsample {
self.sample_counter = 0;
#[allow(clippy::cast_precision_loss)]
let levels = (1 << self.config.bit_depth) as f32;
let step = 2.0 / levels;
let quantized = (input / step).round() * step;
self.hold_sample = quantized.clamp(-1.0, 1.0);
}
self.hold_sample
}
fn reset(&mut self) {
self.hold_sample = 0.0;
self.sample_counter = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bitcrusher() {
let config = BitCrusherConfig::default();
let mut crusher = BitCrusher::new(config);
for _ in 0..100 {
let output = crusher.process_sample(0.5);
assert!(output.is_finite());
assert!(output.abs() <= 1.0);
}
}
#[test]
fn test_bitcrusher_bit_depth() {
let config = BitCrusherConfig {
bit_depth: 2,
downsample: 1,
};
let mut crusher = BitCrusher::new(config);
let output = crusher.process_sample(0.75);
assert!(output.is_finite());
}
}