Reconstructing the Ableton velocity compand curve in Max for Live
Posted on January 3, 2021The Velocity effect in Ableton Live allows us to apply some transformations to the velocity of incoming MIDI notes.
In this brief blog post we will look at how we might recreate this velocity curve; specifically, the Comp
part of the curve. The goal won’t necessarily be to recreate the curve exactly, but rather to have something that functions similarly.
Companding
Comp
stands for companding, which is a combination of two words compression and expanding. It is a standard algorithm in signal processing. There are many algorithms for doing this, but for our purposes a suitable choice is μ-law companding.
Compression
Compression happens when we concentrate the range of values near the two extremes (very loud or very soft), rather than near the middle. In the Velocity effect, this is presented by a positive value of the Comp
parameter, but we will focus on the standard formulas first before relating it back to the Comp
parameter.
The formula for μ-law compression is pretty straight-forward. The intuition is that to compress a signal x
, we take the logarithm in base μ (log
μ(x)
); to expand it again, we compute μ
x. The most important difference between this and the actual formulas is that they add some twiddling to avoid artefacts near zero.
The standard formula for μ-law compression is
or in gnuplot
code:
sgn(x) * log(1 + mu * abs(x)) / log(1 + mu)
(Note that log
can be logarithm in any base; we’re effectively computing a logarithm in base 1 + mu
here.)
The parameter μ in the formula serves a similar function to the Comp
knob: small values of μ compress the curve barely at all; large values compress it a lot. The following graphs shows the effect of this formula for different values of μ:
The range of this graph is from -1
to +1
on both axis; see side note on the original use of this formula in signal processing below. However, if we squint a bit and think of this graph as representing a mapping from velocity values between 0
and 127
on both axis (the MIDI velocity range), observe the effect the compression has: most values will either up really loud or really soft, with little nuance in the middle.
Although larger values of μ do compress more, we have to increase μ quite quickly; in this graph we increased it by an order of magnitude at each step. We will map this back to a parameter in the range 0
to 1
(like the Velocity effect does) below.
Side note: use in signal processing
If you think of the compression graph above in terms of what it does to a wave form, then small changes in the input signal near zero have a large effect on the output signal, but small changes the input signal near one only have a small effect on the output signal.
If we then represent this signal as, say, 8 bits samples, we are effectively using more bits to distinguish values near zero and fewer bits to distinguish values near one. This is useful, because the human ear is more sensitive to small changes in soft sounds than it is in small changes in loud sounds.
In this case, the translation is not done as an effect, but as an encoding technique. Compression is applied before the signal is encoded, and at the decoder side, expansion is applied before the signal is played back.
Expansion
If compression concentrates velocity values near the extremes, expansion concretates velocity near the middle (neither very loud nor very soft). It’s what happens when we dial to the Comp
knob in the Velocity effect to negative values.
The standard formula for μ-law expansion is
or in gnuplot
code:
sgn(x) * (1.0/mu) * (((1.0 + mu) ** abs(x)) - 1.0)
Here is a graph of what this function looks like for various values of μ:
Velocity curves
In order to translate the above formulas/graphs into something more similar to Ableton’s velocity curves, we need to do two things: we need to work in a range of [0:127]
instead of [-1:+1]
, and we need to translate from a parameter in the range [0:1]
(the value of the Comp
knob) to the mu
parameter.
To map a value in the range [0:127]
to [-1:+1]
, we can just translate
in(x) = -1 + (x / 127) * 2
out(y) = (y + 1) / 2 * 127
(Notice that out
is the inverse of in
: out(in(x)) = x
). To define mu
in terms of the Comp
parameter (let’s call it f
), we need to map a value between [0:1]
to, say, [0:100]
. One way we could do that is as follows:
mu(f) = 10.0 ** (2 * f)
Compression
This means we end up with the following gnuplot
code for compression
out(sgn(in(x)) * log(1 + mu(f) * abs(in(x))) / log(1 + mu(f)))
with corresponding graph
Expansion
For expansion, we end up with the gnuplot
code
out(sgn(in(x)) * (1.0/mu(f)) * (((1.0 + mu(f)) ** abs(in(x))) - 1.0))
and graph
Putting it all together
To turn all this into a Max for Live patcher, we can just implement this math in JavaScript (or in Max for Live itself, if you prefer), and set up an itable
that can serve as a lookup table for velocity translations, and is updated by the JavaScript code every time the Comp
parameter is changed.
The device itself can just consist of a live.dial
to serve as a knob to change the Comp
parameter, a js
object to run the JavaScript code, and the itable
itself:
As for the JavaScript code itself, you can download velocity.js; just place it in the same directory as your device, and you should be good to go.