CoProducer Measure a mix
An MPC-style sampler on black marble with a separated drum waveform sliced into glowing cyan chops and a faint magenta bass trace beneath

Sampling · 8 October 2026 · 11 min

Sampling from separated stems: chop around the bleed, not through it

NoDAW Labs · Lacquer Vol. 05

A separated stem is a great sample source, as long as you treat it like a recording with a known flaw. Before you chop, find out where the bleed lives. In drum stems it’s mostly the bass, under 120 Hz. Put the attack back that the mask softened. Null-test so you know exactly what any processing removed. Match the key with a clean pitch ratio. And clear anything you don’t own, because how you got the stem doesn’t change whose recording it is.

Chopping a full mix used to mean choosing the least-bad two bars: the break where the vocal drops out, the intro before the bass comes in. Separation changed that. Now you can lift the drums from bar 37, where the drummer finally opens the hats, or the bassline from the chorus without the singer on top. It’s a huge creative unlock, and it comes with a catch nobody puts on the box. The stem isn’t the original track. It’s an estimate, and estimates have fingerprints.

The good news is that those fingerprints are predictable. Once you know where to look, the cleanup takes minutes.

First, what you’re allowed to sample

Quickly, because it matters more than any EQ move. The separator doesn’t launder anything. Pull a drum stem out of a commercial record and you’re still copying that record. US courts don’t even agree on how small a sample can be before it counts. In Bridgeport Music v. Dimension Films (6th Cir. 2005), the court wrote “Get a license or do not sample” about a two-second sample. In 2016 the Ninth Circuit called that reasoning unpersuasive in VMG Salsoul v. Ciccone. A split like that is exactly why clearance is the only answer that works everywhere.

So this workflow is for sources you have a right to: your own old records and sessions, recordings you’ve licensed, a collaborator’s track they’ve said yes to, sample packs whose license covers it, and genuinely public-domain recordings. That covers a lot of ground. Some of the best flips come from your own catalog, where you no longer have the session.

A stand-in for a real model

To show the flaws honestly, we needed a separator that makes mistakes the way real ones do. Oracle masks are too perfect, so sampling_lab.py takes the oracle mask for a synthetic groove, blurs it by about 70 ms and three frequency bins, and pulls it 25% toward an even split. The result is a believable imitation of a model: decent on the voice, soft on transients, smeary on hats. Everything below is measured on that synthetic stand-in.

#!/usr/bin/env python3
"""sampling_lab.py: three checks before you chop a separated stem.

SYNTHETIC audio only (lab.py). A real model makes mistakes an oracle mask
doesn't, so we fake a model: the oracle mask blurred over ~70 ms and ~3 bins,
then pulled 25% toward an even split. Then:
  1. bleed audit: where the other instruments hide inside the "drum" stem, by band
  2. transient check: kick attack in the true vs separated drums, and after a
     simple transient restore
  3. null test + residual: stems sum back to the mix; mix minus a processed
     stem gives you back exactly what you removed
  4. key match: rubberband pitch shift of the bass stem, verified by FFT
Usage: python3 sampling_lab.py   (numpy + ffmpeg with rubberband + lab.py)
"""
import os, subprocess, tempfile
import numpy as np
import lab

NF = 4096
stems = lab.synth_stems(seed=7)
mix, stems = lab.mixdown(stems)
names = list(stems)

def blur(M, t=3, f=3):
    k = np.ones(t) / t
    M = np.apply_along_axis(lambda c: np.convolve(c, k, "same"), 0, M)
    k = np.ones(f) / f
    return np.apply_along_axis(lambda r: np.convolve(r, k, "same"), 1, M)

oracle = lab.ratio_masks(stems, NF)
imperfect = {k: 0.75 * blur(m) + 0.25 / len(names) for k, m in oracle.items()}
tot = sum(imperfect.values())
imperfect = {k: m / tot for k, m in imperfect.items()}
est = lab.separate_with_masks(mix, imperfect, NF)
print("imperfect-mask stems (stand-in for a real model), SDR dB:",
      " ".join(f"{k}={lab.sdr(stems[k], est[k]):.1f}" for k in names))

