//! ambient - an eno-inspired generative piece //! //! a constant tonal center with auxiliary voices that fade in //! and out on long, incommensurable cycles. const std = @import("std"); const noise = @import("noise"); const sample_rate: u32 = 48000; const duration: u32 = 63; // ~1 minute const num_samples: u32 = sample_rate * duration; const sr_f: f32 = @floatFromInt(sample_rate); // voice with its own presence cycle (fades in and out over time) const Voice = struct { freq: f32, amplitude: f32, cutoff: f32, // cycle: how long before this voice completes one in/out fade (seconds) // using prime numbers so voices never sync cycle_period: f32, // offset: where in the cycle this voice starts (0-1) cycle_offset: f32, // constant: if true, always present (the tonal center) constant: bool = false, phase: f32 = 0, filter_state: noise.State = .{}, filter_coeff: noise.Coefficients = undefined, fn init(self: *Voice) void { self.filter_coeff = noise.biquad.lowpass(self.cutoff, 0.5, sr_f); } fn presence(self: *const Voice, sample_idx: u32) f32 { if (self.constant) return 1.0; // where are we in this voice's cycle? (0 to 1) const t: f32 = @floatFromInt(sample_idx); const cycle_samples = self.cycle_period * sr_f; const pos = @mod(t / cycle_samples + self.cycle_offset, 1.0); // smooth fade: sin^2 gives nice ease in/out // pos 0->0.5 fades in, 0.5->1.0 fades out const angle = pos * std.math.pi; const envelope = @sin(angle); return envelope * envelope; } fn step(self: *Voice, sample_idx: u32) f32 { // oscillator self.phase += 2.0 * std.math.pi * self.freq / sr_f; if (self.phase > 2.0 * std.math.pi) self.phase -= 2.0 * std.math.pi; var sample = @sin(self.phase); // filter sample = noise.biquad.step(self.filter_coeff, &self.filter_state, sample); // apply presence envelope and amplitude return sample * self.amplitude * self.presence(sample_idx); } }; pub fn main() !void { var voices = [_]Voice{ // C2 - the constant tonal center, always present .{ .freq = 65.41, .amplitude = 0.25, .cutoff = 180, .cycle_period = 1, .cycle_offset = 0, .constant = true }, // auxiliary voices that come and go // G2 - fifth, 17 second cycle .{ .freq = 98.0, .amplitude = 0.15, .cutoff = 280, .cycle_period = 17, .cycle_offset = 0.0 }, // C3 - octave, 23 second cycle .{ .freq = 130.81, .amplitude = 0.12, .cutoff = 350, .cycle_period = 23, .cycle_offset = 0.3 }, // E3 - major third, 19 second cycle .{ .freq = 164.81, .amplitude = 0.09, .cutoff = 450, .cycle_period = 19, .cycle_offset = 0.5 }, // G3 - fifth up high, 29 second cycle .{ .freq = 196.0, .amplitude = 0.06, .cutoff = 550, .cycle_period = 29, .cycle_offset = 0.7 }, // B3 - major seventh, rare visitor, 31 second cycle .{ .freq = 246.94, .amplitude = 0.04, .cutoff = 600, .cycle_period = 31, .cycle_offset = 0.2 }, }; for (&voices) |*v| v.init(); const file = try std.fs.cwd().createFile("ambient.wav", .{}); defer file.close(); const header = noise.WavHeader.init(num_samples, sample_rate, 1); try file.writeAll(header.asBytes()); for (0..num_samples) |i| { const idx: u32 = @intCast(i); var sample: f32 = 0; for (&voices) |*v| { sample += v.step(idx); } // master fade in/out const t: f32 = @floatFromInt(i); const total: f32 = @floatFromInt(num_samples); const fade: f32 = 3.0 * sr_f; if (t < fade) { sample *= t / fade; } else if (t > total - fade) { sample *= (total - t) / fade; } try file.writeAll(std.mem.asBytes(&sample)); } }