Is there a nice UGen I could be using instead of bitmaster in this snippet? (um, you kinda need scale.ck to run it http://www.dtic.upf.edu/~gcoleman/chuck/tutorial/tutorial.html) //BlitSaw osc => blackhole; //SawOsc osc => blackhole; TriOsc osc => blackhole; Step bitter => ADSR env => dac; env.set(1::ms, 40::ms, .1, 300::ms); spork ~ bitmaster(); 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; } } fun void bitmaster() { 4 => float bits; while(true) { (osc.last() * bits) $ int => int rounded; rounded / bits => bitter.next; samp => now; } } -- Tom Lieber http://AllTom.com/ http://favmusic.net/
You could build a generator using Gen7 or CurveTable. That might be a
little more efficient, but maybe not worthwhile.
andy
On Thu, Nov 18, 2010 at 10:13 AM, Tom Lieber
Is there a nice UGen I could be using instead of bitmaster in this snippet? (um, you kinda need scale.ck to run it http://www.dtic.upf.edu/~gcoleman/chuck/tutorial/tutorial.html)
//BlitSaw osc => blackhole; //SawOsc osc => blackhole; TriOsc osc => blackhole; Step bitter => ADSR env => dac; env.set(1::ms, 40::ms, .1, 300::ms);
spork ~ bitmaster();
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; } }
fun void bitmaster() { 4 => float bits; while(true) { (osc.last() * bits) $ int => int rounded; rounded / bits => bitter.next; samp => now; } }
-- Tom Lieber http://AllTom.com/ http://favmusic.net/ _______________________________________________ chuck-users mailing list chuck-users@lists.cs.princeton.edu https://lists.cs.princeton.edu/mailman/listinfo/chuck-users
On 18 November 2010 19:44, Andrew Turley
You could build a generator using Gen7 or CurveTable. That might be a little more efficient, but maybe not worthwhile.
How about a Lisa set to sync? If you feed that with a arbitrary signal at gain .5 mixed with a step set to a value of .5, (which would turn a signal from range -1 to 1 into one from 0 to 1) you can set any mapping you'd like. Filling that LiSa with a line from -1 to 1 would get you your original signal back, a "staircase" would give bit-depth reduction, etc. Multiple mappings could be made and by changing the loop start and end these could be selected. Shreds might be more versatile as those also give you easy access to modulating bit rate in addition to depth and to get a resolution equal to double floats you'd have to use a very large buffer indeed... but then shreds are often expensive and the code gets messy looking, IMHO. Yours, Kas.
Hi Kassen, this sounds interesting, but I can't follow your steps. Have you got an example of this idea? Thanks a lot! El 18.11.2010, a las 20:00, Kassen escribió:
On 18 November 2010 19:44, Andrew Turley
wrote: You could build a generator using Gen7 or CurveTable. That might be a little more efficient, but maybe not worthwhile. How about a Lisa set to sync? If you feed that with a arbitrary signal at gain .5 mixed with a step set to a value of .5, (which would turn a signal from range -1 to 1 into one from 0 to 1) you can set any mapping you'd like. Filling that LiSa with a line from -1 to 1 would get you your original signal back, a "staircase" would give bit-depth reduction, etc.
Multiple mappings could be made and by changing the loop start and end these could be selected.
Shreds might be more versatile as those also give you easy access to modulating bit rate in addition to depth and to get a resolution equal to double floats you'd have to use a very large buffer indeed... but then shreds are often expensive and the code gets messy looking, IMHO.
Yours, Kas. _______________________________________________ chuck-users mailing list chuck-users@lists.cs.princeton.edu https://lists.cs.princeton.edu/mailman/listinfo/chuck-users
Hey Lars!
this sounds interesting, but I can't follow your steps. Have you got an
example of this idea?
Here we go; -----------8<----------------------------- LiSa l => dac; //more time here means more resolution //here more time actually prevents LiSa's interpolation from cleaning our intended dirt second => l.duration; //only calculating constants once (l.duration()/ samp) $ int => int NUM_SAMPS; 2.0 / NUM_SAMPS => float STEP_SIZE; //straight ramp from -1 to 1 for (int x; x< NUM_SAMPS; x++) { l.valueAt ( -1 + (x * STEP_SIZE) , x::samp); } //get signal's range between 0 and 1. //slightly less, actually, to avoid edge-case glitches SinOsc signal => Gain mix => l; .49 => signal.gain; Step offset => mix; .5 => offset.next; //use the LiSa to map the signal to new values 1 => l.sync; //off we go. you should hear a pure sine here 1 => l.play; 5::second => now; //now we start crushing. 6 => int bits; //the "-1" is for above and minus zero //again we use a constant to minimise per-value processing Math.pow ( bits - 1, 2) => float QUANT; //cheap&dirty quantisation //we take the original value, multiply it by the number of steps in our chosen bit-depth //then round this by casting to int //finally dividing it again to get back in the original range //and putting the value back for (int x; x< NUM_SAMPS; x++) { l.valueAt ((l.valueAt(x::samp) * QUANT) $ int / QUANT , x::samp); } //and there we go again 5::second => now; ---------------------------------------8<-------------------------------- I hacked this up quite quickly, there are bound to be some issues in the details, especially in the rounding. At the very least this should illustrate the technique of using LiSa as a waveshaper for arbitrary shapes. Note that "signal" is a placeholder for any signal you'd like to process, but it must not (initially) exceed the range of -1 to 1 (normal UGens conform to this, as should the ADC) or the result will wrap (that too might sound cool...). Of course using a oscillator here makes no sense; if a plain oscillator would do you are better off using a wavetable set to some suitably lo-fi single cycle wave instead of actual bitcrushing. Another likely issue here is that LiSa is trying to prevent the exact kind of artefact we are trying to create by using interpolation. I'm fairly sure the effect of that can be minimised by extending the size of the buffer and hence it's resolution. This might be a interesting element to play with. Hope this is at least a decent starting point for further experimentation. No warranties, no refunds, please mind your neighbours, pets and speakers. Have a noisy weekend! Kas.
Kas you are gorgeous!! Thanks so much for this example and the clear explication. Lars El 19.11.2010, a las 06:27, Kassen escribió:
Hey Lars!
this sounds interesting, but I can't follow your steps. Have you got an example of this idea?
Here we go;
-----------8<-----------------------------
LiSa l => dac;
//more time here means more resolution //here more time actually prevents LiSa's interpolation from cleaning our intended dirt second => l.duration;
//only calculating constants once (l.duration()/ samp) $ int => int NUM_SAMPS; 2.0 / NUM_SAMPS => float STEP_SIZE;
//straight ramp from -1 to 1 for (int x; x< NUM_SAMPS; x++) { l.valueAt ( -1 + (x * STEP_SIZE) , x::samp); }
//get signal's range between 0 and 1. //slightly less, actually, to avoid edge-case glitches SinOsc signal => Gain mix => l; .49 => signal.gain; Step offset => mix; .5 => offset.next;
//use the LiSa to map the signal to new values 1 => l.sync;
//off we go. you should hear a pure sine here 1 => l.play;
5::second => now;
//now we start crushing. 6 => int bits;
//the "-1" is for above and minus zero //again we use a constant to minimise per-value processing Math.pow ( bits - 1, 2) => float QUANT;
//cheap&dirty quantisation //we take the original value, multiply it by the number of steps in our chosen bit-depth //then round this by casting to int //finally dividing it again to get back in the original range //and putting the value back for (int x; x< NUM_SAMPS; x++) { l.valueAt ((l.valueAt(x::samp) * QUANT) $ int / QUANT , x::samp); }
//and there we go again 5::second => now;
---------------------------------------8<--------------------------------
I hacked this up quite quickly, there are bound to be some issues in the details, especially in the rounding. At the very least this should illustrate the technique of using LiSa as a waveshaper for arbitrary shapes. Note that "signal" is a placeholder for any signal you'd like to process, but it must not (initially) exceed the range of -1 to 1 (normal UGens conform to this, as should the ADC) or the result will wrap (that too might sound cool...). Of course using a oscillator here makes no sense; if a plain oscillator would do you are better off using a wavetable set to some suitably lo-fi single cycle wave instead of actual bitcrushing.
Another likely issue here is that LiSa is trying to prevent the exact kind of artefact we are trying to create by using interpolation. I'm fairly sure the effect of that can be minimised by extending the size of the buffer and hence it's resolution. This might be a interesting element to play with.
Hope this is at least a decent starting point for further experimentation. No warranties, no refunds, please mind your neighbours, pets and speakers.
Have a noisy weekend! Kas. _______________________________________________ chuck-users mailing list chuck-users@lists.cs.princeton.edu https://lists.cs.princeton.edu/mailman/listinfo/chuck-users
Very cool,
This is why I wish ChucK either had an Array UGen or if normal ChucK
arrays could be treated like UGens somehow. Coming from other
languages (Pd, Csound for example) one becomes very used to arrays (or
ftables) being part of the signal chain. Using LiSa as a workaround
is pretty rad. The ChucK wish-list keeps growing.
Later,
Kurt
2010/11/19 Kassen
Hey Lars!
this sounds interesting, but I can't follow your steps. Have you got an example of this idea?
Here we go; -----------8<----------------------------- LiSa l => dac; //more time here means more resolution //here more time actually prevents LiSa's interpolation from cleaning our intended dirt second => l.duration; //only calculating constants once (l.duration()/ samp) $ int => int NUM_SAMPS; 2.0 / NUM_SAMPS => float STEP_SIZE; //straight ramp from -1 to 1 for (int x; x< NUM_SAMPS; x++) { l.valueAt ( -1 + (x * STEP_SIZE) , x::samp); }
//get signal's range between 0 and 1. //slightly less, actually, to avoid edge-case glitches SinOsc signal => Gain mix => l; .49 => signal.gain; Step offset => mix; .5 => offset.next; //use the LiSa to map the signal to new values 1 => l.sync; //off we go. you should hear a pure sine here 1 => l.play; 5::second => now; //now we start crushing. 6 => int bits; //the "-1" is for above and minus zero //again we use a constant to minimise per-value processing Math.pow ( bits - 1, 2) => float QUANT; //cheap&dirty quantisation //we take the original value, multiply it by the number of steps in our chosen bit-depth //then round this by casting to int //finally dividing it again to get back in the original range //and putting the value back for (int x; x< NUM_SAMPS; x++) { l.valueAt ((l.valueAt(x::samp) * QUANT) $ int / QUANT , x::samp); }
//and there we go again 5::second => now; ---------------------------------------8<-------------------------------- I hacked this up quite quickly, there are bound to be some issues in the details, especially in the rounding. At the very least this should illustrate the technique of using LiSa as a waveshaper for arbitrary shapes. Note that "signal" is a placeholder for any signal you'd like to process, but it must not (initially) exceed the range of -1 to 1 (normal UGens conform to this, as should the ADC) or the result will wrap (that too might sound cool...). Of course using a oscillator here makes no sense; if a plain oscillator would do you are better off using a wavetable set to some suitably lo-fi single cycle wave instead of actual bitcrushing. Another likely issue here is that LiSa is trying to prevent the exact kind of artefact we are trying to create by using interpolation. I'm fairly sure the effect of that can be minimised by extending the size of the buffer and hence it's resolution. This might be a interesting element to play with. Hope this is at least a decent starting point for further experimentation. No warranties, no refunds, please mind your neighbours, pets and speakers. Have a noisy weekend! Kas. _______________________________________________ chuck-users mailing list chuck-users@lists.cs.princeton.edu https://lists.cs.princeton.edu/mailman/listinfo/chuck-users
-- -------------------------------------------------- www.kurtkotheimer.com --------------------------------------------------
Kurt; Very cool,
This is why I wish ChucK either had an Array UGen or if normal ChucK arrays could be treated like UGens somehow. Coming from other languages (Pd, Csound for example) one becomes very used to arrays (or ftables) being part of the signal chain. Using LiSa as a workaround is pretty rad.
I think we could say that we *do* have arrays as UGens, if we would want to argue that. Both SndBuf and LiSa are really just arrays as UGens... What we lack is arrays as UGens for control data and for audio treatment. This may have to do with other languages having a "K-rate" and often even UGens at that rate. Arguably we have the best implementation of control-rate around, it's so good that we could port any and all of those things to ChucK code. It might be because of that that we lack such UGens, this is a good trade from one point of view but clearly it's often not convenient. The GenX UGens go in this direction but they aren't that great for waveshaping and not for treatments like this one.
The ChucK wish-list keeps growing.
It does. "Borrowing" ftable doesn't seem like a half bad idea and bit-crushing is so popular in modern music as well as useful in explaining the limits of digital sound that I could see a use for a specialised UGen for that as well. Yours, Kas.
On Thu, Nov 18, 2010 at 10:44 AM, Andrew Turley
You could build a generator using Gen7 or CurveTable. That might be a little more efficient, but maybe not worthwhile.
2010/11/18 Kassen
How about a Lisa set to sync? If you feed that with a arbitrary signal at gain .5 mixed with a step set to a value of .5, (which would turn a signal from range -1 to 1 into one from 0 to 1) you can set any mapping you'd like. Filling that LiSa with a line from -1 to 1 would get you your original signal back, a "staircase" would give bit-depth reduction, etc. Multiple mappings could be made and by changing the loop start and end these could be selected.
I'm grateful that Kassen dropped some code for LiSa, because I didn't understand his description until I was knee deep in CurveTables and had finally worked through all the problems Kassen's succinct proposal addresses. While I got the CurveTable working, I can't get real sharp transitions between the quantization levels because CurveTable crashes if I give it a coefs array with more than 35 points. So I think I'll be using LiSa because it doesn't crash with longer buffers to get around the linear interpolation. Thanks everybody, especially Kassen and Andrew. (here's the CurveTable code: SinOsc s; CurveTable c; Step dc; s => c => dac; dc => c; .49 => s.gain; .5 => dc.next; 35 => int res; // # of CurveTable points 4 => int levels; // # of quantization levels (/ 2) // build CurveTable coefs levels => float flevels; float coefs[res * 3 + 2]; for( 0 => int r; r < res; r++ ) { ((r $ float) / res) * 2 - 1 => float f; // [0, res) -> [-1, 1) // the triplet r => coefs[r * 3]; // time ((f * levels) $ int) / flevels => coefs[r * 3 + 1]; // value 0 => coefs[r * 3 + 2]; // curvature (linear) } res => coefs[res * 3]; // time 1 => coefs[res * 3 + 1]; // value coefs => c.coefs; day => now; ) -- Tom Lieber http://AllTom.com/ http://favmusic.net/
Tom; I'm grateful that Kassen dropped some code for LiSa, because I didn't
understand his description until I was knee deep in CurveTables and had finally worked through all the problems Kassen's succinct proposal addresses.
You're welcome, as always. It's also nice to see how a formal language can sometimes be *easier* to understand than a natural one. I think that's a non-trivial point if we want to consider the relative importance of livecoding. :¬)
While I got the CurveTable working, I can't get real sharp transitions between the quantization levels because CurveTable crashes if I give it a coefs array with more than 35 points.
Weird, and what a odd number for it to crash at; I'd expect a power of 2. Just increasing the resolution should help but as you note; LiSa is happy to deal with hundreds of thousands of points. Interestingly, for this niche-application; LiSa, unlike SndBuf, has no option to turn off interpolation. If she did we could stick to far smaller tables and a lower overhead setting them. Not sure that's worth "fix" because this is a bit of a niche application and a "proper" crusher might be a much nicer investment of dev-time. As a side-note; a big part of the sound of older d-a converters came from deviations in the actual value of the resistors in them compared to their nominal value (to put it bluntly; cheap crap was used). Basically that means the "steps" aren't all the same height. If you really want to emulate that things will get a bit more tricky. It's interesting though, because if we discard quantisation in the time domain the errors of bit-depth crushing should all be harmonic so such errors should give slightly different spectra. I don't think many implementations do this. Yours, Kas.
2010/11/27 Kassen
It's also nice to see how a formal language can sometimes be *easier* to understand than a natural one. I think that's a non-trivial point if we want to consider the relative importance of livecoding. :¬)
When I was in Japan, after I gave a talk on ruck some Japanese folks in the audience had questions that I had trouble answering in English, Japanese, or pictures on paper. But once I demonstrated the answer with Ruby, everyone understood. :)
While I got the CurveTable working, I can't get real sharp transitions between the quantization levels because CurveTable crashes if I give it a coefs array with more than 35 points.
Weird, and what a odd number for it to crash at; I'd expect a power of 2.
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.
As a side-note; a big part of the sound of older d-a converters came from deviations in the actual value of the resistors in them compared to their nominal value (to put it bluntly; cheap crap was used). Basically that means the "steps" aren't all the same height. If you really want to emulate that things will get a bit more tricky. It's interesting though, because if we discard quantisation in the time domain the errors of bit-depth crushing should all be harmonic so such errors should give slightly different spectra. I don't think many implementations do this.
Ohh, I'm definitely going to play with that. Was it logarithmic, or something less regular? 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. -- Tom Lieber http://AllTom.com/ http://favmusic.net/
On Sat, Nov 27, 2010 at 8:41 PM, Tom Lieber
2010/11/27 Kassen
: As a side-note; a big part of the sound of older d-a converters came from deviations in the actual value of the resistors in them compared to their nominal value (to put it bluntly; cheap crap was used). Basically that means the "steps" aren't all the same height. If you really want to emulate that things will get a bit more tricky. It's interesting though, because if we discard quantisation in the time domain the errors of bit-depth crushing should all be harmonic so such errors should give slightly different spectra. I don't think many implementations do this.
Ohh, I'm definitely going to play with that. Was it logarithmic, or something less regular?
Ooh, I like this one (with the ulaw( value ) => value line): 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(); } // 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; } } -- Tom Lieber http://AllTom.com/ http://favmusic.net/
Hey, Tom. When I was in Japan, after I gave a talk on ruck some Japanese folks
in the audience had questions that I had trouble answering in English, Japanese, or pictures on paper. But once I demonstrated the answer with Ruby, everyone understood. :)
Victory!
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?
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.
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.
2010/11/27 Kassen
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/
On 18 Nov 2010, at 19:13, Tom Lieber wrote:
Is there a nice UGen I could be using instead of bitmaster in this snippet? (um, you kinda need scale.ck to run it http://www.dtic.upf.edu/~gcoleman/chuck/tutorial/tutorial.html)
A search for "scale.ck site:.dtic.upf.edu" gave: http://www.dtic.upf.edu/~gcoleman/chuck/svn/scale.ck
participants (6)
-
Andrew Turley
-
Hans Aberg
-
Kassen
-
kurt
-
lars ullrich
-
Tom Lieber