Created
August 8, 2026 17:11
-
-
Save zeffii/3554f5d7194f107caa8b10e25ff74a8e to your computer and use it in GitHub Desktop.
wave slicer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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