this repo has no description
at main 113 lines 3.9 kB view raw
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}