void SIMPLEOSC::doupdate() { // The Instrument base class update() function fills the p[] array with // the current values of all pfields. There is a way to limit the values // updated to certain pfields. For more about this, read // src/rtcmix/Instrument.h. double p[6]; update(p, 6); freq = p[3]; amp = p[2]; pan = p[5]; }
The doupdate() function gets called in the SIMPLEOSC::run() member function like before. But we need to take the updated "freq" value and use it to alter the frequency of our oscillator. Scanning the documentation for the Ooscili object, we see that we can do this easily by employing the setfreq(freq) member function of the Ooscili class. All we have to do is add the frequency-setting in the block were we run the doupdate() check:
int SIMPLEOSC::run() { float currentsample; for (int i = 0; i < framesToRun(); i++) { if (--doupdatecheck <= 0) { doupdate(); theOscillator->setfreq(freq); doupdatecheck = getSkip(); } ...
Going back to the original idea that got us started on this marvelous synthetic journey (the frog-guiro), we thought that we might be able to incorporate the ability to generate a periodic pulse into our oscillator to produce an interesting result. Just for fun, we imagined a crude bowed-string moedl with the string having some basic fundamental frequency (the oscillator part), but the string/oscillator getting reset by the horse-hairs of a rosined bow dragging across it (the pulsed part). We know how to make an oscillator, and we know how to implement a periodic 'impulse' that we could use to interrupt that oscillator. The challenge is to figure out how to reset the oscillator at with each impulse.
Again, so easy! Looking again at the Ooscili documentation, there is a really handy setphase(double phase) class member function that will allow us to reset where the oscillator is reading from the wavetable array (see last week's class for a discussion of how we set up and use the wavetable array). Our pfields now look like this:
/* SIMPLEBOW - sample code for a very basic synthesis instrument simple oscillator/bowed instrument p0 = output start time p1 = duration p2 = amplitude multiplier p3 = frequency of oscillator p4 = wavetable p5 = phase reset frequency p6 = pan value (0 - 1) */
resetsamps = SR/p[5]; resetcounter = 0;
void SIMPLEBOW::doupdate() { double p[7]; update(p, 7); freq = p[3]; amp = p[2]; resetsamps = SR/p[5]; pan = p[6]; }
if (resetcounter <= 0) { theOscillator->setphase(0); resetcounter = resetsamps; }
rtoutput("/some/output/file.aif")
Because we are relying on a computation to generate our samples, we don't need a wavetable, no frequency specification, in fact we need to specify very little at all for this instrument to operate. We do want to have control over the "R" value, which determines the various output regions of the equation (perioidic, quasi-periodic, chaotic). This value typically moves from 3.0 to just less than 4.0 (4.0 and above will cause the equation to "explode", less than 3.0 produces a single repeating value -- not good as an oscillator).
Our pfields now look like this:
/* CHAOS1 - sample code for a basic chaos synthesis instrument This uses the "logistic map": R*x*(1.0-x) simple chatic instrument p0 = output start time p1 = duration p2 = amplitude multiplier p3 = R value p4 = pan value (0 - 1) */
int CHAOS1::init(double p[], int n_args) { // Tell scheduler when to start this inst. If rtsetoutput returns -1 to // indicate an error, then return DONT_SCHEDULE. if (rtsetoutput(p[0], p[1], this) == -1) return DONT_SCHEDULE; Rvalue = p[3]; amp = p[2]; doupdatecheck = 0; // iterate the chaotic equation (probably not even necessary in our case) x = 0.5; for (int i = 0; i < 1000; i++) { x = Rvalue * x * (1.0 - x); } pan = p[4]; repeater = 0; return nSamps(); }
The CHAOS1::run() member function looks like this:
int CHAOS1::run() { float currentsample; for (int i = 0; i < framesToRun(); i++) { if (--doupdatecheck <= 0) { doupdate(); doupdatecheck = getSkip(); } float out[2]; // Space for only 2 output chans! if (--repeater <= 0) { // get a new value from the equation x = Rvalue * x * (1.0 - x); repeater = 10; } currentsample = x * amp; out[1] = currentsample * pan; out[0] = currentsample * (1.0 - pan); // Write this sample frame to the output buffer. rtaddout(out); // Increment the count of sample frames this instrument has written. increment(); } // Return the number of frames we processed. return framesToRun(); }
void CHAOS1::doupdate() { double p[5]; update(p, 5); amp = p[2]; Rvalue = p[3]; for (int i = 0; i < 1000; i++) { x = Rvalue * x * (1.0 - x); } pan = p[4]; }
I should also note that we are declaring the variables "Rvalue", "x", and "repeater" in the CHAOS1.h class definition file:
class CHAOS1 : public Instrument { public: CHAOS1(); virtual ~CHAOS1(); virtual int init(double *, int); virtual int configure(); virtual int run(); private: void doupdate(); float amp; int doupdatecheck; float pan; float x; float Rvalue; int repeater; };
One final comment: we are also including the CHAOS1::configure() member
function. In our current instruments we don't use it at all. Essentially
this is called just prior to a note being executed. This is different
from the init() member function which is called when a note is
scheduled. The configure() member function allows you to do things
(like allocate memory arrays, etc.) that you don't really need to do througout
an entire RTcmix script, but that you want to do in time for the
execution of a note. We may show how this is used in a future class.
RTcmix Instrument: CHAOS2
The equation system for the Lorenz attractor is a little more complicated, instead of generating a single value it instead puts out three 'increments' (it's a system of differential equations). Each of these increments determines how far to move on an x-y-z plane to plot the next point. The only value we can easily control is the amount of increment, or "delta", that is then multiplied by the x- y- and z-outputs of the equation system as it iterates. We specify that delta as a pfield:
p0 = output start time p1 = duration p2 = amplitude multiplier p3 = delta value (typically 0.01) p4 = pan value (0 - 1)
int CHAOS2::init(double p[], int n_args) { // Tell scheduler when to start this inst. If rtsetoutput returns -1 to // indicate an error, then return DONT_SCHEDULE. if (rtsetoutput(p[0], p[1], this) == -1) return DONT_SCHEDULE; amp = p[2]; doupdatecheck = 0; x = 1.0; y = 1.0; z = 1.0; delta = p[3]; pan = p[3]; return nSamps(); }
Finally, the CHAOS2::run() member function is a little more complicated than our past instruments because the Lorenz equation itself requires a few steps to calculate:
int CHAOS2::run() { float currentsample; float xnew, ynew, znew, dx, dy, dz; for (int i = 0; i < framesToRun(); i++) { if (--doupdatecheck <= 0) { doupdate(); doupdatecheck = getSkip(); } float out[2]; // Space for only 2 output chans! // here is the attractor equation! dx = 10.0 * (y-x); dy = x * (28.0-z) - y; dz = x*y - (8.0/3.0)*z; xnew = x + delta*dx; ynew = y + delta*dy; znew = z + delta*dz; x = xnew; y = ynew; z = znew; currentsample = (x/30.0) * amp; out[1] = currentsample * pan; out[0] = currentsample * (1.0 - pan); // Write this sample frame to the output buffer. rtaddout(out); // Increment the count of sample frames this instrument has written. increment(); } // Return the number of frames we processed. return framesToRun(); }
float currentsample; float xnew, ynew, znew, dx, dy, dz;
Also note the scaling of the "x" value before being multipled by "amp":
currentsample = (x/30.0) * amp;
The remarkable thing is that this instrument worked! The sound
is interesting, definitely worthy of further exploration. I
had planned to look at a few other chaotic attractor equations,
but we ran out of time. Check back on this page in a few days,
if I get some time I may try coding some more up and putting them
here.