ꘇ chop.quantize
Keep your notes in key without a music degree
- input
- channels
- value
chop.quantize does one thing: it takes a GENERETI_CHOP, snaps the note channels to the nearest pitch in a scale, and passes everything else through. That's it. One output FLOAT, one CHOP, one transformation. And it's the node that decides whether your procedurally generated melody sounds like music or like someone mashing a piano with a broom.
It's part of the Performance family in ꘇ Genereti (languel/genereti, by Liuboto, MIT, super-alpha) - the layer that gives a ComfyUI workflow a clock, a scale, and automation. Quantization is the payoff for the first two being in the same pack.
How the snapping works
The implementation is worth describing because it explains the behavior you'll see. The node takes a scale context - degrees as semitone offsets, root as a pitch class - and for each incoming note computes the nearest allowed pitch, but only searching the octave below, the octave of the note, and the octave above. Then it wraps that in a plain nearest-neighbour search: whichever legal pitch is closest wins. Ties fall to the earlier candidate, which is a detail you only notice on exact midpoints and won't care about.
Which channels get touched is the slightly quirky part. A channel is quantized if:
- it's named
note, or starts withnote(soch1.note60-style names from the MIDI-friendly side of the pack get handled), or - the input has exactly one channel
Everything else is copied through untouched. So gate, velocity, step, and any analysis channel you've merged in are unaffected. If you're feeding a single arbitrary channel in from a sensor, it gets treated as pitch - which is either exactly what you wanted or a surprise, depending on the graph.
Also worth knowing: the output is built as a new dict with a new channels mapping, and each quantized channel is stored as float32 with fractional values preserved. You get MIDI-ish pitch numbers that can be non-integer.
Inputs and outputs
Required: input, the CHOP to quantize. Optional: music, a STRING - that's the JSON that ꘇ mod.scale emits. If you leave music empty, the node takes the music document out of the performance snapshot instead (the second optional input, a JSON string), and if that's empty it falls back to a minor scale rooted at 0. So "no configuration" isn't an error state; it's A minor.
Outputs: channels (GENERETI_CHOP) and value (FLOAT). value is the last sample of the first channel in the output order - helpful as a control value, useless as an event.
The natural chain is chop.sequencer or chop.arpeggiator → chop.quantize → chop.synth or mod.synth, with mod.scale feeding the music input. Three nodes and you have a pattern that can't play a wrong note.
Install
Manager → search "Genereti" → install ꘇ Genereti → restart → refresh the browser. Manual path:
git clone https://github.com/languel/genereti.git custom_nodes/genereti
./.venv/bin/python -m pip install -r custom_nodes/genereti/requirements.txt
The only pinned dependency is comfyui-frontend-package>=1.53.6; no models, no extra servers. Needs ComfyUI 0.38.2+, Python 3.11+. Examples install under Workflows → Genereti.
Gotchas
A connected music input beats everything. The node reads music first, then the performance snapshot's music document, then the hard-coded minor scale. If your quantizer is ignoring the scale you set up in the timeline, it's because something is wired into music - or because a stale string is sitting in the widget from an earlier experiment. Clear it.
Watch what you feed it. Quantizing something that isn't pitch (a spectrum magnitude, an audio RMS, a hand position) will produce plausible-looking numbers that mean nothing musically. Use chop.select upstream so only note channels arrive.
Queue is fine here, unlike most of the family. This node is pure arithmetic on the input block plus whatever snapshot it's given, so a queued run behaves like a live one - same input, same output. Nothing plays, nothing is scheduled, no devices open. If you're debugging a musical patch, this is a good place to test that your CHOP data itself is sane.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| input | GENERETI_CHOP | — | |
| musicopt | STRING | — | |
| performanceopt | STRING | {} | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| channels | GENERETI_CHOP | — |
| value | FLOAT | — |