Skip to content

Latest commit

 

History

262 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

grainneukeln — a rhythm hallucinator

In one breath: give it audio you already have — a song, a field recording, a voice memo — and it rebuilds it into new audio. It slices the sound into tiny grains lined up to the beat, then stitches those grains back together in a shuffled, re-filtered order. The result keeps the original's pulse and texture but becomes something new — hypnotic, glitchy, remixed. That's granular resynthesis, driven by the source's own rhythm.

Two runs with the same settings give two different tracks (grain picks are random) — but both stay locked to the source's groove.

grainneukeln TUI


Why "rhythm hallucinator"?

Beat detection here never asks whether the audio is rhythmic — it fits a pulse to whatever it can find, and it always has a default tempo in mind (~120 BPM). Feed it literal white noise and it will confidently hear 23 beats at 112 BPM and grind to it. Feed it a real 400 ms click track and it nails 400.1 ms. It runs in two regimes — locking to a real pulse, or hallucinating one where there is none — and it never tells you which. That's not a bug; it's the instrument. The imagined grid is what lets you grind a rainstorm or a room's silence into something that grooves.

The one thing to remember: beatless input is a use case, not a failure — but a source with 0 detected beats gives the grinder nothing and yields a dead mix. A few beats (real or hallucinated) is all it needs.

→ The full story, the measured proof, and the step-by-step pipeline: docs/HOW_IT_WORKS.md.


Install (Python 3.12+, no Conda)

uv venv .venv && . .venv/bin/activate     # or: python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt           # TUI/GUI extras are optional
# system prerequisite: ffmpeg  (pydub uses it for mp3/m4a/webm)

Beat detection and time-stretch use librosa — no madmom/rubberband build needed. The Conda flow still works but isn't required.


Use it — command line

python main.py <audio file or YouTube URL> <output dir> [amc params]
# default automix — fast path, raw grains (no band-pass filtering)
python main.py song.mp3 output/

# half-length grains, each grain a touch faster, whole mix a touch slower
python main.py song.mp3 output/ amc l /2 ss 1.2 s 0.9

# two frequency bands (bass + air), tighter windows — opts into band-pass filtering
python main.py song.mp3 output/ amc c 1,250;10000,15000 w 6

# reproducible render — same seed + same params = byte-identical output
python main.py song.mp3 output/ amc seed 42 l /2 ss 1.2

# a whole sweep at once — one file per grain length
python main.py song.mp3 output/ amc l [/2,/3,/4]

You can also pass a YouTube URL or an "artist - track" search string in place of a file, and it will fetch the audio first.


Record the room — live mic as a source

The fourth source, a peer of file / URL / search: the room you are sitting in.

python main.py --list-inputs                 # what this node can actually record from
python main.py --record 15 "" output/ amc l /2   # 15s off the mic, then grind it
python main.py --record 15 --record-device alsa_input.pci-0000_2d.analog-stereo "" output/

In the TUI it is the ● REC button in the source panel, or ctrl+g from anywhere. Press to start, press again to stop; the take lands in <output>/recordings/ and is loaded as the source through the same pipeline a file takes.

Every take is measured before it is used. A muted, unplugged, or already-held mic produces a perfectly well-formed wav full of zeros — indistinguishable from a good recording by every check except the one that looks at the samples. So a silent or too-short take is reported and refused rather than handed to the grinder, and kept on disk so you can load it by hand if you meant it:

SILENT capture — 2.0s · 44100Hz · 1ch · rms 1 · peak 4 (below rms 8: nothing was on the input)
  · pcmC0D0c is held by pid 1127493: arecord -D plughw:CARD=Camera,DEV=0

That second line is the difference between "the mic is broken" and "something else already has it". Capture backends are tried best-first — pw-record, parecord, ffmpeg, then arecord last because raw ALSA is exclusive and locks the card away from every other client on the machine. Pin one with --record-backend if you want that.

A .monitor source records what this node is playing rather than the room — a legitimate source (grind your own speakers), which is why the picker labels it instead of hiding it.


The pult — grind from your phone

python main.py --pult output/        # prints a URL with a token; open it on the phone
python main.py --pult 9000 output/   # a different port
python main.py --pult --pult-bind 127.0.0.1   # this host only
grainneukeln pult
  output   /home/you/grainneukeln/output
  open     http://192.168.8.224:8731/?t=Ur3k9Xq2p

A single mobile page with the same three verbs the TUI has — record · load · grind — plus a player for what came out. Params go in as an amc line (l /2 ss 1.2 s 0.9), the same grammar the TUI's command bar and the CLI take.

