A Max/MSP external wrapping monome's softcut-lib — the
real-time audio buffer looping/resampling engine from the norns sound
computer. softkut~ exposes 6 independent softcut voices that read and write a shared (or
per-voice) Max buffer~, with sub-sample looping, crossfaded overdub, resampling read/write heads,
per-voice input and output multimode filters, per-voice output level, and voice-to-voice plus
inlet-to-voice routing matrices.
The DSP lives in a host-agnostic engine (source/include/softkut_engine.h); this Max external is a
thin shell over it. Control messages are marshalled to the audio thread through a lock-free
command queue, so parameter changes are click-free and thread-safe.
The package builds two externals over the same engine: softkut~ (discrete per-voice
inlets/outlets, 6 voices) and mc.softkut~ (a multichannel variant with a variable voice count —
see Multichannel variant below). They share an identical message
API.
git submodule update --init --recursive # max-sdk-base (softcut-lib is vendored)
make build # configure + build (universal mac binary)
make test # build + run the offline engine test harness
make build produces externals/softkut~.mxo and externals/mc.softkut~.mxo. make link symlinks
the repo into your Max Packages folder so Max can find the externals and the help patch. The build
is also a standard CMake project (cmake .. && cmake --build .).
[softkut~ <buffer~ name> <channels> @report <ms>]
<channels> is the voice count — default 1 (mono), maximum 6 — and sets how many signal
inlets and outlets the object has. So [softkut~ skbuf] is a one-voice mono looper; [softkut~ skbuf 4]
has four voices.
- Inlets (N signals): one record input per voice (voice 0..N-1). Control messages go to the left inlet.
- Outlets:
0..N-1= per-voice signal outputs ·N= message outlet (phase/position reports).
All voices share the buffer~ named in the first argument (or via set). Each voice can instead
be pointed at its own buffer~ with voicebuf (e.g. for stereo: voice 0 -> left, voice 1 -> right).
softcut needs contiguous mono sample memory and wraps buffer indices with a power-of-two bitmask. Therefore:
- The
buffer~must be mono (a multichannelbuffer~is refused with a warning). - Only the largest power-of-two prefix of the buffer's length is used; the remainder is left
untouched. softkut~ posts a note on DSP start when it trims (e.g. "using 32768 of 48000 frames").
For full use of a buffer, size it to a power of two (e.g.
buffer~ skbuf 1000≈ 48000 samples -> 32768 used; size it exactly if you need every sample).
File loading/saving/clearing is handled by Max's buffer~ itself (read, replace, write,
crop, clear), not by softkut~.
Most messages address a single voice and take the form <message> <voice> <value> where voice
is 0..5. Times are in seconds, gains/levels are linear amplitudes, flags are 0/1.
| Message | Args | Description |
|---|---|---|
play |
<voice> <0/1> |
Enable/disable playback (reading) for a voice. |
rec |
<voice> <0/1> |
Enable/disable recording (writing) for a voice. |
reconce |
<voice> <0/1> |
Record-once: records a single loop pass then auto-clears the record flag. |
stop |
<voice> |
Immediately park the voice's heads (silent), distinct from play 0. |
position |
<voice> <sec> |
Jump the play head to a position (seconds within the buffer). |
enable |
<voice> <0/1> |
Master on/off gate for a voice. When off, the voice is skipped entirely (no audio, no recording, no feedback contribution). Default on. |
| Message | Args | Description |
|---|---|---|
rate |
<voice> <ratio> |
Playback/record rate. 1 = normal, 0.5 = half speed/octave down, 2 = double, negative = reverse. |
loopstart |
<voice> <sec> |
Loop start point (seconds). |
loopend |
<voice> <sec> |
Loop end point (seconds). |
loop |
<voice> <0/1> |
Loop flag. 0 = one-shot (stops at loop end), 1 = loop. |
fade |
<voice> <sec> |
Loop/record crossfade time (seconds). |
| Message | Args | Description |
|---|---|---|
reclevel |
<voice> <amp> |
Record amplitude — how much of the input is written. |
prelevel |
<voice> <amp> |
Preserve level of existing buffer content. 0 = overwrite, 1 = full overdub (sum on top), in between = decaying overdub. |
recoffset |
<voice> <sec> |
Offset of the record head relative to the play head (seconds; default ≈ -8 samples). |
| Message | Args | Description |
|---|---|---|
prefc |
<voice> <Hz> |
Cutoff frequency. |
prerq |
<voice> <rq> |
Reciprocal Q (resonance; smaller = more resonant). |
prelp prehp prebp prebr |
<voice> <mix> |
Low-pass / high-pass / band-pass / band-reject mix amounts. |
predry |
<voice> <amp> |
Dry (unfiltered) input mix. |
prefcmod |
<voice> <amt> |
How much the cutoff tracks playback rate. |
| Message | Args | Description |
|---|---|---|
postfc |
<voice> <Hz> |
Cutoff frequency. |
postrq |
<voice> <rq> |
Reciprocal Q (resonance). |
postlp posthp postbp postbr |
<voice> <mix> |
Low-pass / high-pass / band-pass / band-reject mix amounts. |
postdry |
<voice> <amp> |
Dry (unfiltered) playback mix. |
| Message | Args | Description |
|---|---|---|
level |
<voice> <amp> |
Voice output gain. Smoothed. |
levelslew |
<voice> <sec> |
Smoothing time for level. |
Neither object has a built-in stereo mix outlet — each voice is its own output. pan/panslew are
still accepted (shared command set) but have no effect; do stereo placement downstream by routing
the voice outputs to L/R yourself, or by panning an mc.softkut~ bundle with mc.*~ objects.
| Message | Args | Description |
|---|---|---|
recpreslew |
<voice> <sec> |
Smoothing time for record/pre levels. |
rateslew |
<voice> <sec> |
Smoothing time for rate changes. |
| Message | Args | Description |
|---|---|---|
quant |
<voice> <unit> |
Quantization unit for the reported phase. |
phaseoffset |
<voice> <sec> |
Offset applied to the reported phase (seconds). |
sync |
<follow> <lead> <offset> |
Snap voice follow to voice lead's position plus offset (seconds). |
poll |
— | Emit each voice's current play position out the report outlet as position <p0> <p1> ... <p5>. |
With @report <ms> set to a non-zero interval, the report outlet also automatically emits
phase <voice> <quantphase> whenever a voice's quantized phase changes.
| Message | Args | Description |
|---|---|---|
set |
<buffer~ name> |
Point all voices at the named (shared) mono buffer~. |
voicebuf |
<voice> <buffer~ name> |
Point one voice at its own buffer~ (overrides the shared default). |
| Message | Args | Description |
|---|---|---|
inlevel |
<inlet> <voice> <gain> |
Route signal inlet (0..5) into voice's record input at gain. Defaults to identity (inlet v -> voice v at unity); the off-diagonal lets one input feed several voices. |
feedback |
<src> <dst> <gain> |
Route voice src's pre-level output into voice dst's record input at gain (one block delayed). level is an output gain only, so changing it does not alter a feedback network (this matches softcut/norns). Enables overdub/looping networks; self-feedback (src == dst) is allowed — mind stability. |
A voice's record input is therefore: sum over inlets ( inlet * inlevel[inlet][voice] ) + sum over src ( rawVoiceOutput[src] * feedback[src][voice] ), where rawVoiceOutput is the voice's softcut
output before level and pan are applied.
| Message | Args | Description |
|---|---|---|
reset |
— | Reset all voices and routing to defaults (rate 1, 1 s loop, looping on, level 1, no feedback, identity input routing). |
| Attribute | Description |
|---|---|
@report <ms> |
Phase-report interval in milliseconds (0 = off, the default). |
Basic loop — record 2 seconds into a mono buffer, then loop it:
[buffer~ skbuf 2000] (mono, ~96000 samples -> 65536 used at 48k)
[softkut~ skbuf]
set skbuf
loopstart 0 0 , loopend 0 2 , loop 0 1
rec 0 1 , play 0 1 (record while monitoring)
... after 2 s ...
rec 0 0 (stop recording, keep looping)
Stereo — a two-voice instance, each voice on its own buffer and output; route outlet 0 to the left channel and outlet 1 to the right:
[buffer~ bufL 2000] [buffer~ bufR 2000]
[softkut~ skbuf 2] (2 voices -> outlets 0 and 1)
voicebuf 0 bufL , voicebuf 1 bufR
Feedback overdub — feed voice 0 into voice 1 to build layers:
feedback 0 1 0.8 (voice 0 raw output -> voice 1 record at 0.8)
mc.softkut~ is the MC (multichannel) build of the same engine — instead of discrete per-voice
inlets/outlets it uses Max's multichannel signals:
[mc.softkut~ <buffer~ name> <voices>] e.g. [mc.softkut~ skbuf 8]
- Inlet (1): a multichannel record input — channel v feeds voice v's record input
(
Z_MC_INLETS, so a single mono cord or an N-channel cord both work). Control messages go here. - Outlet 0: multichannel voice outputs, one channel per voice (
mc.unpack~ <voices>to split). - Outlet 1: message outlet (phase/position reports).
The voice count is a creation argument (default 6, capped at 16) and becomes the channel count of
outlet 0. Everything else — the entire message API above, including the inlevel matrix (whose
"inlet" index is now the input channel index) — is identical, since both objects share the same
engine. There is no built-in stereo mix on mc.softkut~: the per-voice level still applies to the
bundle, but stereo placement is left to downstream mc.*~ objects (so pan/panslew have no audible
effect here). Drop the voice bundle straight into mc.*~ chains, or pan/sum it as you like.
source/include/softkut_engine.h— host-agnostic engine (voices, command queue, level/pan, routing matrices, buffer framing, runtime voice count). No Max dependency.source/include/softkut_control.h— shared control-message table + dispatch (used by both shells).source/projects/softkut_tilde/softkut~.cpp— discrete Max shell (6 voices).source/projects/mc.softkut_tilde/mc.softkut~.cpp— multichannel shell (variable voices).source/tests/test_engine.cpp— offline test harness (make test).source/thirdparty/softcut-lib— the upstream softcut library (built statically).help/softkut~.maxhelp— help patch.docs/softkut~.maxref— structured object reference (Max ref panel).CHANGELOG.md— release notes.TODO.md— deferred work (fade-curve shaping).
softkut~ wraps softcut-lib by monome (ezra buchla et al.);
see source/thirdparty/softcut-lib for its license. The Max object scaffolding follows common
buffer~/MSP idioms (originally modeled on the karma~ external).