Attached is a file with the three different formulas I gave last time for two-bit quantization, just for illustration -- any of these could work as a general bit-crusher with a little extra effort. Number one has a variant you can toggle on or off. Also notice how positive max-amp is an exception in numbers one and three -- it can be clipped out though.
Matt
On Thu, Apr 29, 2010 at 9:56 PM, Matt Barber brbrofsvl@gmail.com wrote:
Reduced bit depth (which is what I think 'bit-crushing' means) can be achieved by dividing the signal by x, pass it through something like [int~], multiply it by x again. An [int~] can be implemented by using [wrap~] and [-~], which are both vanilla.
It is also worth considering adding a DC offset. Your [wrap~] solution implements a floor-function, that is, rounding downwards. You can make it round to closest, by adding x/2 before rounding downwards.
[expr int($v1)] is rounding zerowards (thus output zero corresponds to a twice bigger input range as any other input). Thus it will behave in a weird unequal way.
When the volume gets low, the _effective_ bits-per-sample gets very low, because the relative precision of integers is proportional to amplitude (which is not the case with floats). Therefore, even with 16-bit audio, if your amplitude is 0.0001 times the max, it will feel as if it were 3-bit audio. In such circumstances, the differences between the possible roundings will become quite audible.
Bit-depth transitions are really complicated things, especially when going from normalized float to int (and among different utitlities which do this, e.g. libsndfile or ecasound, I've found several different standards). The biggest problem is what to do with the asymmetry about zero with int representations -- there are more negative numbers than positive in a two's complement int system.
Here are three formulas I've used for noise-shaping dither in csound (the first one is more compatible with the bit-depth transitions that occur when csound writes a file).
let X = the input signal and N = target bits:
- Y=(floor([2^(N-1)]*X)+0.5)/[2^(N-1)]
- Y=floor([2^(N-1)]*X+0.5)/[2^(N-1)]
- Y=floor([2^(N-1)]*X)/[2^(N-1)]
The only difference is whether the 0.5 is added, and whether it's added before or after the floor function.
So let's say you're moving from floats (-1 to +1) to 8-bits:
In the first, one multiplies the signal by 2^7, getting a signal which varies from -128 to +128 (floats). Flooring that will result in ints in the same range (+128 after the floor will be a relative rarity in real signals, though, so the effective range is -128 to 127). Adding 0.5 to all of that gives a range of -127.5 to 127.5 (with the very rarely encountered 128.5). Then division by 128 gives a signal which varies between two values equally spaced from 0, but a constant 0 in will result in a DC-offset out (it's a mid-rise function with 256 possible values).
In the second, one multiplies the signal by 2^7, getting a signal which varies from -128 to +128 (floats). Adding 0.5 to this will result in a signal that varies between -127.5 to +128.5, and flooring this gives -128 to +128 (ints). This time, +128 will not be as rare. This also is really just a rounding function, with x.5 values rounded up. It's a mid-tread function, so zeros in will result in zeros out. At first glance it may seem as though this has one more than 2^8 values (128 negative values, 128 positive values, and 0 = 257 total possible values). However, if one were to graph the rounding function, one would find that anything that started out between -127.5 and +127.5 will be rounded to -127 to +127 (255 values), while anything lower than -127.5 will round to -128, and anything higher than 127.5 will round to +128 -- the extremes of the staircase function have half-quantization-step mappings, so the total effect is still 8-bit. This one has the advantage that both zero in and maxamp (positive or negative) give "correct" output values as well, but the disadvantage that one of the quantized steps is divided between the top and the bottom of the total range. Also, its graph is a reflection of that of the first function about a diagonal.
In the third, one starts with the same math as the first, up to the floor function, which gives an effective range of -128 to 127. Then one simply divides by the original scalar, which just gives the mid-tread version of the function from the first formula (the first formula minus half the quantization step), with zeros-in-zeros out but one extra negative value than positive.
Maybe sometime later I could post some pictures for two-bit audio. I'm sure there are plenty on the web. There's also the question of whether, even for a bit-crushing effect, one would add dither.
Matt