this repo has no description
at main 120 lines 3.2 kB view raw
1//! low frequency oscillator - generates control signals for modulation. 2//! 3//! uses a 32-bit phase accumulator with overflow for efficient cycling. 4//! the top 2 bits indicate the quarter (0-3), which determines direction 5//! and sign of the waveform. 6//! 7//! inspired by torvalds/AudioNoise 8 9const std = @import("std"); 10 11pub const Waveform = enum { 12 sine, 13 triangle, 14 sawtooth, 15}; 16 17/// lfo state - call step() once per sample 18pub const Lfo = struct { 19 phase: u32 = 0, 20 step_size: u32, 21 22 const two_pow_32: f64 = 4294967296.0; 23 24 /// create an lfo at the given frequency 25 pub fn init(freq: f32, sample_rate: f32) Lfo { 26 const step_size = @as(u32, @intFromFloat(freq * two_pow_32 / sample_rate)); 27 return .{ .step_size = step_size }; 28 } 29 30 /// set frequency (can be changed dynamically) 31 pub fn setFreq(self: *Lfo, freq: f32, sample_rate: f32) void { 32 self.step_size = @as(u32, @intFromFloat(freq * two_pow_32 / sample_rate)); 33 } 34 35 /// advance and return value in range [-1, 1] 36 pub fn step(self: *Lfo, waveform: Waveform) f32 { 37 const now = self.phase; 38 self.phase +%= self.step_size; // wrapping add 39 40 if (waveform == .sawtooth) { 41 return uintToFraction(now) * 2.0 - 1.0; 42 } 43 44 const quarter = now >> 30; // top 2 bits 45 var pos = now << 2; // remaining 30 bits scaled to full range 46 47 // quarters 1 and 3 reverse direction 48 if (quarter & 1 != 0) { 49 pos = ~pos; 50 } 51 52 var val: f32 = undefined; 53 if (waveform == .sine) { 54 // approximate sine using parabola 55 val = uintToFraction(pos); 56 val = 4.0 * val * (1.0 - val); // parabolic approximation 57 } else { 58 // triangle 59 val = uintToFraction(pos); 60 } 61 62 // quarters 2 and 3 are negative 63 if (quarter & 2 != 0) { 64 val = -val; 65 } 66 67 return val; 68 } 69 70 fn uintToFraction(x: u32) f32 { 71 return @as(f32, @floatFromInt(x)) / @as(f32, two_pow_32); 72 } 73}; 74 75// tests 76 77test "lfo sine stays in range" { 78 var lfo = Lfo.init(1.0, 48000); 79 80 for (0..96000) |_| { 81 const val = lfo.step(.sine); 82 try std.testing.expect(val >= -1.0 and val <= 1.0); 83 } 84} 85 86test "lfo triangle stays in range" { 87 var lfo = Lfo.init(1.0, 48000); 88 89 for (0..96000) |_| { 90 const val = lfo.step(.triangle); 91 try std.testing.expect(val >= -1.0 and val <= 1.0); 92 } 93} 94 95test "lfo sawtooth stays in range" { 96 var lfo = Lfo.init(1.0, 48000); 97 98 for (0..96000) |_| { 99 const val = lfo.step(.sawtooth); 100 try std.testing.expect(val >= -1.0 and val <= 1.0); 101 } 102} 103 104test "lfo completes one cycle" { 105 var lfo = Lfo.init(1.0, 1000); // 1Hz at 1000 samples/sec = 1000 samples per cycle 106 107 var crossed_zero_count: u32 = 0; 108 var prev: f32 = 0; 109 110 for (0..1000) |_| { 111 const val = lfo.step(.sine); 112 if (prev < 0 and val >= 0) { 113 crossed_zero_count += 1; 114 } 115 prev = val; 116 } 117 118 // should cross zero rising once per cycle 119 try std.testing.expect(crossed_zero_count == 1); 120}