this repo has no description
1//! ambient - an eno-inspired generative piece
2//!
3//! a constant tonal center with auxiliary voices that fade in
4//! and out on long, incommensurable cycles.
5
6const std = @import("std");
7const noise = @import("noise");
8
9const sample_rate: u32 = 48000;
10const duration: u32 = 63; // ~1 minute
11const num_samples: u32 = sample_rate * duration;
12const sr_f: f32 = @floatFromInt(sample_rate);
13
14// voice with its own presence cycle (fades in and out over time)
15const Voice = struct {
16 freq: f32,
17 amplitude: f32,
18 cutoff: f32,
19
20 // cycle: how long before this voice completes one in/out fade (seconds)
21 // using prime numbers so voices never sync
22 cycle_period: f32,
23 // offset: where in the cycle this voice starts (0-1)
24 cycle_offset: f32,
25 // constant: if true, always present (the tonal center)
26 constant: bool = false,
27
28 phase: f32 = 0,
29 filter_state: noise.State = .{},
30 filter_coeff: noise.Coefficients = undefined,
31
32 fn init(self: *Voice) void {
33 self.filter_coeff = noise.biquad.lowpass(self.cutoff, 0.5, sr_f);
34 }
35
36 fn presence(self: *const Voice, sample_idx: u32) f32 {
37 if (self.constant) return 1.0;
38
39 // where are we in this voice's cycle? (0 to 1)
40 const t: f32 = @floatFromInt(sample_idx);
41 const cycle_samples = self.cycle_period * sr_f;
42 const pos = @mod(t / cycle_samples + self.cycle_offset, 1.0);
43
44 // smooth fade: sin^2 gives nice ease in/out
45 // pos 0->0.5 fades in, 0.5->1.0 fades out
46 const angle = pos * std.math.pi;
47 const envelope = @sin(angle);
48 return envelope * envelope;
49 }
50
51 fn step(self: *Voice, sample_idx: u32) f32 {
52 // oscillator
53 self.phase += 2.0 * std.math.pi * self.freq / sr_f;
54 if (self.phase > 2.0 * std.math.pi) self.phase -= 2.0 * std.math.pi;
55
56 var sample = @sin(self.phase);
57
58 // filter
59 sample = noise.biquad.step(self.filter_coeff, &self.filter_state, sample);
60
61 // apply presence envelope and amplitude
62 return sample * self.amplitude * self.presence(sample_idx);
63 }
64};
65
66pub fn main() !void {
67 var voices = [_]Voice{
68 // C2 - the constant tonal center, always present
69 .{ .freq = 65.41, .amplitude = 0.25, .cutoff = 180, .cycle_period = 1, .cycle_offset = 0, .constant = true },
70
71 // auxiliary voices that come and go
72 // G2 - fifth, 17 second cycle
73 .{ .freq = 98.0, .amplitude = 0.15, .cutoff = 280, .cycle_period = 17, .cycle_offset = 0.0 },
74 // C3 - octave, 23 second cycle
75 .{ .freq = 130.81, .amplitude = 0.12, .cutoff = 350, .cycle_period = 23, .cycle_offset = 0.3 },
76 // E3 - major third, 19 second cycle
77 .{ .freq = 164.81, .amplitude = 0.09, .cutoff = 450, .cycle_period = 19, .cycle_offset = 0.5 },
78 // G3 - fifth up high, 29 second cycle
79 .{ .freq = 196.0, .amplitude = 0.06, .cutoff = 550, .cycle_period = 29, .cycle_offset = 0.7 },
80 // B3 - major seventh, rare visitor, 31 second cycle
81 .{ .freq = 246.94, .amplitude = 0.04, .cutoff = 600, .cycle_period = 31, .cycle_offset = 0.2 },
82 };
83
84 for (&voices) |*v| v.init();
85
86 const file = try std.fs.cwd().createFile("ambient.wav", .{});
87 defer file.close();
88
89 const header = noise.WavHeader.init(num_samples, sample_rate, 1);
90 try file.writeAll(header.asBytes());
91
92 for (0..num_samples) |i| {
93 const idx: u32 = @intCast(i);
94 var sample: f32 = 0;
95
96 for (&voices) |*v| {
97 sample += v.step(idx);
98 }
99
100 // master fade in/out
101 const t: f32 = @floatFromInt(i);
102 const total: f32 = @floatFromInt(num_samples);
103 const fade: f32 = 3.0 * sr_f;
104
105 if (t < fade) {
106 sample *= t / fade;
107 } else if (t > total - fade) {
108 sample *= (total - t) / fade;
109 }
110
111 try file.writeAll(std.mem.asBytes(&sample));
112 }
113}