# 1. bleed audit in the drum stem (kick + hats)
drums_true = stems["kick"] + stems["hats"]
drums_est = est["kick"] + est["hats"]
X, meta = lab.stft(mix, NF)
Md = imperfect["kick"] + imperfect["hats"]
contrib = {k: lab.istft(lab.stft(v, NF)[0] * Md, meta) for k, v in stems.items()}
bleed = sum(contrib[k] for k in ("bass", "pad", "lead"))
target = contrib["kick"] + contrib["hats"]
f = np.fft.rfftfreq(len(mix), 1 / lab.SR)
B = np.abs(np.fft.rfft(bleed)) ** 2; T = np.abs(np.fft.rfft(target)) ** 2
print("\n1) bleed audit, drum stem: drums-to-bleed ratio by band (higher = cleaner)")
for lo, hi in [(20, 60), (60, 120), (120, 250), (250, 1000), (1000, 4000), (4000, 20000)]:
    sel = (f >= lo) & (f < hi)
    r = 10 * np.log10(T[sel].sum() / (B[sel].sum() + 1e-20) + 1e-20)
    share = 100 * B[sel].sum() / B.sum()
    print(f"   {lo:5d}-{hi:<5d} Hz  drums/bleed {r:6.1f} dB   {share:5.1f}% of all bleed")

# 2. transient check on kick hits
k = stems["kick"]
on = np.where((np.abs(k[1:]) > 0.05) & (np.abs(k[:-1]) <= 0.05))[0]
on = [o for i, o in enumerate(on) if i == 0 or o - on[i - 1] > lab.SR // 4]
def attack(x):
    a = int(0.005 * lab.SR); b = int(0.05 * lab.SR); p = int(0.02 * lab.SR)
    early = np.mean([np.sum(x[o:o + a] ** 2) for o in on])
    body = np.mean([np.sum(x[o:o + b] ** 2) for o in on])
    pre = np.mean([np.sum(x[o - p:o] ** 2) for o in on if o > p])
    return 10 * np.log10(early / body), 10 * np.log10(pre / body + 1e-12)
def restore(x, fast=0.0005, slow=0.02, amount=1.5):
    env = np.abs(x)
    def follow(e, tau):
        a = np.exp(-1 / (tau * lab.SR)); y = np.empty_like(e); s = 0.0
        for i, v in enumerate(e):
            s = a * s + (1 - a) * v; y[i] = s
        return y
    ef, es = follow(env, fast), follow(env, slow)
    g = np.clip((ef / (es + 1e-6)) ** (amount - 1), 1.0, 4.0)  # boost only where the hit outruns the body
    return x * g
print("\n2) kick attack: first 5 ms vs first 50 ms (higher = punchier); 20 ms pre-echo vs body (lower = cleaner)")
for label, x in [("true drums", drums_true), ("separated drums", drums_est),
                 ("separated + transient restore", restore(drums_est))]:
    a, p = attack(x)
    print(f"   {label:30s} attack {a:6.1f} dB   pre-echo {p:6.1f} dB")

# 3. null test and residual recovery
recon = sum(est.values())
print(f"\n3) null test: mix - sum(stems) peak = {lab.db(np.abs(mix - recon).max()):.1f} dBFS")
hp = np.fft.irfft(np.fft.rfft(est["kick"] + est["hats"]) * (f > 150), n=len(mix))
other = recon - (est["kick"] + est["hats"])
removed = mix - other - hp
print(f"   high-passed the drum stem at 150 Hz; mix - (other stems + HP drums) = what you cut:")
print(f"   residual RMS {lab.db(np.sqrt(np.mean(removed**2))):.1f} dBFS, of which "
      f"{100*np.sum(np.abs(np.fft.rfft(removed))[f<150]**2)/np.sum(np.abs(np.fft.rfft(removed))**2):.1f}% sits below 150 Hz")

# 4. key match with rubberband
def peak_hz(x, lo=30, hi=90):
    X = np.abs(np.fft.rfft(x[: 4 * int(lab.SR * 60 / 92)] * np.hanning(4 * int(lab.SR * 60 / 92))))
    fr = np.fft.rfftfreq(4 * int(lab.SR * 60 / 92), 1 / lab.SR)
    sel = (fr > lo) & (fr < hi)
    return fr[sel][np.argmax(X[sel])]
with tempfile.TemporaryDirectory() as d:
    src = os.path.join(d, "bass.wav"); dst = os.path.join(d, "bass_up2.wav")
    lab.write_wav(src, est["bass"])
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", src, "-af",
                    f"rubberband=pitch={2**(2/12):.6f}", dst], check=True)
    up = lab.read_audio(dst)
