Skip to main content

rill_analog_effects/
op_amp.rs

1/// Model of an operational amplifier with slew-rate limiting,
2/// bandwidth roll-off, and voltage rail clamping.
3#[derive(Debug, Clone)]
4pub struct OperationalAmplifier {
5    gain: f64,
6    slew_rate: f64,
7    bandwidth: f64,
8    voltage_rails: (f64, f64),
9    output_voltage: f64,
10    internal_state: f64,
11}
12
13impl OperationalAmplifier {
14    /// Create a new op-amp model.
15    ///
16    /// * `gain` — open-loop gain (V/V)
17    /// * `slew_rate` — slew rate in V/µs
18    /// * `bandwidth` — gain-bandwidth product (Hz)
19    pub fn new(gain: f64, slew_rate: f64, bandwidth: f64) -> Self {
20        Self {
21            gain,
22            slew_rate: slew_rate * 1e6,
23            bandwidth,
24            voltage_rails: (-15.0, 15.0),
25            output_voltage: 0.0,
26            internal_state: 0.0,
27        }
28    }
29
30    /// Set positive and negative voltage rails
31    pub fn set_voltage_rails(&mut self, negative: f64, positive: f64) {
32        self.voltage_rails = (negative, positive);
33    }
34
35    /// Process one sample.
36    ///
37    /// * `input` — differential input voltage
38    /// * `dt` — sample period (seconds)
39    pub fn process(&mut self, input: f64, dt: f64) -> f64 {
40        let ideal_output = input * self.gain;
41
42        let max_change = self.slew_rate * dt;
43        let output_change = ideal_output - self.internal_state;
44        let limited_change = output_change.clamp(-max_change, max_change);
45        self.internal_state += limited_change;
46
47        let pole_frequency = self.bandwidth / self.gain;
48        let alpha = 1.0 / (1.0 + 2.0 * std::f64::consts::PI * pole_frequency * dt);
49        self.output_voltage = alpha * self.internal_state + (1.0 - alpha) * ideal_output;
50
51        self.output_voltage = self
52            .output_voltage
53            .clamp(self.voltage_rails.0, self.voltage_rails.1);
54
55        self.output_voltage
56    }
57
58    /// Reset internal state
59    pub fn reset(&mut self) {
60        self.output_voltage = 0.0;
61        self.internal_state = 0.0;
62    }
63
64    /// Current output voltage
65    pub fn output_voltage(&self) -> f64 {
66        self.output_voltage
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_op_amp_dc_gain() {
76        let mut op = OperationalAmplifier::new(10.0, 0.5, 1e6);
77        let output = op.process(0.5, 1.0 / 44100.0);
78        assert!(output > 0.0);
79    }
80
81    #[test]
82    fn test_op_amp_rail_clamp() {
83        let mut op = OperationalAmplifier::new(100.0, 100.0, 1e6);
84        let output = op.process(1.0, 1.0 / 44100.0);
85        assert!(output <= 15.0);
86        assert!(output >= -15.0);
87    }
88
89    #[test]
90    fn test_op_amp_reset() {
91        let mut op = OperationalAmplifier::new(10.0, 0.5, 1e6);
92        op.process(0.5, 1.0 / 44100.0);
93        op.reset();
94        assert_eq!(op.output_voltage(), 0.0);
95    }
96}