Hum a sound and hear it become realistic Foley in 75 ms — fine-grained pitch/volume/brightness control AND real-time streaming, in one open-source model, by fusing sketch conditioning with adversarial post-training and a block-autoregressive chunker.
You are a sound designer scoring a film. A character slams a door, and you need the thud. You could dig through a sample library for an hour — or you could just make the sound with your mouth: "duh-DHUMP." That vocal imitation is a sketch. It has roughly the right loudness shape, the right pitch contour, the right brightness. It's wrong in every detail and right in every gesture.
Generative Text-to-Audio (TTA) models can turn "a heavy wooden door slamming" into audio. But text is a blunt instrument. It cannot say "loud here, soft there, pitch rising then a sharp transient at 0.4 seconds." For creative work — Foley, sound design, live jamming — you want to steer the sound continuously through time, the way your voice does when you hum it.
Modern TTA models are flow / diffusion models. They start from pure noise and iteratively denoise toward audio. "Iteratively" is the problem: high quality needs many denoising steps (say 50), and each step is a full forward pass through a large transformer. That is inherently slow — seconds per clip.
The two research communities then pulled in opposite directions:
Suppose you somehow get a fast, controllable model. You still can't jam with it in real time. Why? Because these models are non-autoregressive: they generate the whole 12-second clip at once, using bidirectional context (the sound at second 2 depends on the sketch at second 10). So you must capture the entire sketch before generation can even start. There is no "play a note, hear it instantly" — the model needs to see the future first.
| What you want | Why it's hard | Who had it before |
|---|---|---|
| Sketch control (pitch/vol/bright over time) | Needs finetuning + conditioning machinery | Sketch2Sound (slow, closed) |
| Fast inference (<0.5 s) | Diffusion needs many steps | Distilled TTA (text-only) |
| Real-time streaming (jam live) | Non-AR models need the whole future | Nobody, for flow models |
FlashFoley's claim is that you can have all three at once, in one open-source model. The rest of this lesson builds, piece by piece, exactly how.
FlashFoley is not one idea — it's three ideas stacked in a specific order, and the order is the cleverness. Each one removes exactly one of the three blockers from Chapter 0.
Start with Stable Audio Open Small (a 340M-parameter rectified-flow DiT). Stage 1: finetune it on the velocity loss with sketch controls + inpainting masks injected via Pre-Transformer Projection. Stage 2: ARC post-training replaces the velocity loss with a relativistic adversarial loss plus a contrastive loss, turning the 50-step model into a 1–8 step generator. Inference: for offline use, run the few-step sampler once; for live use, run the block-autoregressive chunker that generates audio in overlapping blocks as the sketch streams in.
We will build each stage from zero. By the end you could re-draw the training diagram on a whiteboard and explain why every arrow is there.
Before controls or speed, we need the base generative model. FlashFoley is a rectified flow (RF) model operating in a VAE latent space. Let's earn that sentence.
Raw stereo audio is huge: x0 ∈ ℝ2×S·fs — 2 channels, S seconds at fs=44,100 samples/sec. About 1 million numbers for an 11 s clip. We never model that directly. A pretrained VAE encoder ℰ compresses it: z0 = ℰ(x0) ∈ ℝM×N. In the paper's model the compression is 2048× in time, leaving 64 channels × 256 latent frames at ~21.5 Hz. All generation happens on these 256 frames; a VAE decoder turns the result back into audio.
We want to sample new latents from the data distribution p0. Rectified flow defines a dead-simple bridge between a Gaussian p1 (pure noise) and data p0: a straight line. Given a clean latent z0 and a noise sample ε ∼ 𝒩(0,I), the corrupted point at time t∈[0,1] is just a linear blend:
Symbol by symbol: t=0 is clean data, t=1 is pure noise, and in between you slide linearly. The velocity — the direction you'd move to go from data toward noise — is the derivative of that line:
To generate, you run the line backwards: start at noise z1 and integrate dzt = −v\,dt down to t=0. The only thing the network must learn is to predict that velocity at any point and time, given the text prompt:
This is a simulation-free objective: you don't run the ODE during training, you just pick a random t, blend, and regress the velocity. vθ is the DiT; ctxt is the text embedding; the squared error is the same MSE you've seen a hundred times.
The teal dot is data z0; the gray dot is a noise sample ε. Drag t to slide along the straight line to the current zt (orange). The orange arrow is the velocity v=ε−z0 the network learns to predict. Hit Integrate to walk from noise back to data — that's generation.
A vocal sketch is a sound. To condition the model on it, we don't feed the raw waveform — we extract three interpretable, time-varying signals that capture the gesture without the timbre. These come from Sketch2Sound, and choosing them well is what lets a hum control a door slam.
| Control | What it measures | Extracted by | Shape |
|---|---|---|---|
| Volume | Loudness over time: RMS in dB of the A-weighted magnitude spectrum | signal processing | [1, N] |
| Pitch | Fundamental frequency over time (a probability matrix over pitches) | PESTO (a neural pitch estimator) | [Kpitch, N] |
| Brightness | Spectral centroid — the "center of mass" of the spectrum, i.e. how trebly vs muffled | signal processing | [1, N] |
Each feature fi ∈ ℝKi×N is a time series of length N (resampled to match the 256 latent frames). The full control set is F = {fi}i=1..l. These are local controls: one value (or vector) per frame, unlike a single global text embedding.
Here is the most beautiful trick in the control design. If you feed the model the exact extracted features, it tries to reproduce them precisely — great for style transfer, but it overfits to the spectral quirks of your voice. If you blur them, the model treats them as loose suggestions — a true sketch. FlashFoley convolves the controls with a median filter of random width during training, so at inference the filter width becomes a continuous knob:
The faint line is the raw extracted control from a noisy "vocal sketch"; the bold line is after a median filter of width w. Drag w up and watch the jittery sketch smooth into a clean gesture — that's the "render ↔ sketch" dial. Switch the control to see volume (loudness), pitch (a contour through the probability matrix), and brightness (spectral centroid).
We have three time-varying controls. How do we feed them into a 340M-parameter DiT without retraining everything or adding bulky adapters? The naive options each add 6–50% new parameters. FlashFoley uses Pre-Transformer Projection (PTP), which adds ~0.1%.
A DiT first turns the noisy latent zt into an initial hidden state via an input projector (a small MLP) before any transformer blocks:
where H is the hidden dimension and N=256 frames. PTP learns one tiny linear operator Wi ∈ ℝKi×H per control, projects each control up to hidden dimension, and simply adds it to that initial hidden state:
That's it. Each Wi⊤ fi maps a [Ki, N] control to [H, N] and lands as a per-frame additive bias on the hidden state. Then the whole model finetunes on the velocity loss (Eq. of Ch 2). The DiT blocks themselves never change shape.
The authors prove a small, illuminating fact. If ProjInθ were a single linear layer, then PTP and channel-wise concatenation are exactly equivalent — concatenating ΣKi extra channels and learning a wider weight matrix is the same as learning a separate Wi per control. But their ProjIn is a 2-layer residual MLP. With that nonlinearity, concatenation forces implicit weight sharing in the projection matrices, and in short finetuning runs the module can't adapt to each control's distribution separately. PTP sidesteps this by construction.
The latent and the three controls each get projected, then summed into h'init — which is the only place the controls enter. Everything downstream is the untouched pretrained DiT. Click the nodes to read the shapes.
# Pre-Transformer Projection: ~0.1% new params, no DiT changes import torch, torch.nn as nn class PTP(nn.Module): def __init__(self, ctrl_dims, H): # one independent linear per control: K_i -> H, no bias, no sharing self.W = nn.ModuleList([nn.Linear(k, H, bias=False) for k in ctrl_dims]) def forward(self, h_init, controls): # h_init: [B, H, N] controls[i]: [B, K_i, N] for Wi, f in zip(self.W, controls): # project K_i -> H along channel dim, add as per-frame bias h_init = h_init + Wi(f.transpose(1,2)).transpose(1,2) return h_init # [B, H, N] -> straight into the DiT blocks
We now have a slow but controllable model: 50 denoising steps, beautiful sketch control. Stage 2 makes it fast. The goal is a generator Gφ that produces good latents in 1–8 steps instead of 50. The method is ARC — Adversarial Relativistic-Contrastive post-training.
If you take the 50-step model and run it for 4 steps, the output is blurry garbage — the velocity field assumes small steps. Classic distillation fixes this by training a student to match a strong teacher, but teachers are expensive and limit quality. ARC instead replaces the regression target entirely with an adversarial one: a discriminator Dψ judges realism at any noise level, so the generator learns to make realistic audio in very few steps with no teacher.
Take a real latent z0 and conditioning C={ctxt, F}. Noise it to zt, generate &zcirc;0=Gφ(zt,t,C). Re-noise both to a common level s and let Dψ score them. The min-max objective on the relative scores is:
Read it: Dψ(&zcirc;s) − Dψ(zs) is "how much higher the fake scores than the real." Gφ wants this difference large (fool the judge); Dψ wants it small (catch the fake). f is the softplus that turns the score gap into a smooth loss. Everything is relative to the paired real sample — that is the whole anti-collapse mechanism.
Once trained, Gφ generates by "ping-pong": denoise toward data, re-noise to a slightly lower level, denoise again — a handful of times. The result: 11.88 s of stereo 44.1 kHz audio in 75 ms, ~10× faster than existing controllable TTA, with no quality loss (and, surprisingly, slightly better control accuracy).
The orange sketch control (a loudness/pitch gesture) is fixed. Press Generate: the noisy mel-like field (left) ping-pongs into structured audio (right) in T steps, snapping to the sketch's contour. Drag T to fewer steps and watch quality degrade gracefully — even 1 step stays usable because the sketch supervises the generator. Toggle Relativistic D off to simulate a vanilla GAN collapsing to a flat, generic output.
The relativistic loss makes audio realistic. But realistic-and-ignoring-the-prompt is a known GAN failure: the generator makes great sound that doesn't match the text. ARC adds a second, auxiliary loss to force the discriminator to care about the conditioning.
Same relativistic form, but now both samples are real — they differ only in their conditioning. One gets the correct condition C; the other gets a batch-shuffled, wrong condition P[C] (some random permutation of the batch's conditions):
The discriminator is trained to maximize this — to score a real sample with its correct caption higher than the same real sample with a mismatched caption. To do that, Dψ must actually read the condition and judge semantic agreement, not just sniff for high-frequency GAN artifacts. The combined objective is:
Standard ARC (text-only) shuffles the text. But FlashFoley has two kinds of conditioning with very different granularity: a single global text caption, and dense time-varying local controls F. So: which condition should we shuffle in the contrastive loss? The original ARC paper would say "shuffle everything." The authors test this and find it is a trap.
Two simulated spectrograms for the same prompt. Shuffle text only: the discriminator must read the caption, so generations keep rich text-driven timbre (full frequency content). Shuffle all: the discriminator latches onto the sketch and ignores text — watch the high-frequency content collapse and the prompt get ignored, exactly as in Figure 3.
We now have fast, controllable, high-quality offline generation. But it's still non-autoregressive: it needs the whole sketch first. For live jamming, that's a dealbreaker. Stage 3 makes generation streamable without ever training a causal model.
During Stage 1 finetuning, the authors add inpainting/outpainting conditions through the same PTP machinery. They feed the model a masked clean latent (plus the mask itself) where the mask always hides some future context. This trains the model to outpaint — to continue audio given a few conditioning frames. These conditions are kept through ARC post-training too, so both Gφ and Dψ understand "given this much, generate the next."
Pick a block size B < N and a stride k. The model generates the next chunk of B frames by inpainting the last B−k frames of the previous chunk. The trick that makes this run on an unchanged non-causal model: it always generates a full N frames, but only fills the first B sketch-control slots (the rest padded with zeros), and uses a "seconds total" embedding to say how much is real. After each pass, the overlap region is forced to equal the previous chunk for seamless transitions; then the new frames stream out.
The timeline is N=256 latent frames. Each Next chunk generates B frames; the overlap (the last B−k frames of the previous chunk, shown striped) is forced to match for a seamless join, and the fresh k frames (solid) stream out. Smaller B → lower latency but the speed–quality tradeoff worsens; k has little effect on quality.
The setup: base model is Stable Audio Open Small (340M DiT, stereo 44.1 kHz, 256 latent frames ≈ 11.88 s). Trained on WavCaps (~400K audio samples), 40K control-finetuning steps + 70K ARC steps, batch 256 across 4 H100s. Evaluated on VimSketch — ~10K real human vocal imitations of 500 audio classes.
| Metric | Measures | Direction |
|---|---|---|
| Control L1 (RMS / centroid / pitch / chroma) | How well the generation follows the sketch (L1 on each control) | lower better ↓ |
| FD (Fréchet Distance, openl3) | Audio quality / realism vs real audio distribution | lower better ↓ |
| CLAP | Text adherence (cosine sim of audio & caption embeddings) | higher better ↑ |
| MOS | Human-rated subjective quality (0–100, 95% CI) | higher better ↑ |
| OL / SL | Offline latency / Streaming latency (seconds) | lower better ↓ |
| Model | Steps | FD ↓ | CLAP ↑ | MOS ↑ | OL ↓ | SL ↓ |
|---|---|---|---|---|---|---|
| SAOS (base, no controls) | 50 | 41.87 | 0.32 | 50.8 | 0.63 | 12.52 |
| + sketch controls | 50 | 56.38 | 0.26 | 64.2 | 0.63 | 12.52 |
| FlashFoley (ARC) | 5 | 54.32 | 0.23 | 63.7 | 0.08 | 11.96 |
| + BAR (streaming) | 5 | 56.87 | 0.22 | 61.9 | 0.08 | 6.02 |
| + sketch LC (the trap) | 5 | 65.10 | 0.13 | 54.4 | 0.08 | 11.96 |
Note the expected tradeoff in the first two rows: adding sketch controls raises FD and lowers CLAP (controls are a harder target than text alone) — but raises MOS sharply, because humans find controllable, sketch-following audio far more useful. The whole point is that ARC then makes this controllable model ~10× faster without giving that back.
The ablations plot FD against average relative error (control error relative to the baseline SAOS error, averaged over controls). Two knobs are swept; each reveals something non-obvious.
As expected from Sketch2Sound, there's a tradeoff: a small median filter (w=1) gives low control error but worse FD; a large filter (w=5) gives better FD but looser control. The surprise is on sampling steps: dropping from 8 steps to 1 degrades quality only slightly.
Block size B is anti-correlated with both FD and error — a clean speed–quality tradeoff (smaller blocks = lower latency but worse quality). Stride k, however, has little relationship to performance. That means FlashFoley needs surprisingly little preceding context to stay coherent — you can use a large stride (stream more fresh audio per chunk) almost for free.
Each dot is a configuration; lower-left is better (low FD, low control error). Hover/tap points to read the config. Filter & steps: see steps barely move FD, while filter width trades control for quality. Block & stride: see block size B drive the frontier while stride k barely matters.
FlashFoley is a synthesis paper: it stands on three prior lines and welds them into the first open-source, fast, controllable, streamable sketch2audio system.
| Idea | Equation / Rule | What each symbol means |
|---|---|---|
| RF corruption | zt = (1−t)z0 + tε | z0=clean latent, ε=noise, t∈[0,1] |
| RF velocity / loss | v=ε−z0; min ‖v−vθ‖2 | vθ=DiT velocity prediction |
| PTP injection | h'init = hinit + Σi Wi⊤ fi | Wi=per-control linear (Ki→H), fi=control |
| Relativistic loss | f(D(&zcirc;s)−D(zs)), f=−log(1+e−x) | paired real/fake, same C; boundary per real sample |
| Contrastive loss | f(D(zs,P[C])−D(zs,C)) | P[C]=shuffled text only; forces D to read prompt |
| ARC objective | minφmaxψ ℒR + λℒC, λ=1 | G=generator, D=discriminator |
| Streaming latency | ≈ B·S/N | B=block size, S=clip seconds, N=latent frames |
FlashFoley starts from a rectified-flow text-to-audio DiT (Stable Audio Open Small). Stage 1: finetune on the velocity loss while injecting three time-varying sketch controls (volume, pitch, brightness) — plus inpainting masks — through Pre-Transformer Projection, a per-control linear added to the hidden state for ~0.1% new params; a random-width median filter becomes the inference dial between "render exactly" and "loose sketch." Stage 2: ARC post-training distills 50 steps down to a few via a relativistic adversarial loss (score the difference between paired real/fake to kill mode collapse) plus a contrastive loss that shuffles text only (shuffling the sketch makes the discriminator ignore text and lose timbre). Result: 11.88 s stereo 44.1 kHz in 75 ms, ~10× faster, no quality loss. Inference: a block-autoregressive chunker generates overlapping B-frame blocks with forced overlap, cutting streaming latency to ~B·S/N so you hear the first chunk while still sketching.