print(f"\n4) key match: bass bar 1 fundamental {peak_hz(est['bass']):.1f} Hz -> "
      f"{peak_hz(up):.1f} Hz after +2 semitones (expected x{2**(2/12):.4f} = {peak_hz(est['bass'])*2**(2/12):.1f} Hz)")

Real output from the box:

imperfect-mask stems (stand-in for a real model), SDR dB: kick=6.8 bass=6.9 pad=7.3 lead=11.0 hats=2.2

1) bleed audit, drum stem: drums-to-bleed ratio by band (higher = cleaner)
      20-60    Hz  drums/bleed    4.5 dB    48.9% of all bleed
      60-120   Hz  drums/bleed   10.3 dB    28.9% of all bleed
     120-250   Hz  drums/bleed    8.3 dB     9.3% of all bleed
     250-1000  Hz  drums/bleed  -13.5 dB    10.9% of all bleed
    1000-4000  Hz  drums/bleed   -9.4 dB     1.7% of all bleed
    4000-20000 Hz  drums/bleed   19.0 dB     0.1% of all bleed

2) kick attack: first 5 ms vs first 50 ms (higher = punchier); 20 ms pre-echo vs body (lower = cleaner)
   true drums                     attack   -7.7 dB   pre-echo -120.0 dB
   separated drums                attack   -8.9 dB   pre-echo  -19.8 dB
   separated + transient restore  attack   -5.2 dB   pre-echo  -19.8 dB

3) null test: mix - sum(stems) peak = -240.0 dBFS
   high-passed the drum stem at 150 Hz; mix - (other stems + HP drums) = what you cut:
   residual RMS -27.8 dBFS, of which 100.0% sits below 150 Hz

4) key match: bass bar 1 fundamental 54.8 Hz -> 61.3 Hz after +2 semitones (expected x1.1225 = 61.5 Hz)

1. Audit the bleed before you chop

The audit splits the drum stem into what truly came from the drums and what leaked in from bass, pad and lead, then reports it band by band. The pattern is the one you’ll hear on almost every real drum stem: 78% of the bleed sits under 120 Hz, where the bass lives right under the kick. In the low mids, 250 Hz to 4 kHz, the bleed actually outweighs the drums by 9–14 dB. That isn’t a lot of energy, but it’s the pad and voice ghosting between hits. Above 4 kHz the stem is clean.

Band (synthetic drum stem)Drums vs bleedShare of all bleedWhat to do with the chop
20–60 Hz4.5 dB48.9%High-pass, then layer your own sub or kick
60–120 Hz10.3 dB28.9%Keep the punch; duck it against your new bass
120–250 Hz8.3 dB9.3%Usually fine; a narrow cut if the old bass note rings
250 Hz–4 kHz−13.5 to −9.4 dB12.6%Gate or trim between hits; this is the ghost pad and voice
4–20 kHz19.0 dB0.1%Leave it; this is your snap and air

The practical lesson is to chop around the bleed, not through it. Start each slice right on the hit and end it before the next ghost swells up. Take the low end out of the chop and put your own underneath, which is the same chair problem as in Your kick and 808 are arguing in the same chair, just with an old bassline as the second person. If the old bass note under the kick is in a different key from your track, no amount of EQ will make it sit. High-pass it and move on.

2. Put the attack back, carefully

Masks smear energy across their window, so separated hits come out softer and get a faint pre-echo. In the test, attack (the first 5 ms against the first 50 ms) dropped from −7.7 dB on the true drums to −8.9 dB on the separated stem, and a ghost appeared at −19.8 dB in the 20 ms before each kick, where the original had nothing.

The simple transient restore in the script compares a fast and a slow envelope and boosts only where the hit outruns the body. It lifted attack to −5.2 dB, which is past the original. That overshoot is the warning. Transient shapers on separated drums are easy to overdo, and the result sounds clicky and fake. Dial it in by ear against the source. Notice too that the pre-echo didn’t move. A shaper can’t remove energy that sits before the transient. Only editing can: start the slice on the hit, with a 1–2 ms fade-in.

3. Null-test, then use the residual

