D1D-Turbo: 3-step drum one-shot generator (kick, snare, hi-hat)
D1D-Turbo, the fast version of D1D ("Drum 1D"), generates drum one-shots as raw waveform: 16,384 samples at 32 kHz (0.512 s), mono, class-conditional on kick, snare or hi-hat. It needs only 3 network passes per sample, about 0.35 s on a 6-core CPU.
It is a reflow distillation of the D1D teacher model (50 steps + CFG = 100 passes, not released), with classifier-free guidance baked in. On top of that it has a learned output gate, so it produces exact digital silence before the hit and after the decay with no post-processing.
Inference code (C++ header-only library, C++ command line, Python/ONNX): github.com/sixf0ur/D1D-Turbo. Everything is CPU-only and needs no PyTorch.
| file | |
|---|---|
model.onnx |
fp32 ONNX (opset 20), for ONNX Runtime and any other ONNX runtime |
model.bin |
the same weights in a simple binary format, for the C++ library d1d.hpp |
test_io.bin |
PyTorch reference outputs: drumgen --test test_io.bin checks a C++ build |
samples/ |
the audio samples below |
Samples
These samples were generated with the C++ code (drumgen --seed 2026, 3 steps).
They are peak-normalised and have no post-processing.
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| kick | ||||
| snare | ||||
| hat |
Usage
The full tools are on GitHub.
C++ (header-only, standard library only):
#include "d1d.hpp"
d1d::Model model = d1d::Model::load("model.bin"); // or load(bytes, size)
d1d::GenParams p;
p.seed = 42; // same seed -> same sound
std::vector<float> kick = model.generate(d1d::Drum::Kick, p); // 16384 floats @ 32 kHz
d1d::save_wav("kick.wav", kick);
Python without any extra code: pip install numpy onnxruntime soundfile, then
import numpy as np, onnxruntime as ort, soundfile as sf
from huggingface_hub import hf_hub_download
sess = ort.InferenceSession(hf_hub_download("sixf0ur/D1D-Turbo", "model.onnx"))
cls = 0 # 0 kick, 1 snare, 2 hat
x = np.random.randn(1, 1, 16384).astype(np.float32) # noise
for i in range(3): # 3 Euler steps, t = 0, 1/3, 2/3
t = np.array([i / 3], np.float32)
x = x + sess.run(None, {"x": x, "t": t, "y": np.array([cls], np.int64)})[0] / 3
sf.write("kick.wav", x[0, 0] / np.abs(x).max() * 0.95, 32000) # output level is arbitrary: peak-normalise
Steps. 3 steps is the setting chosen by listening. 4 and 8 sound practically identical, and with 1 step the pre-roll gets noisier. CFG is already baked into the weights, so keep the CFG weight at 1.0: one pass per step.
Speed
Measured on an AMD Ryzen 5 5500 (6 cores / 12 threads, AVX2), batch 1:
| per network pass | per sample (3 passes) | |
|---|---|---|
C++ (d1d.hpp), 12 threads |
114 ms | 0.34 s |
C++ (d1d.hpp), 1 thread |
481 ms | 1.4 s |
| Python, ONNX Runtime fp32 | ~118 ms | 0.35 to 0.38 s |
| teacher, for comparison | 11.7 s |
Model details
| type | rectified flow, reflow-distilled, velocity prediction, Euler sampler |
| network | 1D U-Net (same as the teacher) + learned output gate |
| parameters | 4,872,770 (fp32, 19.5 MB); the gate adds 225 |
| output | 16,384 samples, mono, 32 kHz (0.512 s); the hit starts at sample 256 (8 ms pre-roll) |
| classes | 0 kick, 1 snare, 2 hat (3 = null, unused: CFG is baked in) |
| sampler | 3 Euler steps, CFG weight 1.0 (3 passes per sample) |
Architecture. It is the same network as D1D:
- Encoder: a stem conv (k=7) feeds 5 resolution levels with channels 32 / 64 / 96 / 128 / 192. Each level has 2 FiLM residual blocks (GroupNorm, SiLU, conv k=3, FiLM scale/shift from the conditioning vector) and a strided conv downsample.
- Bottleneck: at length 1024 it is residual block, attention, residual block.
- Decoder: it mirrors the encoder with U-Net skip concatenation and uses nearest-neighbour Γ2 upsampling followed by a conv.
- Attention: self-attention with 4 heads runs in the bottleneck and in the first decoder level, both at length 1024, with a gated sin-cos position table.
- Conditioning: a sinusoidal timestep embedding goes through a 2-layer MLP; a class embedding is added to the result.
Output gate.
What it computes: from the final head features
h(after GroupNorm and SiLU, 32 channels), the network computes the velocityv = conv7(h)and a gateg = clamp(1 + conv7_gate(h), 0, 1). The predicted soundx1_hat = x_t + (1βt)Β·vis multiplied byg. The network returns that as a velocity:v_out = gΒ·v + (g β 1)Β·x_t / max(1 β t, 1e-3)Why it exists: with plain velocity prediction, digital silence requires
v = βx_t/(1βt)to about 0.03 % accuracy. In practice that left a β55 to β67 dB noise floor that was audible on headphones, and loss tweaks alone plateaued around β57 dB. With the gate, silence only needsgate β€ β1, a margin rather than a precision.Initialisation: the gate conv is zero-initialised, so
g = 1and training started as an exact copy of the ungated model.In ONNX: all of this is inside
model.onnx. The sampler is a plain Euler loop.
ONNX interface (model.onnx, opset 20, dynamic batch):
| name | type | shape | |
|---|---|---|---|
x |
float32 | (B, 1, 16384) | current noisy waveform |
t |
float32 | (B,) | time in [0, 1): 0 = noise, 1 = sound |
y |
int64 | (B,) | class 0..2 |
v |
float32 | (B, 1, 16384) | velocity (output, gate already applied) |
Sampler. This is all of the inference logic:
x = np.random.randn(1, 1, 16384).astype(np.float32) # noise
for i in range(3): # t = 0, 1/3, 2/3
t = np.array([i / 3], np.float32)
x = x + run(x, t, cls) / 3
audio = x[0, 0] / np.abs(x).max() * 0.95 # output level is arbitrary: peak-normalise
model.bin uses a trivial binary format, so it is easy to read from any
language:
- the header is
char[4] "DDW1", thenu32 count; - then come
countrecords, eachu32 name_len, the name,u32 ndim,u32 dims[ndim]andfloat32 data; - all values are little-endian;
- names are the PyTorch
state_dictkeys, plusmeta.stepsandmeta.cfg_w.
Training procedure
1. Pair generation.
- The teacher (D1D: 50 Euler steps, CFG 2.0) turned 60,000 noise vectors into sounds, classes balanced. This took 5 h on an RTX 3090.
- The noise of each pair is regenerated from its seed, so only the sounds were stored.
- 240 pairs (80 per class) are held out for evaluation.
2. Reflow.
- The student was initialised from the teacher.
- It was trained with the flow-matching loss on the straight line between
each pair:
x_t = (1βt)Β·z0 + tΒ·teacher(z0), targetv = teacher(z0) β z0,t ~ U(0, 1). Straight paths can be integrated in very few Euler steps. Because the pairs were made with CFG 2.0, the guidance is baked in. - Settings: 30,000 steps, batch 32, AdamW lr 1e-4 (500 warm-up steps), EMA 0.9995, about 2.5 h.
3. Gate fine-tune.
- The output gate was added (zero-init).
- The model was trained 12,000 more steps (lr 5e-5, EMA 0.999, about 1 h) towards silence-cleaned teacher targets. In these targets the pre-roll is zeroed (fade-in from sample 160 to 224), and the tail is faded to zero once its envelope reaches the teacher's noise floor.
- The loss had three terms:
- the velocity MSE;
- the MSE of the predicted sound
x1_hat; - a small (0.02) log-energy penalty on
x1_hatwhere the target is exactly 0.
Evaluation
The table uses 64 samples per class from fresh noise that was never used in training. Every row uses the same noise for the same index.
- Bands: energy share in %.
- pair-cos: diversity (lower = more varied).
- decay: time from the envelope peak to β20 dB.
- pre-roll / tail: level of samples 0β200 / the last 2,048 samples, relative to the peak. β180 means digital silence.
- SNR: waveform SNR against the teacher's output for the same noise.
| 0β250 | 250β2k | 2kβ8k | 8kβ16k | pair-cos | decay | pre-roll | tail | SNR | |
|---|---|---|---|---|---|---|---|---|---|
| kick, real | 95.8 | 3.7 | 0.4 | 0.1 | 0.713 | 126 ms | β180 | β180 | |
| kick, teacher (100 passes) | 97.4 | 2.5 | 0.1 | 0.0 | 0.595 | 122 ms | β59 | β57 | |
| kick, teacher, 4 steps, no distillation | 83.9 | 10.9 | 4.6 | 0.6 | 0.606 | 258 ms | β54 | β26 | 7.3 dB |
| kick, this model (3 passes) | 97.6 | 2.4 | 0.1 | 0.0 | 0.607 | 132 ms | β161 | β160 | 17.1 dB |
| snare, real | 22.9 | 41.8 | 29.4 | 5.8 | 0.722 | 86 ms | β180 | β180 | |
| snare, teacher | 16.5 | 47.6 | 33.7 | 2.3 | 0.769 | 74 ms | β66 | β67 | |
| snare, teacher, 4 steps, no distillation | 10.4 | 31.6 | 48.1 | 9.9 | 0.662 | 322 ms | β56 | β34 | 5.5 dB |
| snare, this model | 16.0 | 47.3 | 34.2 | 2.4 | 0.854 | 76 ms | β168 | β169 | 18.0 dB |
| hat, real | 2.9 | 3.7 | 58.2 | 35.3 | 0.640 | 52 ms | β180 | β180 | |
| hat, teacher | 0.0 | 0.7 | 55.7 | 43.4 | 0.703 | 39 ms | β70 | β73 | |
| hat, teacher, 4 steps, no distillation | 0.7 | 4.4 | 59.0 | 35.9 | 0.576 | 468 ms | β57 | β29 | 3.8 dB |
| hat, this model | 0.0 | 0.6 | 55.9 | 43.4 | 0.751 | 44 ms | β173 | β173 | 18.8 dB |
The distilled model matches the teacher's spectrum, attack and decay. Running the undistilled teacher with only 4 steps gives smeared noise tails instead (the "no distillation" rows). Pair-cos is higher than the teacher's, partly because the teacher's random noise floor makes any two of its samples less similar. Applying the same silence cleanup to the ungated student raises its snare pair-cos from 0.78 to 0.84 as well.
Limitations
- Noise under the sound. The gate only silences the regions that should be silent. While the sound plays, a faint noise floor remains underneath it, masked by the sound and quieter than the teacher's floor in the silent regions.
- Not the teacher. It is not a bit-exact copy of the teacher: about 17β19 dB SNR against the teacher's output for the same noise. It is the same kind of sound, not the identical waveform.
- Inherited from the teacher. The weak snare top octave (8β16 kHz 2.4 % vs 5.8 % real), slightly lower diversity than real one-shots, a fixed 0.512 s mono 32 kHz format, and three classes with no further control.
- Fixed sampler. CFG is baked in. A CFG weight other than 1.0 is not meaningful for this model.
License
CC BY-NC 4.0: free for
non-commercial use with attribution (see License.txt). For commercial
licensing contact info@sixf0ur.com.