[chuck-users] 8-bit?

Tom Lieber tom at alltom.com
Sun Nov 28 02:46:50 EST 2010


2010/11/27 Kassen <signal.automatique at gmail.com>:
>> Me too! Though I just realized that 35 and 36 points correspond to
>> coefs array sizes of 104 and 107. So I searched ChucK for "100" and
>> found:
>>
>>  #define genX_MAX_COEFFS 100
>>
>> D'oh.
>
> Ok... does that make any sense at all, as a limit? Wouldn't ChucK memory be
> divided in some sort of blocks?

Hm, not sure what you mean by blocks, but the problem seems to be that
while every Gen* checks that you don't provide too many points by
comparing with genX_MAX_COEFFS, CurveTable compares with a separate
variable, MAX_CURVE_PTS, which is set to 256, even though that
function creates a coeffs array only genX_MAX_COEFFS elements long.

The patch to fix it would probably look like this:

========
diff --git a/src/ugen_osc.cpp b/src/ugen_osc.cpp
index 6a31ddc..deb3469 100644
--- a/src/ugen_osc.cpp
+++ b/src/ugen_osc.cpp
@@ -1461,7 +1461,7 @@ CK_DLL_CTRL( curve_coeffs )
     t_CKINT i, points, nargs, seglen = 0, len = genX_tableSize;
     t_CKDOUBLE factor, *ptr, xmax=0.0;
     t_CKDOUBLE time[MAX_CURVE_PTS], value[MAX_CURVE_PTS], alpha[MAX_CURVE_PTS];
-    t_CKFLOAT coeffs[genX_MAX_COEFFS];
+    t_CKFLOAT coeffs[MAX_CURVE_PTS * 3];
     t_CKUINT ii = 0;
     t_CKFLOAT v = 0.0;

========

In fact, maybe I'll publish that branch to my ChucK mirror...

>> Ohh, I'm definitely going to play with that. Was it logarithmic, or
>> something less regular?
>
> Well, say that we are using n bits to describe a range from 0V to 1V. First
> we'll consider bit 0. Assume this one is the most significant one. If it's
> high it should contribute .5V to the total. However, the resistors used
> would be cheap and may have as much as a 10% error (medical and military
> grade ones with smaller margins would be lots more expensive, if available).
> Because of this and depending on the exact properties of the device in our
> hands we'd get something like .47V instead. Let's consider bit 1 and say
> it's high too. This should contribute .25V. In practice it might instead add
> .26 . At this point the total value should be .750000 but instead it will be
> (.47 + .26 =) .73 . Repeat for all bits.
> I think you can assume the error per resistor to stay constant over the use
> of the "dac", for pieces of a realistic length. I also think that a 10%
> margin of error is about realistic, maybe we have members who used to solder
> back in the mid 80's who will know more.
> Oh, and of course these used plain analogue LPF's, not some sort of
> phase-linear FIR filter over a over-sampled version of the signal like
> modern soundcards. For the ultimate in realism of emulating old digital
> stuff note that often compander (compressor / expander) chips were used
> to suppress noise. Those might well be a bigger factor in the "punch"
> instruments like the MPC brought to genres like HipHop than the low
> bit-depth and rate on their own.
> There is a whole world of fascinating phenomena there.

Yes there is! So much more than I ever knew I wanted to know. I wonder
if I will one day know it.

It's interesting to see this on the Wikipedia page about the original
Gameboy's audio: "2 square waves, 1 programmable 32-sample 4-bit PCM
wave, 1 white noise, and one audio input from the cartridge" That's...
a little more constrained than I thought it was. And it still sounds
like this:

  http://www.youtube.com/watch?v=NmCCQxVBfyM

Or maybe the music was pre-rendered, I guess I don't know.