With mask-based separation the stems sum back to the mix. The test shows a null at −240 dBFS, which is numerical silence. That gives you a superpower most producers never use: whatever you take away from a stem, you can get back exactly. After high-passing the drum stem at 150 Hz, the mix minus the other stems minus the processed drums is precisely the part you cut. In the test that was −27.8 dBFS RMS, all of it below 150 Hz.

Print that residual as its own track. Sometimes the “bleed” you removed is the thing that made the loop feel like a record: a room tone, the old bass breathing under the kick. Now it’s a fader instead of a loss. One caveat: not every model is mask-based. Some predict waveforms directly, and their stems may not null perfectly. Run the test anyway. The size of the residual tells you how much the model made up.

4. Match the key with a real ratio

A semitone is a frequency ratio of 21/12, so n semitones is 2n/12. Two up is ×1.1225. In the test, Rubber Band through ffmpeg’s rubberband filter moved the separated bass from 54.8 Hz to 61.3 Hz, against an expected 61.5 Hz. That’s within the FFT’s resolution and close enough for any tuner. Use a formant-aware shifter for vocals so a +3 doesn’t turn the singer into a cartoon. Keep shifts within a few semitones when you can, because bleed shifts along with the target and gets more noticeable the further you go.

Tempo works the same way, as a ratio. To pull a 92 BPM loop up to 100 BPM, stretch by 100/92 = 1.087. In ffmpeg, rubberband=tempo=1.087 changes the timing and leaves the pitch alone, and atempo does the same with a simpler algorithm that’s fine for small moves. Do the tempo stretch and the pitch shift as separate, deliberate steps, and print each one to a new file. If you only change the sample rate, the old turntable way, pitch and tempo move together. That can be exactly the color you want for a chipmunked soul flip, but then choose it on purpose, not by accident.

The routine, start to finish

  1. Confirm the rights. Yours, licensed, cleared or public domain. Write down which.
  2. Use the best source you can get. A first-generation WAV, not a lossy copy. The cleanest-copy essay in this series shows why.
  3. Split to four stems first. Go wider only when a part really needs its own lane.
  4. Audit, then chop. Slice on the hits, clear the low end, trim the ghosts.
  5. Restore the attack by ear, stopping before it clicks.
  6. Null-test and keep the residual as an optional layer.
  7. Key-match with clean ratios and check with a tuner.
  8. Bounce the chops as WAV with the source noted in the file name or metadata, so clearance questions later take a minute instead of a week.

For the split itself, DeepSplit runs locally, does everything from two stems to separate kick, snare and hat lanes, and includes per-stem effects presets if you want a starting point before you do the careful work by hand. On your own catalog, keeping the files on your machine is the natural fit.

Once the beat is built, remember the separated parts were only ever a diagnosis of someone’s mix. Stems diagnose mixes — they do not replace them is the reminder. The chops are raw material. The record is still yours to mix.

Is sampling from a separated stem legal?
How you got the stem doesn’t change the copyright. If the recording isn’t yours, licensed, or public domain, clear it. In the Sixth Circuit, Bridgeport v. Dimension Films said ‘Get a license or do not sample’; the Ninth Circuit disagreed in VMG Salsoul v. Ciccone, so the law isn’t uniform. Clearance avoids the argument.
Why does my separated drum loop have a ghost bassline?
The kick and bass share the same low frequencies, so a separator can’t fully pull them apart. In our synthetic audit, about 78% of the bleed in the drum stem sat below 120 Hz. A high-pass or a layered kick usually fixes it.
How do I get the punch back on separated drums?
Use a transient shaper, gently. In our test it lifted attack from −8.9 dB to −5.2 dB, which is past the original −7.7 dB, so go by ear. It doesn’t remove pre-echo, so trim each chop’s start to the hit.
What is a null test for stems?
Sum all the stems, flip polarity, and add the original mix. With mask-based separation the result should be near silence. After you process a stem, the same subtraction gives you exactly what you removed.
How do I change the key of a sampled bassline?
Shift by a ratio of 2^(n/12) for n semitones, with a formant-aware pitch shifter such as Rubber Band. Check the result with a tuner or FFT. Our +2 semitone shift moved 54.8 Hz to 61.3 Hz, against an expected 61.5 Hz.

A free one-pager

The checklist we run before every release.

Loudness, true peak, mono, tails, export settings and the metadata basics, on one page. Leave your email and it opens straight away, or read it here first. No email needed for that.