this repo has no description
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}