Anyway... either I'm implementing this wrong (naively?), or they'd
have needed much smaller error than 10% to get any two devices to
sound alike... unless the envelope is moved to after the quantization,
in which case it sounds great no matter what you do. :D

  // this code no longer needs scale.ck, yay

  // sub-class and override valueFor() to define
  // whatever type of wavetable you want
  class SuperTable {
      // connect these
      Gain input;
      LiSa output;

      // private

      Gain mix;
      Step dc;

      // public

      // recalculates the table
      fun void recalculate() {
          (output.duration()/ samp) $ int => int NUM_SAMPS;
          1.0 / NUM_SAMPS => float STEP_SIZE;

          // fill LiSa buffer with quantization map
          for( int x; x < NUM_SAMPS; x++ ) {
              output.valueAt( 2.0 * valueFor( x * STEP_SIZE ) - 1.0,
x::samp ); // save
          }
      }

      // private

      // override this function to provide the value that
      // should be output for the given input
      // ('in' ranges from 0 to 1)
      fun float valueFor( float in ) {
          return in;
      }

      fun void initialize() {
          input => output;
          dc => output;

          // configure LiSa
          ( 10000 )::samp => output.duration;
          1 => output.sync;
          1 => output.play;

          // map input from [-1, 1] to (0, 1)
          .49 => input.gain;
          .5 => dc.next;
      }

      initialize();
  }

  class Cruncher extends SuperTable {

      // private

      int bits;
      float levels;
      float bitContribution[1];
      float error;
      float compressMix;

      // public

      fun void setBits( int num ) {
          if( num > 30 ) {
              <<< "I can't let you use", num, "bits, Dave." >>>;
              return;
          }

          num => bits;
          bits => bitContribution.size;
          Math.pow( 2, bits ) => levels;

          calculateErrors();
          recalculate();
      }

      fun void setError( float percent ) {
          percent => error;

          calculateErrors();
          recalculate();
      }

      fun void setCompressMix( float mix ) {
          mix => compressMix;

          calculateErrors();
          recalculate();
      }

      // private

      fun void calculateErrors() {
          for( 0 => int bit; bit < bits; bit++ ) {
              Std.rand2f( ( 1 << bit ) * ( 1. - error ),
                          ( 1 << bit ) * ( 1. + error ) ) =>
bitContribution[bit];
          }
      }

      fun float valueFor( float in ) {
          Math.round( in * levels ) $ int => int quantized;

          float out;
          for( 0 => int bit; bit < bits; bit++ ) {
              (quantized & (1 << bit)) > 0 => int on;
              out + (on ? bitContribution[bit] : 0.) => out;
          }
          out / (1 << bits) => out;

          //return out;
          return ( out * ( 1. - compressMix ) ) +
                 ( ( ulaw( out * 2. - 1. ) / 2. + .5 ) * compressMix );
      }

      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 cruncherInitialize() {
          // set default bit contribution error
          .1 => error;

          // set default compressor mix
          .3 => compressMix;

          // set default quantization level
          setBits( 8 );
      }

      cruncherInitialize();
  }

  // ugens
  Cruncher cruncher;
  TriOsc osc;
  ADSR env;
  LPF lpf;

  // patch
  osc => env => cruncher.input;
  //cruncher.output => lpf => dac;
  cruncher.output => dac;

  // configure
  env.set( 1::ms, 40::ms, .1, 300::ms );
  cruncher.setBits( 8 );
  cruncher.setError( .1 );
  cruncher.setCompressMix( .3 );
  11025 => lpf.freq;

  // GO!
  [ 0, 1, 3, 8, 6, 4 ] @=> int notes[];
  [ 0, 2, 4, 5, 7, 9, 11, 12, 14 ] @=> int scale[];

  while( true ) {
      for( 0 => int i; i < notes.size(); i++ ) {
          scale[ notes[ i ] ] + 75 => Std.mtof => osc.freq;
          env.keyOn( 1 );
          second / 6 => now;
          env.keyOff( 1 );
          second / 7 => now;
      }
  }

>> Also, while turning the LiSa code into a UGen-like class, I realized
>> that the ADSR in my original e-mail was not being quantized, and once
>> I moved it inside, the sound became much less pleasant. The laser
>> whizzing noises (aliasing?) become much more apparent. Interesting
>> again with only 3 bits, though.
>
> Yes, that makes a difference. I do think that real historical gear would
> sometimes put the envelope last (where this is viable, of course, it would
> be in the S612, not so in the gameboy) to suppress noise. This is why all
> non-modular analogue synths have the ADSR after the filter, even if the
> filter wouldn't ever self-oscillate. In anything hybrid I'd predict the
> envelope would be last. In purely digital stuff the envelope would be before
> the converter and it's trigger quantised to the bitrate. That last bit is a
> bit obvious when you think about it, but it matters in how static the final
> result will be perceived to be if we repeat the same drum a few times.
> To conclude; it's not entirely unlikely that we'll have grey beards (where
> appropriate) before we'll be able to perfectly emulate the sounds of our
> youths¹.
> ;¬)
> Kas.
> ¹Some might simply have greyer beards, but they may have to deal with tape
> and tube-amp emulation so it evens out.

Hm, that reminds me, I think I cribbed some tube amp patches off the
forum a while back. I wonder how those'd sound in this...

-- 
Tom Lieber
http://AllTom.com/
http://favmusic.net/


More information about the chuck-users mailing list