With the help of the ChucK mailing list, especially this post by Kassen, I put together this handy class, Cruncher, which quantizes at the desired bit level, and for kicks, adds some interesting distortion.
public class Cruncher {
// connect with these
Gain input;
LiSa output;
// private
int bits;
float levels;
Gain mix;
Step dc;
// public
fun void setBits( int num ) {
num => bits;
Math.pow( 2, bits ) => levels;
// pre-calculate some constants for the loop
Math.pow( bits - 1, 2 ) => float QUANT;
(output.duration()/ samp) $ int => int NUM_SAMPS;
2.0 / NUM_SAMPS => float STEP_SIZE;
// fill LiSa buffer with quantization map
for( int x; x< NUM_SAMPS; x++ ) {
-1 + x * STEP_SIZE => float in; // calculate input value as [-1, 1)
( in * QUANT ) $ int / QUANT => float value; // quantize
ulaw( value ) => value; // distort
output.valueAt( value, x::samp ); // save
}
}
// private
fun float sgn( float f ) {
return f >= 0 ? 1. : -1.;
}
fun float ulaw( float f ) {
return Math.sgn(f) * Math.log( 1 + levels * Std.fabs( f ) ) / Math.log( 1 + levels );
}
fun void initialize() {
input => output;
dc => output;
// configure LiSa
second => output.duration;
1 => output.sync;
1 => output.play;
// map input from [-1, 1] to (0, 1)
.49 => input.gain;
.5 => dc.next;
// set default quantization level
setBits( 8 );
}
initialize();
}
And here is a small patch that uses it (and requires you to load scale.ck ahead of time):
// ugens
Cruncher cruncher;
TriOsc osc;
ADSR env;
// patch
osc => env => cruncher.input;
cruncher.output => dac;
// configure
env.set( 1::ms, 40::ms, .1, 300::ms );
cruncher.setBits( 5 );
// GO!
Scale sc;
[ 0, 1, 3, 8, 6, 4 ] @=> int notes[];
while( true ) {
for( 0 => int i; i < notes.size(); i++ ) {
sc.scale( notes[ i ], sc.maj ) + 60 => Std.mtof => osc.freq;
env.keyOn( 1 );
second / 6 => now;
env.keyOff( 1 );
second / 7 => now;
}
}
Hear the original, then the 5-bit version, then the 5-bit, distorted version.