this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add ambient composition

eno-inspired generative piece with constant tonal center (C2) and
auxiliary voices that fade in/out on prime-number cycles (17, 19,
23, 29, 31 seconds). voices never sync - patterns emerge and dissolve.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

zzstoatzz 91b84374 f2b7ae14

+142 -2
+13 -2
README.md
··· 16 16 17 17 - **biquad** - second-order IIR filter (lowpass, highpass, bandpass, notch, allpass) 18 18 - **lfo** - low frequency oscillator (sine, triangle, sawtooth) 19 + - **wav** - wav file writer (44-byte header + samples, no ffmpeg) 20 + 21 + ## compositions 22 + 23 + ### ambient 24 + 25 + ```bash 26 + zig build ambient # generates ambient.wav 27 + ``` 28 + 29 + an eno-inspired generative piece. a low C2 drone serves as the constant tonal center, while auxiliary voices (fifth, octave, major third, major seventh) fade in and out on their own long cycles. each voice has a prime-number period (17, 19, 23, 29, 31 seconds) so they never sync up - sometimes you hear just the drone, sometimes several voices bloom together, never the same combination twice. 19 30 20 31 ## usage 21 32 ··· 36 47 ## example 37 48 38 49 ```bash 39 - zig build run 2>/dev/null | ffplay -f f32le -ar 48000 -ac 1 - 50 + zig build run # generates output.wav 40 51 ``` 41 52 42 - generates a 440Hz sine with lowpass filtering and tremolo. 53 + a 440Hz sine with lowpass filtering and tremolo. demonstrates basic synthesis pipeline. 43 54 44 55 ## install 45 56
+16
build.zig
··· 31 31 const run_example = b.addRunArtifact(example); 32 32 const run_step = b.step("run", "run example"); 33 33 run_step.dependOn(&run_example.step); 34 + 35 + // ambient composition 36 + const ambient = b.addExecutable(.{ 37 + .name = "ambient", 38 + .root_module = b.createModule(.{ 39 + .root_source_file = b.path("src/ambient.zig"), 40 + .target = target, 41 + .optimize = optimize, 42 + .imports = &.{.{ .name = "noise", .module = mod }}, 43 + }), 44 + }); 45 + b.installArtifact(ambient); 46 + 47 + const run_ambient = b.addRunArtifact(ambient); 48 + const ambient_step = b.step("ambient", "generate ambient piece"); 49 + ambient_step.dependOn(&run_ambient.step); 34 50 }
+113
src/ambient.zig
··· 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 + 6 + const std = @import("std"); 7 + const noise = @import("noise"); 8 + 9 + const sample_rate: u32 = 48000; 10 + const duration: u32 = 63; // ~1 minute 11 + const num_samples: u32 = sample_rate * duration; 12 + const sr_f: f32 = @floatFromInt(sample_rate); 13 + 14 + // voice with its own presence cycle (fades in and out over time) 15 + const 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 + 66 + pub 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 + }