All posts
Essay·6 min read

I Made My Generative Audio Pipeline Refuse Its Own Output

Six SuperCollider environments, a nine-stage render pipeline, and a gate battery that would rather produce no file than a bad one. The gate turned out to be the useful part.

The render was four and a half seconds too long, so the pipeline deleted it.

Not flagged it. Not warned me and wrote it anyway. There was no file at the end. I'd asked for two minutes of a modern Atlanta studio — HVAC, monitor noise, a 142 BPM session happening in the room — and what I got back was a non-zero exit code and an empty output directory.

That's the part of the project that ended up mattering, and it isn't the synthesis.

What the thing actually is

Six independent SuperCollider environments. A city block. A mountain cabin in a thunderstorm. An alien rainforest. A teenage bedroom in Chicago in 1987. A treated Atlanta producer studio. A forest camp in Washington.

None of them are recordings. There are no samples anywhere in the project — every sound is synthesized from stock UGens at render time. And none of them are loops. Each environment runs independent procedural populations — traffic, weather, wildlife, machines, a person moving around — that are coupled by slow shared environmental state rather than by a repeating pattern. The radiator doesn't know about the drum machine. They just happen to be in the same room, and the room ties them together.

That means every render is a different two minutes. Pass the same seed and you get the same file back, byte for byte. Don't pass one and you get a different night.

The synthesis was the fun part. It was also the part that produced the most garbage.

The problem with generating things

Generative systems are extremely good at handing you output. That's the whole pitch. You run the thing, you get a file, you feel productive.

What they're bad at is handing you nothing. And nothing is very often the correct answer.

I had renders that started mid-transient because the server hadn't warmed up. Renders that ended on a fade because the tail got clipped. Renders where a population happened to go quiet for eight seconds and the middle of the file was effectively silence. Renders that were fine but eleven dB quieter than the last one, so you couldn't put them next to each other.

Every one of those is listenable enough that you'd ship it at 2am. You'd notice the problem three days later, in the middle of something else, and by then you'd have four more built on top of it.

So I stopped trying to generate better and started making the pipeline refuse.

The gate battery

Every environment renders through the same PowerShell pipeline. Nine stages, and most of them exist to say no.

The render itself is 124.5 seconds: a 4.0-second warm-up pre-roll, the 120.0 seconds I actually want, and a 0.5-second cleanup tail. The pre-roll is there so the file never starts from silence or a half-open envelope. The tail is there so it never ends on a fade. Neither of them ships.

Then the trim, and the trim has to be exact. The raw WAV is gated at duration_ts=5976000 samples. The trimmed deliverable is gated at 5,760,000 samples — 120.000 seconds at 48 kHz, and not a sample either side. That number isn't arbitrary: it's divisible by the audio server's 64-sample block size, so the trim boundary always lands on a count the server actually finished writing.

Then five quality checks on the trimmed file:

  • Overall peak between −40 and −0.8 dBFS. The ceiling catches clipping. The floor catches a render that came out effectively empty.
  • Zero silence windows at −55 dB lasting 2.5 seconds or longer. This is the one that catches a population going quiet mid-file.
  • The head — the first 0.15 seconds — has to peak above −50 dBFS. If it doesn't, the pre-roll didn't do its job.
  • The tail — 119.955 to 120 seconds — has to peak above −50 dBFS and must not be silent. Same logic at the other end.
  • Integrated loudness between −25.0 and −17.0 LUFS, measured with EBU R128. This is what makes six independent renders sit next to each other without a fader.

Then the MP3 encode, and this is my favourite one. The deliverable gets encoded to 320 kbps CBR with the seed written into its metadata — and then decoded straight back to 24-bit PCM and re-gated for duration_ts=5760000. If the round-trip doesn't return the exact same sample count, the encode is wrong and the run fails.

Fail any single one of those and there is no deliverable. Not a warning in a log nobody reads. No file.

What changed

Most of what I generate now, I never hear. That sounds like a loss and it's the opposite.

The gate isn't a quality judgement — it can't tell you whether an environment is good. It has no opinion about whether the alien rainforest is convincing. What it does is remove an entire category of failure from the set of things I have to listen for. When a file exists, I already know it starts somewhere, ends somewhere, doesn't have a hole in the middle, and sits at the same loudness as the other five. That's five things my ears don't have to do, and my ears are worse at all five of them than a threshold is.

So the listening I do is about the thing I'm actually qualified to judge. Does the storm feel like it's outside? Does the bedroom feel like somebody lives in it?

There's a second effect I didn't plan for. Because the gate is strict and deterministic, changing the synthesis got cheaper. I can rewrite how thunder decays and re-render all six environments, and if something structural broke, the pipeline tells me before I've listened to a single second of it. The gate is a test suite that happens to be about audio.

The bit I'd steal for something else

You don't need SuperCollider for any of this. The pattern is:

Decide what "obviously broken" looks like, in numbers, before you generate anything. Then make producing nothing the default outcome when you hit it.

That's it. The specific thresholds are audio-shaped because this project is audio, but the shape transfers. If you're generating images, "obviously broken" is a dimension, a file size, an alpha channel that shouldn't be there. Text, and it's a length band, a required section, a forbidden phrase. The numbers are cheap to write and they're checking the exact things you'll skip at 2am.

The mistake I made for a long time was treating output as the goal and review as a separate step I'd get to. Review doesn't happen. A gate happens every single time, whether you're paying attention or not.

One honest caveat: a gate this strict will occasionally kill something good. A render that genuinely wanted a quiet stretch longer than 2.5 seconds is a legitimate piece of sound design, and my pipeline will not ship it. I decided I'd rather lose that than keep the four failure modes it protects against — but that's a trade I made on purpose, not a free win, and if you build one of these you're making the same trade.


I'm packaging the six environments up. Six Rooms will be all six two-minute renders at 24-bit/48 kHz plus 320 kbps MP3s, the spectrogram for each, the seed that reproduces it byte for byte, and a short PDF on the render contract if you want to build the gate battery into something of your own. It isn't up yet — this post gets the link when it is.