rtsetparams(44100, 2) // use your own path to the "libBASIC.so" lib you build (click and drag!) load("/Users/brad/papes/CLASS2012/SOFTspring/week3/BASIC/libBASIC.so") start = 0 for (i = 0; i < 5000; i = i+1) { BASIC(start, 1.0) start = start + irand(0, 0.01) }The fact that each note (each impulse) was spaced between 0 and 0.01 seconds gave the result a quasi-pitched sound. So instead of going through the motions of feeding the output into a filter, instead we created a variant of the instrument designed to produce a regular series of impulses at an audio rate.
/* CLICKFREQ1 - pitched click instrment p0 = output start time p1 = duration p2 = frequency */We added two new variables, one to hold how many samples we need to pass before generating a new click, and one to count how many samples have passed. The first (called "clicksamps") will determine the frequency. Initially we specified this number of samples directly, but that's an inelegant way of settng the frequency. Instead, we simply divide the sampling rate by the frequency we desire to determine how many samples are 'passed': sampling-rate/desired-freq = nsamps.
This is what our modified CLICKFREQ1::init() member function looked like:
// Called by the scheduler to initialize the instrument. int CLICKFREQ1::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; clicksamps = SR/p[2]; clickcounter = 0; return nSamps(); }Because we are using the variables "clicksamps" and "clickcounter" throughout the CLICKFREQ1 instrument, we need to declare them as part of the CLICKFREQ1 class in the CLICKFREQ1.h file:
class CLICKFREQ1 : public Instrument { public: CLICKFREQ1(); virtual ~CLICKFREQ1(); virtual int init(double *, int); virtual int run(); private: int clicksamps; int clickcounter; };In the CLICKFREQ1::run() member function, we use "clickcounter" to count how many samples have passed. When it reaches "0" (counting down from the "clicksamps" we calculated to give us the frequency we wanted), it generates a full-amplitude impulse and resets itself to "clicksamps". This periodic repetition of pulse-generating is what gives us the sound we want:
// Called by the scheduler for every time slice in which this instrument // should run. This is where the real work of the instrument is done. int CLICKFREQ1::run() { float out[2]; // Space for only 2 output chans! // framesToRun() gives the number of sample frames // Each loop iteration outputs one sample frame. for (int i = 0; i < framesToRun(); i++) { if (clickcounter <= 0 ) { out[0] = 32767.0; out[1] = 32767.0; clickcounter = clicksamps; } else { out[0] = 0.0; out[1] = 0.0; } // Write this sample frame to the output buffer. rtaddout(out); // Increment the count of sample frames this instrument has written. increment(); clickcounter = clickcounter - 1; } // Return the number of frames we processed. return framesToRun(); }Note the clickcounter = clickcounter - 1; statement for each sample we generate. This counts down the samples between clicks. This 'counting-down' (and 'counting-up') is such a common operation that C/C++ has a special operator to do it. We could have written clickcounter--;.
After building the libCLICKFREQ1.so loadable lib (see the "cd" and "make" directions given in last week's class for how to do this), we can run it with a simple scorefile:
rtsetparams(44100, 2) load("/Users/brad/papes/CLASS2012/SOFTspring/week4/CLICKFREQ1/libCLICKFREQ1.so") CLICKFREQ1(0, 3.5, 500)with the appropriate location in the load() command for your own libCLICKFREQ1.so library (click and drag! yay!) and we should hear a buzzy, 500 Hz tone.
Click here [CLICKFREQ2.zip] to download the CLICKFREQ2 instrument, a modification of CLICKFREQ1 to allow this control.
All we had to do was introduce one new variable -- "resetclicks" -- and a new member function -- CLICKFREQ2::doupdate() -- and our real-time controllable instrument was ready-to-go.
"resetclicks" is used to control how often we update the 'pfield' data, or the continuous information coming into the instrument through one of the parameters (in this case, p2, or the frequency). Initially we set it to "0":
// Called by the scheduler to initialize the instrument. int CLICKFREQ2::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; clicksamps = SR/p[2]; clickcounter = 0; resetclicks = 0; return nSamps(); }In our CLICKFREQ2::run() member function we check to see if "resetclicks" is less-than or equal to "0". If it is, we call the CLICKFREQ2::doupdate() member function and set "resetclicks" to a value returned by the Instrument class member function getSkip():
for (int i = 0; i < framesToRun(); i++) { if (--resetclicks <= 0) { doupdate(); resetclicks = getSkip(); } ...The rest of the CLICKFREQ2::run() member function is the same as it was in CLICKFREQ1.
What does this getSkip() class function do? It returns the number of samples to skip between calls to the CLICKFREQ2::doupdate() function based upon the RTcmix score directive control_rate(). This gives us the freedom to specify how fast we want to update this information independently of the instrument we build. For some instances, maybe a very slow drift in pitch, we may only need to update the information a few times each second. For other cases, perhaps we are creating short 'chirps' of sound, we might need to update it almost every sample. Using control_rate() (or the older reset()) score command, we can do this without having to recompile our instrument.
The CLICKFREQ2::doupdate() member function is where the work of retrieving incoming pfield data is done. The structure is very simple:
void CLICKFREQ2::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[3]; update(p, 3); clicksamps = SR/p[2]; }We declare an array -- double p[3]; to hold the new values for the pfields of the CLICKFREQ2 instrument. Then we call the Instrument class function update(), telling it to fill this declared array "p" with the first 3 pfields-worth of data update(p, 3);. We then use this to recalculate "clicksamps" to yield a new frequency based on the incoming value of "p[2]" (the third pfield, remember that C/C++ counts from 0).
Why do we need to tell it the number of pfields to update? RTcmix allows you to have, umm, I think 32768 (perhaps more) pfields in a given instrument. If we used an update() function without telling it how many we actually wanted, it would have to assume we wanted all of the pfields updated. This is not terribly efficient.
Build the CLICKFREQ2 instrument, and then run it with this score:
rtsetparams(44100, 2) load("./libCLICKFREQ2.so") CLICKFREQ2(0, 3.5, 500) clicks = maketable("line", "nonorm", 1000, 0, 300, 1, 700, 2, 500) CLICKFREQ2(4, 3.5, clicks)You should hear a 3.5-second tone at 500 Hz, followed by a 3.5-second tone that changes pitch from 300 Hz up to 700 Hz and then back down to 500 Hz. This is because the "clicks" variable in the score is passing real-time pfield data from the control envelope we created with maketable(). Note that we used the "nonorm" option for the maketable. This is so that the RTcmix table-creating function won't try to 'normalize' all the data in the table to fit between -1 and 1. Very low pitches!
We could also use CLICKFREQ2 with mouse control of the pitch using this score:
rtsetparams(44100, 2) load("./libCLICKFREQ2.so") // pfield = makeconnection("mouse", "axis", min, max, default, lag) clicks = makeconnection("mouse", "X", 200, 1000, 500, 20) CLICKFREQ2(0, 999, clicks)In this instance, the "clicks" variable will receive data from our mouse-movement, based upon the parameters we used in the makeconnection("mouse", ...) scorefile command. CLICKFREQ2 will sound for 999 seconds, so we can mouse-away until we decide to stop the execution (or wait for 16.65 minutes).
Just for fun, I also showed how the CLICKFREQ2 instrument could be loaded
into Max/MSP using [rtcmix~]. There are still some bugs in this,
though, so I haven't released it for general use yet.
RTcmix Instrument: SIMPLEOSC
I also used this as an excuse to show how to start with the example
code given in RTcmix/docs/sample_code/ distribution to build an
instrument:
NAME = MYSYNTH
NAME = SIMPLEOSC
Next we decided on the parameters (pfields) we needed for the instrument. Put these in a comment at the top of the instrument:
/* SIMPLEOSC - sample code for a very basic synthesis instrument simple oscillator instrument p0 = output start time p1 = duration p2 = amplitude multiplier p3 = frequency of oscillator p4 = wavetable */We also needed to add this line to our #include files:
#include <Ougens.h>which will allow us to use the "object-oriented" unit generators that will serve as the building-blocks for our RTcmix instrument.
Our SIMPLEOSC::init() member function now looks like this:
int SIMPLEOSC::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; int tablelen = 0; wavetable = (double *) getPFieldTable(4, &tablelen); theOscillator = new Ooscili(SR, p[3], wavetable, tablelen); amp = p[2]; doupdatecheck = 0; return nSamps(); }We have four variables that we will use throughout the SIMPLEOSC instrument, "wavetable", "theOscillator", "amp", and "doupdatecheck". "amp" and "doupdatecheck" are relatively straightforward, they will hold the value for the amplitude of the note used in the int SIMPLEOSC::run() member fucntion ("amp") and the number of samples to pass before doing a doupdate() to allow real-time control of our parameters ("doupdatecheck") We discussed this above in the CLICKFREQ2 instrument.
"wavetable" and "theOscillator" are new, however. Here is how all of these are declared in the SIMPLEOSC.h file:
class SIMPLEOSC : public Instrument { public: SIMPLEOSC(); virtual ~SIMPLEOSC(); virtual int init(double *, int); virtual int configure(); virtual int run(); private: void doupdate(); Ooscili *theOscillator; double *wavetable; float amp; int doupdatecheck; };"wavetable" is declared as a pointer to a space for double-precision numbers. This is like declaring it as an array:
double wavetable[];when we don't know how big the array will be.
This turns out to be exactly what we need, because "wavetable" will be referencing the array holding the template of the waveform we will oscillate in this instrument. As we said, this template is created in the score with the maketable("wave", ...) command, and we can make these templates as large or as small as we want (usually we just use "1000" locations for the waveform. It works).
We also use a locally-declared variable called "tablelen" in the SIMPLEOSC::init() member function to get the length of the maketable() array:
int tablelen = 0;How do we actually get the length and the location of this maketable() waveform template? This is accomplished by this statement:
wavetable = (double *) getPFieldTable(4, &tablelen);getPFieldTable() is an Instrument class member function that takes two parameters. The first is the pfield that will pass in the variable from the score that refers to the maketable() used to construct the waveform. In our SIMPLEOSC instrument, this is "p[4]". So our score might look like this:
... wave = maketable("wave", 1000, "saw") SIMPLEOSC(x, x, x, x, wave, ...)"wave" will then get passed into getPFieldTable() because it is "p[4]".
The second parameter of getPFieldTable() is the variable for the length of the table, "tablelen". This is an integer, and we pass it with an "&" in front. This allows getPFieldTable() to set that variable to the length of the table it finds. We will use this length in the next statement of our code.
We will also use the thing that is returned by getPFieldTable():
wavetable = (double *) getPFieldTable(4, &tablelen);In this case, it is a (double *) value, or the location of a double-precision (floating point) array. We store this location in our "wavetable" variable -- also declared as this double * type -- and we know where the array containing our waveform template is located in computer memory.
The next line of code:
theOscillator = new Ooscili(SR, p[3], wavetable, tablelen);initializes a variable, "theOscillator", which is declared as an Ooscili * type, or a pointer to an "Ooscili" object. An Ooscili object is one of thise 'unit generators' that is part of RTcmix. It creates an object -- an Ooscil object -- that has functionality associated with it to produce an oscillating waveform. We are creating one of these objects using the constructor that takes 4 parameters: the sampling rate (stored in the instrument variable "SR", the frequency (set in p[3] by our choice), the array where the waveform template is stored ("wavetable", now valid because of the getPFieldTable() call), and the length of the table ("tablelen", also set by getPFieldTable()).
The rest of the SIMPLEOSC::init() member function is straightfoward, store the amplitude in the variable "amp" and set the variable "doupdate" to "0" so that the SIMPLEOSC::update() member function will get called first thing.
The really wonderful thing about the Ooscili object is how easy it is to use to generate oscillating samples. This is our basic SIMPLEOSC::run() member function code:
int SIMPLEOSC::run() { float out[2]; // Space for only 2 output chans! // framesToRun() gives the number of sample frames -- 1 sample for each // channel -- that we have to write during this scheduler time slice. // Each loop iteration outputs one sample frame. for (int i = 0; i < framesToRun(); i++) { if (--doupdatecheck <= 0) { doupdate(); doupdatecheck = getSkip(); } out[0] = theOscillator->next() * amp; out[1] = out[0]; // 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(); }All we have to do to synthesize our waveform is theOscillator->next() statement. Multiply it by "amp" so we can get it loud enough to hear (amp range is 0 to 32767), and a score like this will work:
rtsetparams(44100, 2) load("/Users/brad/papes/CLASS2012/SOFTspring/week4/SIMPLEOSC/libSIMPLEOSC.so") wavetable = maketable("wave", 1000, "sine") SIMPLEOSC(0, 3.7, 20000, 250, wavetable)We made a few additions to the code in class, adding the capability to dynamically control the amplitude (we could then design a nice amp envelope to fade up and fade down the note) and stereo panning. Rather than explain what we did, take a look at the code that came with the SIMPLEOSC.zip archive and see if you can figure how it works. The SIMPLEOSC::update() member function is where most of that action is.