A Generalized Oscillator Framework
[color-box]
### Note
This tutorial is a little more advanced for those of you wanting to get into device/algorithm development rather than just patch creation. It covers topics that will require you to modify or add code to the nw2s library folder rather than just the sketches folder.
[/color-box]
One of the goals of this project is not only to provide a flexible hardware platform, but also to provide a framework around which you can develop patches more quickly with most of the boilerplate taken care of.
A good opportunity for such a framework is a generalized voltage controlled digital oscillator. Generally, they all work the same way - an input voltage drives the frequency of the oscillator. The frequency of the oscillator and the sample rate of the DAC determine the period length in samples. The specific oscillator implementations output a discrete value for each sample period.
With a good framework, all the developer needs to worry about is being able to generate samples quickly enough - be it a mathematical function that generates a triangle or sine or interpolating values between two wavetables.
This tutorial will cover two simple oscillators. The first is a saw - a signal whose value is simply the same as the phase index scaled by the output values. This is very basic and it's a reasonably quick step from here to a wavetable-based oscillator.
The second is an oscillator that is effectively a noise generator, but an interesting take on abusing some principles of the Nyquist Theorem. Rather than generating a random value per sample as a digital noise generator would work, only one random value per oscillation is generated. The output of this oscillator is the same as if a random discrete signal were generated with a sampling frequency equal to the oscillator's frequency. Think of it as more of an AC-coupled sample and hold operating in the audio range.
### The Oscillator
One of the principles of object-oriented design is encapsulation. By encapsulating logic of interest into a single object, you are effectively hiding the implementation details from other areas of the program that are not really interested in all of those wobbly bits. The first bit of code will encapsulate the part of our instrument that manages the following aspects:
1. Allocation and configuration of an AC-coupled DAC output
2. Configuring a timer that will act as our sample clock - for us, this is operating at 10kHz.
Let's call this object an Oscillator. Trust me when I say that you want to encapsulate this logic. This is some ugly ugly stuff... take a look:
Oscillator::Oscillator(PinAudioOut pinout)
{
this->pinout = pinout;
/* The event handler needs static references to these devices */
if (pinout == DUE_DAC0) AudioDevice::device0 = this;
if (pinout == DUE_DAC1) AudioDevice::device1 = this;
/* Make sure the dac is zeroed and on */
analogWriteResolution(12);
analogWrite(pinout, 0);
this->channel = (pinout == DUE_DAC0) ? 1 : 2;
this->dac = (pinout == DUE_DAC0) ? 0 : 1;
int tc_id = (pinout == DUE_DAC0) ? ID_TC4 : ID_TC5;
IRQn_Type tc_irq = (pinout == DUE_DAC0) ? TC4_IRQn : TC5_IRQn;
pmc_set_writeprotect(false);
pmc_enable_periph_clk(tc_id);
TC_Configure(TC1, this->channel, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_TCCLKS_TIMER_CLOCK2);
TC_SetRC(TC1, this->channel, 1050); // sets 10Khz interrupt rate
TC_Start(TC1, this->channel);
/* enable timer interrupts */
TC1->TC_CHANNEL[this->channel].TC_IER = TC_IER_CPCS;
TC1->TC_CHANNEL[this->channel].TC_IDR = ~TC_IER_CPCS;
NVIC_EnableIRQ(tc_irq);
}
void Oscillator::timer_handler()
{
/* Get the sample and dither it */
int sample = this->getSample() ^ random(0, 1);
dacc_set_channel_selection(DACC_INTERFACE, this->dac);
dacc_write_conversion_data(DACC_INTERFACE, sample);
this->nextSample();
}
### The VCO
Now that's done and we have an output allocated as well as something happening 10,000 times a second which happens to be a pretty grungy frequency we can use to make some lo-fi sounds, we need a way to control these sounds. The most common audio-frequency oscillator in the modular world is a voltage-controlled oscillator. It's typically one whose frequency of oscillation is controlled by a voltage from 0V to 5V or -5V to 5V or whatever.
We'll soon find that we're not just limited to changing the frequency of oscillation, but let's start with that.
1. The first thing to do is to allocate an input signal. This will be our control voltage.
2. The second is that we need to convert that voltage to a frequency with some semblance of correlation to the volt per octave standard.
3. The final thing we need to do is set up a discrete counter that will tell our implementations where they are in the wave cycle... we'll call that the phase - since that's what it is.
Here's the code:
VCO::VCO(PinAudioOut pinout, PinAnalogIn pinin) : Oscillator(pinout)
{
this->pinin = pinin;
/* Read the analog in and get a frequency */
int value = analogRead(pinin);
value = (value < 0) ? 0 : (value > 4000) ? 4000 : value;
this->frequency = CVFREQUENCY[value];
this->phaseindex = 0;
this->sample = 0;
this->samplespercycle = 1000000UL / this->frequency; // 10kHz sample rate
this->nextsamplespercycle = this->samplespercycle;
}
void VCO::timer(unsigned long t)
{
if (t % 250 == 0)
{
/* Read the analog in and get a frequency */
int value = analogRead(pinin);
value = (value < 0) ? 0 : (value > 4000) ? 4000 : value;
this->frequency = CVFREQUENCY[value];
/* Don't update the number of samples per cycle during a cycle. wait till the next one */
this->nextsamplespercycle = 1000000UL / this->frequency; // 10kHz sample rate
}
}
void VCO::nextSample()
{
/* Next sample calulates the next value */
this->phaseindex = (phaseindex + 1) % this->samplespercycle;
if (this->phaseindex == 0)
{
this->samplespercycle = this->nextsamplespercycle;
}
this->sample = this->nextVCOSample();
}
int VCO::getSample()
{
/* Sample just returns the sample value */
return this->sample;
}
You'll see that rather than calculate the sample value every time we need it, we are actually outputting the sample as quickly as possible after the clock signal ticks. Once that value has been sent to the DAC, then we can take our time (all 0.0001 seconds of it) to calculate the next sample and hold on to it until the next clock ticks.
You may also notice that there's no math to calculate the frequency from a control voltage. This is some ugly math that is not really convenient nor quick on a microcontroller. Instead, I did the math in excel and made a big array where the index is the 12-bit input value and the value is a frequency. I multiplied it by 100 as well to make it a little easier to do the integer math without rounding errors.
Also, you'll see that we've stubbed a function here - nextVCOSample() - that hasn't yet been defined. That function is what our oscillator implementation will take care of, and since all of the dirty business of IO, clocks, frequencies, and phase are taken care of, our oscillator won't have to take care of much at all besides just figuring out what that next sample should be.
### The Saw
The saw waveform is very basic. It's just a rising voltage up to a point and resets back to zero. It just so happens that the VCO is providing us an increasing number from 0 to N we can use to calculate this signal:
int Saw::nextVCOSample()
{
/* Saw is a simple osc. Current value is the same as the phase index normalized to output scale. */
return (400000 / (this->samplespercycle * 100)) * this->phaseindex;
}
That's it. That's the entire implementation class of a saw VCO. Whew. that was tough what's next? Well, the next step for this oscillator is to reincarnate itself as a wavetable oscillator - this signal doesn't actually sound that good and we can get some better waves if we use a library of single cycle waveforms, but that will come a little later.
Here's a sample of it coming out of the nw2s::b. I have a recording of it coming straight off the Arduino, but that really doesn't sound good. The nw2s::b includes a high-pass filter for blocking DC and a low-pass filter around 10kHz to smooth out some of the rough edges. The recording starts dry and then I add an SeM-20 filter.
[soundcloud params="auto_play=false"]https://soundcloud.com/scottwilson/nw2s-b-saw-oscillator-demo[/soundcloud]
### The Discrete Noise Generator
A framework would not be useful if you couldn't demonstrate how easy it is to get shit done. This is one of my favorite digital sounds. It's basically the sound of something blowing up on an Atari 2600 - until you modulate it or filter it or whatever. It's a rude abuse of the Nyquist Sampling Theory.
int DiscreteNoise::nextVCOSample()
{
if (this->phaseindex == 0)
{
/* Get a new random value */
this->currentvalue = Entropy::getValue(0, 4000);
}
return this->currentvalue;
}
We all know what noise sounds like. Nice pure analog noise. You can even make super clean digital noise. But what happens when you reduce the sampling rate of a noise signal to lower in the audio frequency - like to where we hear normal sounds? This...
[soundcloud params="auto_play=false"]https://soundcloud.com/scottwilson/nw2s-b-discrete-noise-demo[/soundcloud]
The recording starts dry and then I add an SeM-20 filter.
[The entire code listing can be found on github](https://github.com/nw2s/b/blob/master/sketches/libraries/nw2s/Oscillator.cpp)