Skip to content

Instantly share code, notes, and snippets.

@zeffii
Created August 8, 2026 17:11
Show Gist options
  • Select an option

  • Save zeffii/3554f5d7194f107caa8b10e25ff74a8e to your computer and use it in GitHub Desktop.

Select an option

Save zeffii/3554f5d7194f107caa8b10e25ff74a8e to your computer and use it in GitHub Desktop.
wave slicer
#!/usr/bin/env python3
import argparse
from pathlib import Path
import librosa
import soundfile as sf
import numpy as np
def make_one_shots(input_dir, output_dir):
input_dir = Path(input_dir)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
for wav_path in input_dir.glob("*.wav"):
print(f"Processing: {wav_path.name}")
y, sr = librosa.load(wav_path, sr=None, mono=False)
# Use mono signal for onset detection
mono = np.mean(y, axis=0) if y.ndim > 1 else y
# Percussive component tends to give cleaner drum/transient onsets
percussive = librosa.effects.percussive(mono)
# Detect transients using onset strength
onset_env = librosa.onset.onset_strength(
y=percussive,
sr=sr,
aggregate=np.median,
)
onset_frames = librosa.onset.onset_detect(
onset_envelope=onset_env,
sr=sr,
units="frames",
backtrack=True,
pre_max=3,
post_max=3,
pre_avg=3,
post_avg=5,
delta=0.2,
wait=3,
)
starts = librosa.frames_to_samples(onset_frames)
if len(starts) == 0:
print(" No onsets found.")
continue
# Add end of file as final boundary
boundaries = np.append(starts, y.shape[-1])
for i in range(len(boundaries) - 1):
start = boundaries[i]
end = boundaries[i + 1]
if end <= start:
continue
shot = y[..., start:end]
# Remove very quiet material at beginning/end
if shot.ndim > 1:
trim_signal = np.max(np.abs(shot), axis=0)
else:
trim_signal = np.abs(shot)
trimmed, trim_idx = librosa.effects.trim(
trim_signal,
top_db=35,
)
trim_start, trim_end = trim_idx
shot = shot[..., trim_start:trim_end]
# Ignore extremely short slices
if shot.shape[-1] < int(sr * 0.02):
continue
# 1 ms fade in/out to prevent clicks
fade_len = min(int(sr * 0.001), shot.shape[-1] // 2)
if fade_len > 0:
fade_in = np.linspace(0, 1, fade_len)
fade_out = np.linspace(1, 0, fade_len)
if shot.ndim > 1:
shot[..., :fade_len] *= fade_in
shot[..., -fade_len:] *= fade_out
else:
shot[:fade_len] *= fade_in
shot[-fade_len:] *= fade_out
out_name = f"{wav_path.stem}_{i + 1:03d}.wav"
sf.write(output_dir / out_name, shot.T if shot.ndim > 1 else shot, sr)
print(f" Found {len(starts)} onsets.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Slice WAV files into one-shots using percussion onset detection."
)
parser.add_argument("folder", help="Folder containing WAV files")
parser.add_argument(
"-o",
"--output",
default=None,
help="Output folder (default: <folder>/one_shots)",
)
args = parser.parse_args()
input_dir = Path(args.folder)
output_dir = Path(args.output) if args.output else input_dir / "one_shots"
make_one_shots(input_dir, output_dir)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment