Incorporate wavetables to optimise your synthesiser oscillators. Manage the state of a sine wave oscillator using a wavetable and write data to the audio output.
Level: Intermediate
Platforms: Windows, macOS, Linux
Classes: AudioBuffer, AudioAppComponent, Random, MathConstants
Download the demo project for this tutorial here: PIP | ZIP. Unzip the project and open the first header file in the Projucer.
If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.
The demo project simply generates and outputs a stack of random sine wave harmonics through its stereo output. The user interface allows us to monitor the CPU usage by comparing the traditional implementation of an oscillator and the one that makes use of a wavetable.
In order to properly evaluate and compare the CPU usage of our different implementations, we are going to run our application in the Release configuration instead of the regular Debug configuration used during testing and development. By building the project in Release mode the compiler will be able to optimise the code as much as possible by removing assertions and comments from the code and inlining functions for example.
To change the build configuration in Xcode, first click on the deployment targets in the top left corner of the interface and navigate to Edit Scheme... as shown below:
In the pop-up window, select Release in the Build Configuration combo box as shown in the screenshot:
Your application will now run after heavy compiler optimisations and the CPU usage should decrease significantly.
Wavetable synthesis is a synthesis method that uses look-up tables that are pre-filled with periodic waveforms to generate oscillators without having to generate the same waveform for each sample calculated. The wavetable is initialised with periodic waveforms of your choice and the resolution of these waveforms can be specified. When retrieving the correct sample value to output, the value is found by interpolating between two wavetable samples if the number of samples in the table does not match with the number of samples in the audio buffer block and its corresponding requested frequency.
As an example, let's say that we want to look up a sine wave from the wavetable. We would first create a wavetable for a single cycle of a sine wave with a resolution of 128 sample points for instance. For each sample in the buffer block, we can then request the sine wave sample value by calculating the correct interpolated sample using a combination of the sample rate, the requested frequency to play, the resolution of the wavetable and the current phase or angle of the waveform.
Let's start with a simple sine wave oscillator implementation before diving into wavetables.
In the SineOscillator
class, we keep track of two member variables that store the current angle or phase in the waveform cycle and the angle delta to increment between every cycle depending on the frequency and the sample rate:
The setFrequency()
function allows us to calculate the angle delta by first dividing the frequency by the sample rate and multiplying the result by 2pi, the length of a cycle in radians:
The getNextSample()
function gets called by the getNextAudioBlock()
function of the AudioSource on every sample in the buffer to retrieve the sample value from the oscillator. Here we calculate the sample value using the std::sin()
function by passing the current angle as an argument and updating the current angle by calling the helper function updateAngle()
defined after:
The angle is updated by incrementing the current angle with the angle delta calculated previously when setting the frequency and by wrapping the value when the angle exceeds 2pi:
Now let's switch to the implementation of our MainContentComponent
class.
We keep track of the overall level of our output and an array of oscillators as private member variables as shown here:
In the prepareToPlay()
function, we have to initialise the oscillators and set their frequencies to play based on the sample rate as follows:
SineOscillator
object that generates a single sine wave voice.setFrequency()
function. We also add the oscillator to the array of oscillators.In the getNextAudioBlock()
function we simply sum all the oscillator samples and write the result to the output buffers as shown below:
If we run the application now, we should be able to hear a random noise of stacked sine waves.
Let's change the oscillator implementation to a wavetable synthesis method.
In the MainContentComponent
class, add an AudioSampleBuffer as a member variable that will hold the wavetable values of our single sine wave cycle [1]. We also define the wavetable resolution as a constant of 128 samples using the bit shift operator [2]:
Define a new function called createWavetable()
that will be called in the MainContentComponent
constructor before we start the audio processing.
setSize()
method by specifying that we only need one channel and the number of samples equal to the table size, in our case a resolution of 128. Then retrieve the write pointer for that single channel buffer.std::sin()
function, assign the value to the buffer sample and increment the current angle by the delta value.Add this function call in the MainContentComponent
constructor as follows:
The wavetable should now contain 128 samples of a full sine wave cycle.
In the for() loop of the prepareToPlay()
function, change the below line to instantiate a WavetableOscillator
object instead of a SineOscillator
object:
This constructor takes as an argument the wavetable to use for the sound generation and therefore, create a corresponding new WavetableOscillator
class as shown below:
Instead of keeping track of the current angle and the angle delta of the waveform cycle, define two member variables that store the current wavetable index and the angle delta of the wavetable. Also, define an AudioSampleBuffer variable to hold a reference to the wavetable to use.
The setFrequency()
function of the WavetableOscillator
class is fairly similar to the one implemented previously except that the angle delta is calculated using the size of the wavetable instead of the full cycle in radians of 2pi as follows:
The getNextSample()
function is where the interpolation between the wavetable values occur in order to get the correct sample value.
This implementation should give us the same output sound when we run the application.
WavetableSynthTutorial_02.h
file of the demo project.If you paid close attention to the previous code, you may have noticed that we have one missing value in our wavetable. The final value is skipped as it wraps around to the first value which happens to be the same so let's fix that now.
In the WavetableOscillator
constructor, assign the table size variable to hold the resolution of the wave table minus one and define that member variable appropriately as follows:
The setFrequency()
function needs to be updated using this variable and notice that the angle delta of the table will be slightly smaller:
The getNextSample()
function remains fairly similar except that we don't need to wrap the higher index anymore because we will increase the size of the table in the next step:
Here unlike before we set the resolution as one above the defined value and set the last sample as being the same as the first sample:
This allows us to reduce the wrapping condition in the processing call and transfering the load to the createWavetable()
function that only gets called once at the start of the application.
The result should sound the same as the previous section but notice the slight decrease in CPU usage.
WavetableSynthTutorial_03.h
file of the demo project.Instead of outputing a random sine wave sound, let's create a harmonious sine wave by explicitly setting its harmonics.
Modify the createWavetable()
function to incorporate the harmonics in the wavetable samples of the sine wave as follows:
Finally, reduce the number of oscillators to 10 in the prepareToPlay()
function and listen to the result by running the application.
WavetableSynthTutorial_04.h
file of the demo project.In this tutorial we explored how to create a wavetable from a sine wave but you can essentially store any type of periodic waveform of your choice as long as the first sample matches the last one.
createWavetable()
function to generate and store different types of waveforms such as square, triangle or sawtooth waves. In this tutorial, we have learnt how to implement a wavetable synthesiser. In particular, we have: