Table of Contents
This tutorial introduces simple sine wave synthesis. We show how to manage the state of a sine wave oscillator and write data to the audio output.
Platforms: Windows, Mac OS X, Linux, iOS, Android
- This tutorial leads on from Tutorial: Synthesis with level control, which you should have read and understood first.
Download the demo project for this tutorial here: tutorial_sine_synth.zip. Unzip the project and open it in your IDE.
If you need help with this step, see Tutorial: Getting started with the Projucer.
The demo project is based on the Audio Application template from the Projucer. It presents a single slider to the user to control the frequency of a sine wave.
This tutorial synthesises a sine wave using the standard library function
std::sin(). In order to use this we need to maintain a state for our sine wave generation by storing the current phase angle and the amount by which the phase angle needs to increment for each output sample. This size of this change per sample ("delta") is dependent on the sample rate of the output and the frequency of the sine wave we want to generate.
- Most synthesis applications and plug-ins are likely not to use the
std::sin()function as it probably isn't the most efficient technique. Commonly a wavetable would be used, see Tutorial: Wavetable synthesis. Wavetables also allow for waveshapes other than sine waves, too.
MainContentComponent class we store three
double members :
We have a simple function that updates the
- : First we calculate the number of cycles that will need to complete for each output sample.
- : Then this is multiplied by the length of a whole sine wave cycle, which is 2pi radians.
Before this function can work correctly we need to know the output sample rate. This is because we need to know how frequently the samples are being generated, this is in order to know the amount of change that is needed per sample. We are passed the sample rate by the AudioAppComponent::prepareToPlay() callback function:
Here we store a copy of the sample rate value and call our
updateAngleDelta() function initially. (The name for the
samplesPerBlockExpected argument is commented out as we don't need this information.)
When the slider is moved while the app is running, we need to update the
angleDelta member again:
Here we check that the sample rate is valid, before calling the
updateAngleDelta() function again.
getNextAudioBlock() callback we need to generate the actual sine wave and write it to the output:
For each output sample we calculate the sine function for the current angle, then increment the angle for the next sample. Notice that we bring the level down to
0.125 as a full scale sine wave will be very loud! We could (and perhaps should) wrap the current angle value back to zero when it reaches 2pi. Since larger values still return a valid value we can actually avoid this calculation. We get something like that shown in the following image:
You may have noticed that the slider value changes non-linearly (if not you should try this out now). These changes are, in fact, logarithmic. This gives us higher resolution for smaller values and lower resolution for larger values. When controlling a frequency value this is often appropriate (as musically, we hear equal changes in ratios between frequences rather than equal linear changes). This is configured by using the Slider::setSkewFactorFromMidPoint() function . Our slider range is set to 50..5000 therefore setting the centre of the slider track to represent 500 would mean there is an equal musical interval between the slider minimum and the centre, and the centre and the slider maximum:
The skew factor for the slider can be set directly using the Slider::setSkewFactor() function although it is often easier to think about what value you want at the mid-point.
- Add another slider to the application to control the level of the sine wave. Take care to keep the level well below 1.0 — a maximum value of 0.25 should be fine.
You may notice — especially in the higher frequencies — that there are some audible, and probably unwanted, artefacts produced as the slider is moved. This is because the slider is actually changing in discrete steps, and when the slider is moved quickly then these steps are quite large. In addition to this the slider frequency is only updated for each audio block, therefore the precise effect of these changes will be dependent on the hardware block size.
Let's add two members to our class, one to store the current frequency being used for synthesis, and another target frequency that the user has requested by moving the slider. Then we can more slowly ramp between these values to remove the artefacts:
We need to update our constructor to initialise these values. We can also initialise the slider to the same value:
- : Initialise the current frequency.
- : Make sure the that the target frequncy is the same.
- : Set the initial position of the slider.
The key to the way this algorithm works is to check whether the current and target values are the same or different. If they are the same, then we can simply use our original code as the
angleDelta member doesn't need to change. If the current and target values are different, then we need to update the
angleDelta member for each sample as we gradually move the current value closer to the target.
- In this example we just use the number of samples in the output buffer as the duration of our ramp. This means that with very small buffer sizes you may still hear artefacts.
- : Check if our target is different from the current value. Notice that we take a local copy of the target value, just in case the slider changes the value on the message thread while this function runs.
- : Calculate the required increment per sample.
- : Increment the current frequency.
- : Update the
deltaAnglemember based on this new frequency.
- : Otherwise just use the original code.
- The format of this code uses a typically pattern for DSP code. We avoid conditional statements in the inner
for()loop, if possible. Instead, having the condition tested outside the loop, and we use two different, but quite similar loops depending upon whether the parameter is changing.
Finally, we need to update our
sliderValueChanged() function so that it just updates the target value:
And that's it! Try this now and the artefacts from the slider movement should have been removed.
- Add smoothing to the level slider control that you added in an earlier exercise.
- Not wrapping the phase angle around 2pi may not be ideal in all circumstances. If using
doublevariables then there would be some innaccuracy to the calculations when the current angle value became very large. By not wrapping the phase at 2pi using the
std::sin()function performs reasonably well compared to a simple wavetable technnique. See Tutorial: Wavetable synthesis for an exploration of this.
In this tutorial we have introduced some basic methods of synthesising and controlling a sine wave. We have looked at:
- The essential variables needed for maintaining the state of a sine wave oscillator.
- How to set these variable to produce the desired result.
- How to smooth out parameter changes to avoid audio artefacts.
Generated on Fri Jan 12 2018 09:51:15 for JUCE by 1.8.13