It is not a remote control for a running TUI: there is no IPC into a Textual app, and one would make the phone's buttons work only while a particular tmux pane happened to be alive. Instead it runs its own session and shares the TUI's checkpoint file, so params set on the phone are there when the TUI next starts, and the other way round.

Two things to know before you leave it running:

  • The token is not optional. Binding to 0.0.0.0 is the whole point — a phone has to reach it — and a LAN is not a trusted room. Every API route needs the token; it arrives once in the URL and then lives in the browser's local storage. Anyone who can reach the port and has the token can record and grind on this machine. Pin your own with $GRAINNEUKELN_PULT_TOKEN.
  • LAN only. Do not port-forward it. There is no TLS and no rate limiting; it is a pult for a room, not a service.

APK later — the page is a plain URL in the meantime, and it installs to a phone home screen as a web app.


Use it — TUI (recommended, headless-friendly)

python main.py --tui
# or: ./run_tui.sh
python main.py song.mp3 out/ --tui              # preload a source, render into out/
python main.py "Aphex Twin - Rhubarb" out/ --tui --seed 42

A keyboard-driven terminal UI you can run over SSH inside tmux — no display server needed. It reaches everything the command line reaches: the amc grammar is a first-class surface here (via the command bar, Ctrl+E), not a subset re-expressed as widgets, and the line above it is the live recipe — exactly what Run will render, ready to copy out or paste in.

  • Source (Ctrl+1): file · YouTube URL · artist - track search · optional Source B.
  • Params / Mixer / Bands (Ctrl+2/3/4): every amc knob, live. Bands are raw or filtered — a raw band skips the band-pass filter and grinds ~27× faster; each row says which it is, so a slow grind is never a mystery.
  • Run (Ctrl+5): WAV / verbose / self-feed / low-mem toggles, the series sweep, progress + log.
  • Uxn ROM control (Ctrl+6): drive the params from a portable Uxn sequencer ROM, with a dry-run Preview that shows the whole plan without spending a render.
  • Outputs (Ctrl+O): renders newest-first with size + age; non-blocking play/pause/seek.

Crash-tolerant: the session is checkpointed before every grind, so a crash (even an OOM) loses only the render; Ctrl+T shows the recipe that bombed. F1 / ? opens the full keymap + amc grammar on one screen:

grainneukeln TUI help

→ Full walkthrough: docs/TUI_GUIDE.md. TUI extra: textual.


Use it — GUI

python main.py with no arguments launches the PySide6 desktop GUI: load from file or YouTube, detect beats, configure and run the automixer, play and save. (GUI extras: PySide6, pyqtgraph.) Needs a display server — on a headless box, use the TUI above.


The knobs (amc)

The amc block is a list of key value pairs. The handful you'll reach for most:

param meaning example in short
l grain length l /2, l *3, l 250 /N·*N scale the beat-derived default by ratio (stays on the grid); a bare number is absolute ms. Shorter = finer, more fragmented.
s whole-mix speed s 0.8 tempo of the final track (pitch preserved). <1 slower, >1 faster.
ss per-grain speed ss 1.2 tempo of each grain (pitch preserved) — warps the micro-texture.
c channels / bands c 0,250;250,15000 Opt-in band-pass filtering. Each low,high band pulls its own grain and they're layered. Omit it for the fast raw-grain default (4–5× faster).
w window divider w 4 windows = total_beats / w. Bigger w → tighter windows → more local grain picks.
m mode m rw, m q, m poly, m lib grain-selection algorithm (see below). rw is the default.
seed RNG seed seed 42 reproducibility — same params + seed = byte-identical output. Also --seed 42.

There are more — per-grain envelope taper (env) and reverse (rv), a second source (src2), snap-to-beat (snap), swing (sw), euclidean patterns (ek/en), cyclic patterns (pat/cyc/rot/acc — see below), poly streams (pr), library clustering (lib/lk), and series sweeps ([...]).

Cyclic patterns — grind on a real timeline

E(k,n) only knows "k hits spread as evenly as the integers allow", over exactly one beat. pat replaces it with an explicit cycle: a named timeline from the library, an x..x..x. string, or an additive meter +2,2,2,3 whose gaps are deliberately uneven. cyc says how many beats the cycle spans (a 16-pulse clave wants four — crammed into one beat it is a blur), rot rotates it, and acc is a per-slot accent map in dB. That last one is what makes an all-hit cycle like a teental theka or a gnawa qraqeb stream a groove instead of a wash: those traditions carry the rhythm in which strokes are accented, not in which slots are struck.

python main.py djembe.wav out/ amc m q pat bembe snap        # the 12/8 standard bell
python main.py tabla.wav  out/ amc m q pat teental snap      # 16 matras, khali at 9
python main.py doira.wav  out/ amc m q pat aksak9 snap       # 9/8 limping meter
python main.py song.mp3   out/ amc m q pat clave32 rot 4 acc 0,-9
python main.py song.mp3   out/ amc m q pat list              # the library, with traditions

./ethnic_grind.sh is a worked batch: thirteen Asian and African percussion recordings, each ground on a timeline from its own tradition, plus cross-continental src2 grafts — every render checked against its source before it counts.

→ Every parameter, the cross-mode matrix, and recipes: docs/PARAMETERS.md.

The four modes at a glance

mode what it does try
rw random window (default) — picks a random beat per grain; the groove is whatever the source had. amc m rw
q quantized euclidean — fires grains only on the slots a pattern E(k,n) marks: designed grooves (tresillo, cinquillo…) over the source's onsets. amc m q ek 3 en 8
poly phasing polyrhythm — N parallel streams at different subdivisions, overlaid so they phase (Reich-style). amc m poly pr 4;3
lib feature library — measures every grain on 4 axes, clusters them, and sequences by similarity (sim, hypnotic) or contrast (con, glitchy). amc m lib con lk 8

→ The code-level algorithm for each: docs/ALGORITHMS.md.

Series runs — sweep in one command

Wrap any sweepable param's value in square brackets and amc renders the cartesian product — one file per combination:

python main.py song.mp3 output/ amc l [/2,/3,/4]        # 3 renders, grain length varies
python main.py song.mp3 output/ amc l [100:300:50]      # numeric range → 100,150,200,250,300
python main.py song.mp3 output/ amc s [0.8,1.0,1.2] ss [1.0,1.5]   # 6 renders, speed × sample-speed
python main.py song.mp3 output/ amc seed [1,2,3,4,5]    # 5 renders, same recipe, different grain picks
python main.py djembe.wav out/ amc m q pat [bembe,clave32,teental,aksak9]   # the timeline sweep

The timeline sweep is how you find out which metric universe a recording belongs to: the same source ground on a 12/8 bell, a 16-pulse clave, a 16-matra theka and a 9/8 aksak, one command. (An additive pattern sweeps written with semicolons — pat [+2;2;3,bembe] — since the series body splits on commas. acc cannot be swept for the same reason, and says so instead of quietly rendering once.)

[a,b,c] is an explicit list; [start:stop:step] is an inclusive numeric range. Quote the brackets in the shell to defeat globbing. Each render's filename encodes its own params so you can tell combinations apart. The TUI's Run panel and the interactive shell (am N runs combination #N) expose the same grammar. Full grammar and the sweepable-param list are in docs/PARAMETERS.md.

Interactive shell

Run without any amc params to drop into an interactive cutter (python main.py song.mp3 output/):

command does
p play the current selection
b <ms> / l <ms> set selection start / length
s <ms> set the step size for f/r
f / r step forward / rewind (repeat the letter to go further: fff)
cut / cut -a export the current selection (-a snaps to a nearby amplitude peak)
autocut [n] export many cuts automatically
am / am N automix the whole track (am N renders combination #N after a series amc)
amc … / amc info set automix params / show the current config
load <file> load a different track
plot / info view amplitude / current settings
set_wav_enabled / set_wav_disabled also export WAV alongside MP3
help / q help / quit

Learn more

doc what's in it
docs/HOW_IT_WORKS.md the rhythm-hallucinator character (with measured proof), the pipeline step by step, and performance benchmarks
docs/PARAMETERS.md every amc parameter, the cross-mode matrix, series-run grammar, and recipes
docs/ALGORITHMS.md the code-level machinery of each mixer — euclidean generator, polyrhythm math, lib clustering + Markov sequencing, loudness, determinism
docs/TUI_GUIDE.md the full TUI walkthrough — every panel, keybinding, and the Uxn ROM control layer
uxn_ctrl/README.md driving a whole render sequence from a portable Uxn ROM (--uxn-ctrl, --uxn-feedback)

Docker

./run_granular_sampler.sh

Builds the image and runs the container with settings for your OS. Windows users may need an X server (e.g. VcXsrv) for the GUI.

Contributing

Contributions welcome — please open a Pull Request.

Releases

Packages

Used by

Contributors

Languages