From d58d0d3b099543c10d48337b8fcedbfd06164c18 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Sat, 21 Mar 2026 08:47:22 +0000 Subject: [PATCH] Initial release: procedural techno music generator Synthesizes kick, snare, hi-hat, bass, and acid lead from scratch. 32-bar song structure with filter sweeps and pattern variation. Outputs raw float32 stereo at 44.1 kHz to stdout. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 12 ++ README.md | 69 +++++++++ techno-gen.c | 428 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 509 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 techno-gen.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ecfa71b --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +CC = gcc +CFLAGS = -O2 -Wall -Wextra +LDFLAGS = -lm +TARGET = techno-gen + +$(TARGET): $(TARGET).c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +clean: + rm -f $(TARGET) + +.PHONY: clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..950d658 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# techno-gen + +Generates infinite procedural techno music as raw float32 stereo audio on stdout. +138 BPM. No samples — everything is synthesized from scratch. + +## Build + +```sh +make +``` + +Or manually: + +```sh +gcc -O2 -Wall -lm -o techno-gen techno-gen.c +``` + +## Play + +Pipe directly into `paplay` (PulseAudio): + +```sh +./techno-gen | paplay --format=float32le --rate=44100 --channels=2 +``` + +> **Note:** `paplay` is part of the `pulseaudio-utils` package. +> On Debian/Ubuntu: `sudo apt install pulseaudio-utils` + +### Mono version (if you only want 1 channel) + +The program outputs stereo (2 channels). If you want mono, mix down with sox: + +```sh +./techno-gen | sox -t raw -r 44100 -e float -b 32 -c 2 - -t raw -r 44100 -e float -b 32 -c 1 - | paplay --format=float32le --rate=44100 --channels=1 +``` + +### Record to file + +```sh +./techno-gen | sox -t raw -r 44100 -e float -b 32 -c 2 - output.wav trim 0 120 +``` + +This records 2 minutes (`trim 0 120`) to `output.wav`. + +## What it sounds like + +| Time | Section | +|---------------|----------------------------------------------------| +| 0–3 bars | **Intro** — kick + bass, filter closed | +| 4–7 bars | **Rise** — hi-hats join, filter opens | +| 8–15 bars | **Full** — snare, acid lead, LFO filter sweeps | +| 16–19 bars | **Breakdown** — bass only, stripped down | +| 20–23 bars | **Build** — kick returns, filter climbs | +| 24–31 bars | **Heavy** — syncopated kick, acid variation 2 | +| *then loops* | Back to bar 8 (never the sparse intro again) | + +The cycle is 32 bars (~1 min 23 sec at 138 BPM), then repeats with continuous filter and pattern variation. + +## Synthesis + +All audio is synthesized in real time: + +- **Kick** — sine oscillator with pitch envelope (200 → 45 Hz), amp decay, transient click +- **Snare** — high-pass filtered noise blended with a 185 Hz tone +- **Hi-hat** — noise through a 7 kHz high-pass; open/closed variants with different decay times +- **Bass** — sawtooth oscillator through a resonant (Q=4.5) biquad low-pass with per-note filter envelope and glide +- **Acid lead** — dual slightly-detuned sawtooth through a very resonant (Q=6) low-pass; filter sweeps with LFO per section +- **Stereo width** — short delay line (~11 ms) ping-ponged between channels on hats and acid +- **Master** — `tanh` soft saturation to keep levels honest diff --git a/techno-gen.c b/techno-gen.c new file mode 100644 index 0000000..149ca56 --- /dev/null +++ b/techno-gen.c @@ -0,0 +1,428 @@ +/* + * techno-gen — generates infinite techno music as raw float32 stereo audio + * + * Build: gcc -O2 -lm -o techno-gen techno-gen.c + * Play: ./techno-gen | paplay --format=float32le --rate=44100 --channels=2 + */ + +#include +#include +#include +#include +#include + +#define SR 44100 +#define BPM 138.0 +#define TWO_PI (2.0 * M_PI) +#define SPB ((int)(SR * 60.0 / BPM)) /* samples per beat (quarter note) */ +#define SP16 (SPB / 4) /* samples per 16th note */ + +static inline double freq_of(double root, int semitones) { + return root * pow(2.0, semitones / 12.0); +} + +/* ── PRNG ── */ +static uint32_t rng_state = 0xdeadbeef; +static double randf(void) { + rng_state ^= rng_state << 13; + rng_state ^= rng_state >> 17; + rng_state ^= rng_state << 5; + return (double)(int32_t)rng_state * (1.0 / 2147483648.0); +} + +/* ── Biquad filter ── */ +typedef struct { double b0, b1, b2, a1, a2, x1, x2, y1, y2; } Bq; + +static void bq_lp(Bq *f, double fc, double q) { + double w = TWO_PI * fc / SR; + double alpha = sin(w) / (2.0 * q); + double c = cos(w), a0 = 1.0 + alpha; + f->b0 = (1.0 - c) * 0.5 / a0; + f->b1 = (1.0 - c) / a0; + f->b2 = f->b0; + f->a1 = -2.0 * c / a0; + f->a2 = (1.0 - alpha) / a0; +} + +static void bq_hp(Bq *f, double fc, double q) { + double w = TWO_PI * fc / SR; + double alpha = sin(w) / (2.0 * q); + double c = cos(w), a0 = 1.0 + alpha; + f->b0 = (1.0 + c) * 0.5 / a0; + f->b1 = -(1.0 + c) / a0; + f->b2 = f->b0; + f->a1 = -2.0 * c / a0; + f->a2 = (1.0 - alpha) / a0; +} + +static double bq_run(Bq *f, double x) { + double y = f->b0*x + f->b1*f->x1 + f->b2*f->x2 + - f->a1*f->y1 - f->a2*f->y2; + f->x2 = f->x1; f->x1 = x; + f->y2 = f->y1; f->y1 = y; + return y; +} + +/* ── Kick drum ── + * Sine wave with pitch drop (200 → 45 Hz) and amp decay */ +typedef struct { double phase, amp, pitch; } Kick; + +static void kick_trig(Kick *k) { + k->phase = 0.0; k->amp = 1.0; k->pitch = 1.0; +} + +static double kick_tick(Kick *k) { + if (k->amp < 0.001) { return 0.0; } + double freq = 45.0 + 175.0 * k->pitch; + k->phase += TWO_PI * freq / SR; + if (k->phase > TWO_PI) { k->phase -= TWO_PI; } + double click = (k->amp > 0.97) ? randf() * 0.25 : 0.0; + double s = (sin(k->phase) * 0.95 + click) * k->amp; + k->amp *= 0.9997; + k->pitch *= 0.9982; + return s * 0.85; +} + +/* ── Snare/clap ── + * Noise burst + low-tone blend */ +typedef struct { double env, tone_phase; Bq hp; } Snare; + +static void snare_trig(Snare *s) { + s->env = 1.0; s->tone_phase = 0.0; +} + +static double snare_tick(Snare *s) { + if (s->env < 0.001) { return 0.0; } + s->tone_phase += TWO_PI * 185.0 / SR; + if (s->tone_phase > TWO_PI) { s->tone_phase -= TWO_PI; } + double noise = bq_run(&s->hp, randf()); + double out = (noise * 0.70 + sin(s->tone_phase) * 0.30) * s->env; + s->env *= 0.9935; + return out * 0.55; +} + +/* ── Hi-hat ── + * Bandpass-ish noise, two modes: closed / open */ +typedef struct { double env, decay; Bq hp; } Hat; + +static void hat_trig(Hat *h, int open) { + h->env = 1.0; + h->decay = open ? 0.9993 : 0.9958; +} + +static double hat_tick(Hat *h) { + if (h->env < 0.001) { return 0.0; } + double out = bq_run(&h->hp, randf()) * h->env; + h->env *= h->decay; + return out * 0.38; +} + +/* ── Bass synth ── + * Sawtooth through resonant low-pass with filter envelope */ +typedef struct { double phase, freq, tgt, amp_env, flt_env; Bq lpf; } Bass; + +static void bass_note(Bass *b, double freq) { + b->tgt = freq; + b->flt_env = 1.0; + b->amp_env = 1.0; +} + +static double bass_tick(Bass *b, double cutoff_base) { + b->freq += (b->tgt - b->freq) * 0.06; + b->phase += TWO_PI * b->freq / SR; + if (b->phase > TWO_PI) { b->phase -= TWO_PI; } + double saw = (b->phase / M_PI) - 1.0; + double fc = cutoff_base + 3500.0 * b->flt_env; + if (fc < 60.0) { fc = 60.0; } + if (fc > 18000.0) { fc = 18000.0; } + bq_lp(&b->lpf, fc, 4.5); + double out = bq_run(&b->lpf, saw) * b->amp_env; + b->flt_env *= 0.9991; + b->amp_env *= 0.9999; + return out * 0.55; +} + +/* ── Acid lead ── + * Sawtooth through very resonant LP, slightly detuned for bite */ +typedef struct { double phase, phase2, freq, tgt, amp_env, flt_env; Bq lpf; } Acid; + +static void acid_note(Acid *a, double freq) { + a->tgt = freq; + a->flt_env = 1.0; + a->amp_env = 1.0; +} + +static double acid_tick(Acid *a, double cutoff_base) { + if (a->amp_env < 0.001) { return 0.0; } + a->freq += (a->tgt - a->freq) * 0.09; + a->phase += TWO_PI * a->freq / SR; + a->phase2 += TWO_PI * (a->freq * 1.008) / SR; /* slight detune */ + if (a->phase > TWO_PI) { a->phase -= TWO_PI; } + if (a->phase2 > TWO_PI) { a->phase2 -= TWO_PI; } + double saw = ((a->phase / M_PI) - 1.0) * 0.6 + + ((a->phase2 / M_PI) - 1.0) * 0.4; + double fc = cutoff_base + 5000.0 * a->flt_env; + if (fc < 80.0) { fc = 80.0; } + if (fc > 18000.0) { fc = 18000.0; } + bq_lp(&a->lpf, fc, 6.0); + double out = bq_run(&a->lpf, saw) * a->amp_env; + a->flt_env *= 0.9992; + a->amp_env *= 0.9997; + return out * 0.32; +} + +/* ════════════════════════════════════════════════════════════ + * Patterns (16 steps per bar, -1 = rest / hold) + * Notes are semitone offsets from root A1 = 55 Hz + * ════════════════════════════════════════════════════════════ */ + +static const int BASS_PAT[4][16] = { + /* 0: driving quarter-note anchor */ + { 0,-1, 0,-1, 7,-1, 0,-1, 3,-1, 5,-1, 7,-1, 3,-1 }, + /* 1: offbeat pushes */ + { 0, 0,-1,12, 0, 7,-1, 5, 3,-1, 3, 5, 7,-1, 7, 3 }, + /* 2: dark and sparse */ + { 0,-1,-1, 0,-1, 7, 5,-1, 0,-1,-1, 3,-1, 5, 0,-1 }, + /* 3: more frantic */ + { 0, 3, 0, 7, 0,12, 7, 0, 3, 0, 5, 3, 0, 7, 5, 0 }, +}; + +static const int ACID_PAT[2][16] = { + { 12,-1,-1,14,-1,-1,12,-1,10,-1,-1,12,-1,-1,10,-1 }, + { 12,-1,15,-1,14,-1,12,10,-1,12,-1,15,17,-1,15,12 }, +}; + +/* 0=off, 1=closed, 2=open */ +static const int HAT_PAT[2][16] = { + { 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,2 }, + { 1,1,1,0, 1,1,2,0, 1,1,1,0, 1,0,2,0 }, +}; + +/* 0=off, 1=kick */ +static const int KICK_PAT[2][16] = { + { 1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0 }, /* 4-on-the-floor */ + { 1,0,0,0, 1,0,0,1, 1,0,0,0, 1,0,1,0 }, /* syncopated */ +}; + +/* ════════════════════════════════════════════════════════════ + * Section map (cycles every 32 bars): + * + * 0-3 : intro — kick + bass only, filter closed + * 4-7 : rise — add hats, filter opens + * 8-15 : full — kick + hats + snare + bass + acid, sweeps + * 16-19 : break — bass only, everything stripped + * 20-23 : build — kick returns, filter rises + * 24-31 : heavy — syncopated kick, acid pattern 2, wide sweep + * ════════════════════════════════════════════════════════════ */ + +typedef struct { + int kick_pat; + int hat_pat; + int bass_pat; + int acid_pat; + int use_hats; + int use_snare; + int use_acid; + /* cutoff_base for bass and acid as function of bar-in-section */ + double bass_fc_lo, bass_fc_hi; + double acid_fc_lo, acid_fc_hi; +} Section; + +static void get_section(int bar, Section *s) { + int b = bar % 32; + double t = 0.0; /* 0..1 within section for sweeps */ + + if (b < 4) { + t = b / 4.0; + s->kick_pat = 0; + s->hat_pat = 0; + s->bass_pat = 0; + s->acid_pat = 0; + s->use_hats = 0; + s->use_snare = 0; + s->use_acid = 0; + s->bass_fc_lo = 200.0; + s->bass_fc_hi = 200.0 + 400.0 * t; + s->acid_fc_lo = 0; + s->acid_fc_hi = 0; + } else if (b < 8) { + t = (b - 4) / 4.0; + s->kick_pat = 0; + s->hat_pat = 0; + s->bass_pat = 1; + s->acid_pat = 0; + s->use_hats = 1; + s->use_snare = 0; + s->use_acid = 0; + s->bass_fc_lo = 350.0 + 600.0 * t; + s->bass_fc_hi = s->bass_fc_lo + 400.0; + s->acid_fc_lo = 0; + s->acid_fc_hi = 0; + } else if (b < 16) { + t = (b - 8) / 8.0; + s->kick_pat = 0; + s->hat_pat = 0; + s->bass_pat = (b < 12) ? 1 : 2; + s->acid_pat = 0; + s->use_hats = 1; + s->use_snare = 1; + s->use_acid = 1; + /* sweeping filter: sine lfo over 4 bars */ + double lfo = sin(t * TWO_PI * 2.0) * 0.5 + 0.5; + s->bass_fc_lo = 500.0 + 1200.0 * lfo; + s->bass_fc_hi = s->bass_fc_lo + 600.0; + s->acid_fc_lo = 400.0 + 1800.0 * lfo; + s->acid_fc_hi = s->acid_fc_lo + 800.0; + } else if (b < 20) { + /* breakdown */ + s->kick_pat = 0; + s->hat_pat = 0; + s->bass_pat = 2; + s->acid_pat = 0; + s->use_hats = 0; + s->use_snare = 0; + s->use_acid = 0; + s->bass_fc_lo = 180.0; + s->bass_fc_hi = 240.0; + s->acid_fc_lo = 0; + s->acid_fc_hi = 0; + } else if (b < 24) { + /* build */ + t = (b - 20) / 4.0; + s->kick_pat = 0; + s->hat_pat = 1; + s->bass_pat = 3; + s->acid_pat = 0; + s->use_hats = 1; + s->use_snare = 1; + s->use_acid = (t > 0.5); + s->bass_fc_lo = 200.0 + 1200.0 * t * t; + s->bass_fc_hi = s->bass_fc_lo + 800.0; + s->acid_fc_lo = 200.0 + 2000.0 * t * t; + s->acid_fc_hi = s->acid_fc_lo + 1200.0; + } else { + /* heavy */ + t = (b - 24) / 8.0; + double lfo = sin(t * TWO_PI * 3.0) * 0.5 + 0.5; + s->kick_pat = 1; + s->hat_pat = 1; + s->bass_pat = 3; + s->acid_pat = 1; + s->use_hats = 1; + s->use_snare = 1; + s->use_acid = 1; + s->bass_fc_lo = 700.0 + 1500.0 * lfo; + s->bass_fc_hi = s->bass_fc_lo + 900.0; + s->acid_fc_lo = 600.0 + 2400.0 * lfo; + s->acid_fc_hi = s->acid_fc_lo + 1400.0; + } +} + +/* ════════════════════════════════════════════════════════════ */ + +int main(void) { + double root = 55.0; /* A1 */ + + Kick kick = {0}; + Snare snare = {0}; + Hat hat = {0}; + Bass bass = {0}; + Acid acid = {0}; + + bq_hp(&snare.hp, 280.0, 0.7); + bq_hp(&hat.hp, 7200.0, 0.9); + + bass.freq = root; + bass.tgt = root; + acid.freq = freq_of(root, 12); + acid.tgt = acid.freq; + + /* Stereo chorus/width: short delay line */ +#define DLEN 2048 + double dbuf[DLEN] = {0}; + int dpos = 0; + int dtap = (int)(0.011 * SR) % DLEN; + + int step = 0; + int bar = 0; + int sample_in_step = 0; + + /* For per-step cutoff interpolation */ + double bass_fc = 500.0; + double acid_fc = 400.0; + + while (1) { + if (sample_in_step == 0) { + Section sec; + get_section(bar, &sec); + + /* Interpolate cutoff: step position within bar 0..15 */ + double sbar_t = step / 16.0; + bass_fc = sec.bass_fc_lo + (sec.bass_fc_hi - sec.bass_fc_lo) * sbar_t; + acid_fc = sec.acid_fc_lo + (sec.acid_fc_hi - sec.acid_fc_lo) * sbar_t; + + /* Kick */ + if (KICK_PAT[sec.kick_pat][step]) { + kick_trig(&kick); + } + /* Hi-hats */ + if (sec.use_hats) { + int hv = HAT_PAT[sec.hat_pat][step]; + if (hv > 0) { hat_trig(&hat, hv == 2); } + } + /* Snare on steps 4 and 12 */ + if (sec.use_snare && (step == 4 || step == 12)) { + snare_trig(&snare); + } + /* Bass */ + { + int note = BASS_PAT[sec.bass_pat][step]; + if (note >= 0) { + bass_note(&bass, freq_of(root, note)); + } + } + /* Acid */ + if (sec.use_acid) { + int note = ACID_PAT[sec.acid_pat][step]; + if (note >= 0) { + acid_note(&acid, freq_of(root, note)); + } + } + } + + /* ── Synthesize ── */ + double k = kick_tick(&kick); + double h = hat_tick(&hat); + double sn = snare_tick(&snare); + double b = bass_tick(&bass, bass_fc); + double a = acid_tick(&acid, acid_fc); + + /* Stereo mix: slight panning differences */ + double mid = k + sn * 0.9 + b; + double l = mid + h * 0.7 + a * 0.9; + double r = mid + h * 0.9 + a * 0.6; + + /* Delay-based stereo width (ping-pong on hats + acid) */ + dbuf[dpos] = h * 0.25 + a * 0.18; + double delayed = dbuf[(dpos - dtap + DLEN) % DLEN]; + l += delayed * 0.35; + r -= delayed * 0.20; + dpos = (dpos + 1) % DLEN; + + /* Soft saturation (tanh limiter) */ + l = tanh(l * 0.82); + r = tanh(r * 0.82); + + float out[2] = { (float)l, (float)r }; + fwrite(out, sizeof(float), 2, stdout); + + /* ── Advance sequencer ── */ + if (++sample_in_step >= SP16) { + sample_in_step = 0; + step = (step + 1) % 16; + if (step == 0) { bar++; } + } + } + + return 0; +}