This is a part of the DIY Synthesizer series of posts where each post is roughly built upon the knowledge of the previous posts. If you are lost, check the earlier posts!
This is the second chapter in a series of tutorials about programming your own synthesizer
In this chapter we’ll talk about oscillators, and some common basic wave forms: Sine, Square, Saw, Triangle and Noise.
By the end, you should have enough knowledge to make some basic electronic melodies.
You can download the full source for this chapter here: DIY Synthesizer: Chapter 2 Source Code
The Sine Wave
The sine wave is the basis of lots of things in audio synthesis. It can be used on it’s own to make sound, multiple sine waves can be combined to make other more complex wave forms (as we’ll see in the next chapter) and it’s also the basis of a lot of DSP theory and audio analysis. For instance, there is something called Fourier Analysis where you can analyze some audio data and it will tell you what audio frequencies are in that sound data, and how strong each is (useful for advanced synthesis and digital signal processing aka DSP). The math of how to get that information is based on some simple properties of sine waves. More info can be found here: http://en.wikipedia.org/wiki/Fourier_analysis.
If we want to use a sine wave in our audio data, the first problem we hit is that sine has a value from -1 to 1, but our audio data from the last chapter is stored in a 32 bit int, which has a range of -2,147,483,648 to 2,147,483,647, and is unable to store fractional numbers.
The solution is to just map -1 to -2,147,483,648, and 1 to 2,147,483,647 and all the numbers in between represent fractional numbers between -1 and 1. 0.25 for instance would become 536,870,911.
If instead of 32 bits, we wanted to store the data in 16 bits, or 8 bits, we could do that as well. After generating our floating point audio data, we just convert it differently to get to those 16 bits and 8 bits. 16 bits have a range of -32,768 to 32,767 so 0.25 would convert to 8191. In 8 bits, wave files want UNSIGNED 8 bit numbers, so the range is 0 to 255. In that case, 0.25 would become 158.
Note, in the code for this chapter, i modified WriteWaveFile to do this conversion for us so going forward we can work with floating point numbers only and not worry about bits per sample until we want to write the wave file. When you call the function, you have to give it a template parameter specifying what TYPE you want to use for your samples. The three supported types are uint8, int16 and int32. For simple wave forms like those we are working with today, there is no audible difference between the 3, so all the samples just make 16 bit wave files.
So, we bust out some math and figure out here’s how to generate a sine wave, respecting the sample rate and frequency we want to use:
//make a naive sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fFrequency / (float)nSampleRate);
}
WriteWaveFile("sinenaive.wav",pData,nNumSamples,nNumChannels,nSampleRate);
That does work, and if you listen to the wave file, it does sound correct:
Naive Sine Wave Generation
There is a subtle problem when generating the sine wave that way though which we will talk about next.
Popping aka Discontinuity
The problem with how we generated the wave file only becomes apparent when we try to play two tones right next to each other, like in the following code segment:
//make a discontinuitous (popping) sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
if(nIndex < nNumSamples / 2)
{
float fCurrentFrequency = CalcFrequency(3,3);
pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fCurrentFrequency / (float)nSampleRate);
}
else
{
float fCurrentFrequency = CalcFrequency(3,4);
pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fCurrentFrequency / (float)nSampleRate);
}
}
WriteWaveFile("sinediscon.wav",pData,nNumSamples,nNumChannels,nSampleRate);
Quick note about a new function shown here, called CalcFrequency. I made that function so that you pass the note you want, and the octave you want, and it will return the frequency for that note. For instance, to get middle C aka C4 (the tone all these samples use), you use CalcFrequency(3,3), which returns approximately 261.626.
Listen to the wave file generated and you can hear a popping noise where the tone changes from one frequency to the next: Discontinuous Sine Wave
So why is this? The reason is because how we are generating our sine waves makes a discontinuity where the 2 wave files change.
Here you can see the point that the frequencies change and how a pretty small discontinuity can make a pretty big impact on your sound! The sound you are hearing has an official name, called a “pop” (DSP / synth / other audio people will talk about popping in their audio, and discontinuity is the reason for it)
So how do we fix it? Instead of making the sine wave be rigidly based on time, where for each point, we calculate the sine value with no regard to previous values, we use a “Free Spinning Oscillator”.
That is a fancy way of saying we just have a variable keep track of the current PHASE (angle) that we are at in the sine wave for the current sample, and to get the next sample, we advance our phase based on the frequency at the time. Basically our oscillator is a wheel that spins freely, and our current frequency just says how fast to turn the wheel (from wherever it is now) to get the value for the next sample.
Here’s what the looks like in code:
//make a continuous sine wave that changes frequencies
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
if(nIndex < nNumSamples / 2)
{
float fCurrentFrequency = CalcFrequency(3,3);
fPhase += 2 * (float)M_PI * fCurrentFrequency/(float)nSampleRate;
while(fPhase >= 2 * (float)M_PI)
fPhase -= 2 * (float)M_PI;
while(fPhase < 0)
fPhase += 2 * (float)M_PI;
pData[nIndex] = sin(fPhase);
}
else
{
float fCurrentFrequency = CalcFrequency(3,4);
fPhase += 2 * (float)M_PI * fCurrentFrequency/(float)nSampleRate;
while(fPhase >= 2 * (float)M_PI)
fPhase -= 2 * (float)M_PI;
while(fPhase < 0)
fPhase += 2 * (float)M_PI;
pData[nIndex] = sin(fPhase);
}
}
WriteWaveFile("sinecon.wav",pData,nNumSamples,nNumChannels,nSampleRate);
Note that we keep the phase between 0 and 2 * PI. There’s no mathematical reason for needing to do this, but in floating point math, if you let a value get too large, it starts to lose precision. That means, that if you made a wave file that lasted a long time, the audio would start to degrade the longer it played. I also use a while loop instead of a regular if statement, because if someone uses very large frequencies, you can pass 2 * PI a couple of times in a single sample. Also, i check that it’s above zero, because it is valid to use negative frequency values! All stuff to be mindful of when making your own synth programs (:
Here’s what the generated wave file sounds like, notice the smooth transition between the two notes:
Continuous Sine Wave
And here’s what it looks like visually where the wave changes frequency, which you can see is nice and smooth (the bottom wave). The top wave is the popping sine wave image again at the same point in time for reference. On the smooth wave it isn’t even visually noticeable that the frequency has changed.
One last word on this… popping is actually sometimes desired and can help make up a part of a good sound. For instance, some percussion sounds can make use of popping to sound more appropriate!
Sine Wave Oscillator
For our final incarnation of a sine wave oscillator, here’s a nice simple helper function:
float AdvanceOscilator_Sine(float &fPhase, float fFrequency, float fSampleRate)
{
fPhase += 2 * (float)M_PI * fFrequency/fSampleRate;
while(fPhase >= 2 * (float)M_PI)
fPhase -= 2 * (float)M_PI;
while(fPhase < 0)
fPhase += 2 * (float)M_PI;
return sin(fPhase);
}
You pass that function your current phase, the frequency you want, and the sample rate, and it will advance your phase, and return the value for your next audio sample.
Here’s an example of how to use it:
//make a sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate);
}
WriteWaveFile("sine.wav",pData,nNumSamples,nNumChannels,nSampleRate);
Here’s what it sounds like (nothing new at this point!):
Vanilla Sine Wave
Wave Amplitude, Volume and Clipping
You can adjust the AMPLITUDE of any wave form by multiplying each sample by a value. Values greater than one increase the amplitude, making it louder, values less than one decrease the amplitude, making it quieter, and negative values flip the wave over, but also have the ability to make it quieter or louder.
One place people use negative amplitudes (volumes) is for noise cancellation. If you have a complex sound that has some noise in it, but you know the source of the noise, you can take that noice, multiply it by -1 to get a volume of -1, and ADD IT (or MIX IT) into the more complex sound, effectively removing the noise from the sound. There are other uses too but this is one concrete, real world example.
This code sample generates a quieter wave file:
//make a quieter sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate) * 0.4f;
}
WriteWaveFile("sinequiet.wav",pData,nNumSamples,nNumChannels,nSampleRate);
And here’s what that sounds like:
Vanilla Sine Wave – Quiet
And here’s what that looks like:
If you recall though, when we write a wave file, we map -1 to the smallest int number we can store, and 1 to the highest int number we can store. What happens if we make something too loud, so that it goes above 1.0 or below -1.0?
One way to fix this would be to “Normalize” the sound data. To normalize it, you would loop through each sample in the stream and find the highest absolute value sample. For instance if you had 3 samples: 1.0, -1.2, 0.8, the highest absolute sample value would be 1.2.
Once you have this value, you loop through the samples in the stream and divide by this number. After you do this, every sample in the stream will be within the range -1 to 1. Note that if you had any data that would be clipping, this process has the side effect of making your entire stream quieter since it reduces the amplitude of every sample. If you didn’t have any clipping data, this process has the side effect of making your entire stream louder because it increases the amplitude of every sample.
Another way to deal with it is to just clamp the values to the -1, 1 range. In the case of a sine wave, that means we chop off the top and/or the bottom of the wave and there’s just a flat plateau where the numbers went out of range.
This is called clipping, and along with popping are 2 of the main problems people have with audio quality degradation. Aliasing is a third, and is something we address in the next chapter by the way! (http://en.wikipedia.org/wiki/Aliasing)
Here’s some code for generating a clipping sine wave:
//make a clipping sine wave
for(int nIndex = 0; nIndex < nNumSamples; ++nIndex)
{
pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate) * 1.4f;
}
WriteWaveFile("sineclip.wav",pData,nNumSamples,nNumChannels,nSampleRate);
And here’s what it sounds like:
Vanilla Sine Wave – Clipping
Also, here’s what it looks like:
Note that in this case, it doesn’t necessarily sound BAD compared to a regular, non clipping sine wave, but it does sound different. That might be a good thing, or a bad thing, depending on your intentions. With more complex sounds, like voice, or acoustic music, this will usually make it sound terrible. Audio engineers have to carefully control the levels (volumes) of the channels being mixed (added) together to make sure the resulting output doesn’t go outside of the valid range and cause clipping. Also, in analog hardware, going out of range can cause damage to the devices if they aren’t built to protect themselves from it!
In the case of real time synthesis, as you might imagine, normalizing wave data is impossible to do because it requires that you know all the sound data up front to be able to normalize the data. In real time applications, besides just making sure the levels keep everything in range, you also have the option of using a compressor which sort of dynamically normalizes on the fly. Check this out for more information: http://en.wikipedia.org/wiki/Dynamic_range_compression
Square Wave Oscillator
Here’s the code for the square wave oscillator:
float AdvanceOscilator_Square(float &fPhase, float fFrequency, float fSampleRate)
{
fPhase += fFrequency/fSampleRate;
while(fPhase > 1.0f)
fPhase -= 1.0f;
while(fPhase < 0.0f)
fPhase += 1.0f;
if(fPhase <= 0.5f)
return -1.0f;
else
return 1.0f;
}
Note that we are using the phase as if it’s a percentage, instead of an angle. Since we are using it differently, that means if you switch from sine wave to square wave, there will be a discontinuity (a pop). However, in practice this happens anyways almost all the time because unless you change from sine to square at the very top or bottom of the sine wave, there will be discontinuity anyways. In reality, this really doesn’t matter, but you could “fix” it to switch only on those boundaries, or you could use “cross fading” or “blending” to fade one wave out (decrease amplitude from 1 to 0), while bringing the new wave in (increase amplitude from 0 to 1), adding them together to get the output. Doing so will make a smooth transition but adds some complexity, and square waves by nature constantly pop anyways – it’s what gives them their sound!
Here’s what a square wave sounds like and looks like:
Square Wave
Saw Wave Oscillator
We used the saw wave in chapter one. Here’s the code for a saw wave oscillator:
float AdvanceOscilator_Saw(float &fPhase, float fFrequency, float fSampleRate)
{
fPhase += fFrequency/fSampleRate;
while(fPhase > 1.0f)
fPhase -= 1.0f;
while(fPhase < 0.0f)
fPhase += 1.0f;
return (fPhase * 2.0f) - 1.0f;
}
Here’s what a saw wave looks and sounds like:
Saw Wave
Note that sometimes saw waves point the other direction and the “drop off” is on the left instead of on the right, and the rest of the way descends instead of rises but as far as I have seen, there is no audible or practical difference.
Triangle Wave Oscillator
A lot of synths don’t even bother with a triangle wave, and those that do, are just for approximations of a sine wave. A triangle wave sounds a lot like a sine wave and looks a bit like it too.
Here’s the code for a triangle wave oscillator:
float AdvanceOscilator_Triangle(float &fPhase, float fFrequency, float fSampleRate)
{
fPhase += fFrequency/fSampleRate;
while(fPhase > 1.0f)
fPhase -= 1.0f;
while(fPhase < 0.0f)
fPhase += 1.0f;
float fRet;
if(fPhase <= 0.5f)
fRet=fPhase*2;
else
fRet=(1.0f - fPhase)*2;
return (fRet * 2.0f) - 1.0f;
}
Here’s what it looks and sounds like:
Triangle Wave
Noise Oscillator
Believe it or not, even static has it’s place too. It’s used sometimes for percussion (put an envelope around some static to make a “clap” sound), it can be used as a low frequency oscillator aka LFO (the old “hold and sample” type stuff) and other things as well. Static is just random audio samples.
The code for a noise oscillator is slightly different than the others. You have to pass it the last sample generated (you can pass 0 if it’s the first sample) and it will continue returning that last value until it’s time to generate a new random number. It determines when it’s time based on the frequency you pass in. A higher frequency mean more random numbers will be chosen in the same amount of audio data while a lower frequency means that fewer random numbers will be chosen.
At lower frequencies (like in the sample), it kind of sounds like an explosion or rocket ship sound effect from the 80s which is fun 😛
Here’s the code:
float AdvanceOscilator_Noise(float &fPhase, float fFrequency, float fSampleRate, float fLastValue)
{
unsigned int nLastSeed = (unsigned int)fPhase;
fPhase += fFrequency/fSampleRate;
unsigned int nSeed = (unsigned int)fPhase;
while(fPhase > 2.0f)
fPhase -= 1.0f;
if(nSeed != nLastSeed)
{
float fValue = ((float)rand()) / ((float)RAND_MAX);
fValue = (fValue * 2.0f) - 1.0f;
//uncomment the below to make it slightly more intense
/*
if(fValue < 0)
fValue = -1.0f;
else
fValue = 1.0f;
*/
return fValue;
}
else
{
return fLastValue;
}
}
Here’s what it looks and sounds like:
Noise
I think it kind of looks like the Arizona desert 😛
As a quick aside, i have the random numbers as random floating point numbers (they can be anything between -1.0 and 1.0). Another way to generate noise is to make it so it will choose only EITHER -1 or 1 and nothing in between. It gives a slightly harsher sound. The code to do that is in the oscillator if you want to try it out, it’s just commented out. There are other ways to generate noise too (check out “pink noise” http://en.wikipedia.org/wiki/Pink_noise) but this ought to be good enough for our immediate needs!
More Exotic Wave Forms
Two other oscillators I’ve used on occasion is the squared sine wave and the rectangle wave.
To create a “squared sine wave” all you need to do is multiply each sample by itself (square the audio sample). This makes a wave form that is similar to sine waves, but a little bit different, and sounds a bit different too.
A rectangle wave is created by making it so the wave spends either more or less time in the “up” or “down” part of the wave. Instead of it being 50% of the time in “up”, and 50% of the time in “down” you can make it so it spends 80% of the time in up, and 20% of the time in down. It makes it sound quite a bit different, and the more different the percentages are, the “brighter” it sounds.
Also, you can add multiple wave form samples together to get more interesting wave forms (like adding a triangle and a square wave of the same frequency together, and reducing the amplitude to avoid clipping). That’s called additive synthesis and we’ll talk more about that next chapter, including how to make more correct wave forms using sine waves to avoid aliasing.
You can also multiply wave forms together to create other, more interesting waves. Strictly speaking this is called AM synthesis (amplitude modulation synthesis) which is also sometimes known as ring modulation when done a certain way.
As you can see, there are a lot of different ways to create oscillators, and the wave forms are just limited by your imagination. Play around and try to make your own oscillators and experiment!
Final Samples
Now we have the simple basics down for being able to create music. here’s a small “song” that is generated in the sample code:
Simple Song
And just to re-inforce how important keeping your wave data continuous is, here’s the same wave file, but about 0.75 seconds in a put a SINGLE -1.0 sample where it doesn’t belong. a single sample wrong when there’s 44100 samples per second and look how much it affects the audio.
Simple Song With Pop
Until Next Time…
Next up we will talk about “aliasing” and how to avoid it, making much better sounding saw, square and triangle waves that are less harsh on the ears.
Code
Here’s the code that goes with this post:
/*=================================================== Written by Alan Wolfe 5/2012 http://demofox.org some useful links about the wave file format: http://www.piclist.com/techref/io/serial/midi/wave.html https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ Note: This source code assumes that you are on a little endian machine. ===================================================*/ #include #include #include #define _USE_MATH_DEFINES #include //define our types. If your environment varies, you can change these types to be what they should be typedef int int32; typedef unsigned int uint32; typedef short int16; typedef unsigned short uint16; typedef signed char int8; typedef unsigned char uint8; //some macros #define CLAMP(value,min,max) {if(value max) { value = max; }} //this struct is the minimal required header data for a wav file struct SMinimalWaveFileHeader { //the main chunk unsigned char m_szChunkID[4]; uint32 m_nChunkSize; unsigned char m_szFormat[4]; //sub chunk 1 "fmt " unsigned char m_szSubChunk1ID[4]; uint32 m_nSubChunk1Size; uint16 m_nAudioFormat; uint16 m_nNumChannels; uint32 m_nSampleRate; uint32 m_nByteRate; uint16 m_nBlockAlign; uint16 m_nBitsPerSample; //sub chunk 2 "data" unsigned char m_szSubChunk2ID[4]; uint32 m_nSubChunk2Size; //then comes the data! }; //0 to 255 void ConvertFloatToAudioSample(float fFloat, uint8 &nOut) { fFloat = (fFloat + 1.0f) * 127.5f; CLAMP(fFloat,0.0f,255.0f); nOut = (uint8)fFloat; } //–32,768 to 32,767 void ConvertFloatToAudioSample(float fFloat, int16 &nOut) { fFloat *= 32767.0f; CLAMP(fFloat,-32768.0f,32767.0f); nOut = (int16)fFloat; } //–2,147,483,648 to 2,147,483,647 void ConvertFloatToAudioSample(float fFloat, int32 &nOut) { double dDouble = (double)fFloat; dDouble *= 2147483647.0; CLAMP(dDouble,-2147483648.0,2147483647.0); nOut = (int32)dDouble; } //calculate the frequency of the specified note. //fractional notes allowed! float CalcFrequency(float fOctave,float fNote) /* Calculate the frequency of any note! frequency = 440×(2^(n/12)) N=0 is A4 N=1 is A#4 etc... notes go like so... 0 = A 1 = A# 2 = B 3 = C 4 = C# 5 = D 6 = D# 7 = E 8 = F 9 = F# 10 = G 11 = G# */ { return (float)(440*pow(2.0,((double)((fOctave-4)*12+fNote))/12.0)); } //this writes a wave file //specify sample bit count as the template parameter //can be uint8, int16 or int32 template bool WriteWaveFile(const char *szFileName, float *pRawData, int32 nNumSamples, int16 nNumChannels, int32 nSampleRate) { //open the file if we can FILE *File = fopen(szFileName,"w+b"); if(!File) { return false; } //calculate bits per sample and the data size int32 nBitsPerSample = sizeof(T) * 8; int nDataSize = nNumSamples * sizeof(T); SMinimalWaveFileHeader waveHeader; //fill out the main chunk memcpy(waveHeader.m_szChunkID,"RIFF",4); waveHeader.m_nChunkSize = nDataSize + 36; memcpy(waveHeader.m_szFormat,"WAVE",4); //fill out sub chunk 1 "fmt " memcpy(waveHeader.m_szSubChunk1ID,"fmt ",4); waveHeader.m_nSubChunk1Size = 16; waveHeader.m_nAudioFormat = 1; waveHeader.m_nNumChannels = nNumChannels; waveHeader.m_nSampleRate = nSampleRate; waveHeader.m_nByteRate = nSampleRate * nNumChannels * nBitsPerSample / 8; waveHeader.m_nBlockAlign = nNumChannels * nBitsPerSample / 8; waveHeader.m_nBitsPerSample = nBitsPerSample; //fill out sub chunk 2 "data" memcpy(waveHeader.m_szSubChunk2ID,"data",4); waveHeader.m_nSubChunk2Size = nDataSize; //write the header fwrite(&waveHeader,sizeof(SMinimalWaveFileHeader),1,File); //write the wave data itself, converting it from float to the type specified T *pData = new T[nNumSamples]; for(int nIndex = 0; nIndex = 2 * (float)M_PI) fPhase -= 2 * (float)M_PI; while(fPhase 1.0f) fPhase -= 1.0f; while(fPhase 1.0f) fPhase -= 1.0f; while(fPhase < 0.0f) fPhase += 1.0f; if(fPhase 1.0f) fPhase -= 1.0f; while(fPhase < 0.0f) fPhase += 1.0f; float fRet; if(fPhase 2.0f) fPhase -= 1.0f; if(nSeed != nLastSeed) { float fValue = ((float)rand()) / ((float)RAND_MAX); fValue = (fValue * 2.0f) - 1.0f; //uncomment the below to make it slightly more intense /* if(fValue < 0) fValue = -1.0f; else fValue = 1.0f; */ return fValue; } else { return fLastValue; } } //the entry point of our application int main(int argc, char **argv) { //our parameters that all the wave forms use int nSampleRate = 44100; int nNumSeconds = 4; int nNumChannels = 1; float fFrequency = CalcFrequency(3,3); // middle C //make our buffer to hold the samples int nNumSamples = nSampleRate * nNumChannels * nNumSeconds; float *pData = new float[nNumSamples]; //the phase of our oscilator, we don't really need to reset it between wave files //it just needs to stay continuous within a wave file float fPhase = 0; //make a naive sine wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fFrequency / (float)nSampleRate); } WriteWaveFile("sinenaive.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a discontinuitous (popping) sine wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { if(nIndex < nNumSamples / 2) { float fCurrentFrequency = CalcFrequency(3,3); pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fCurrentFrequency / (float)nSampleRate); } else { float fCurrentFrequency = CalcFrequency(3,4); pData[nIndex] = sin((float)nIndex * 2 * (float)M_PI * fCurrentFrequency / (float)nSampleRate); } } WriteWaveFile("sinediscon.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a continuous sine wave that changes frequencies for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { if(nIndex = 2 * (float)M_PI) fPhase -= 2 * (float)M_PI; while(fPhase = 2 * (float)M_PI) fPhase -= 2 * (float)M_PI; while(fPhase < 0) fPhase += 2 * (float)M_PI; pData[nIndex] = sin(fPhase); } } WriteWaveFile("sinecon.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a sine wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate); } WriteWaveFile("sine.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a quieter sine wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate) * 0.4f; } WriteWaveFile("sinequiet.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a clipping sine wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = AdvanceOscilator_Sine(fPhase,fFrequency,(float)nSampleRate) * 1.4f; } WriteWaveFile("sineclip.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a saw wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = AdvanceOscilator_Saw(fPhase,fFrequency,(float)nSampleRate); } WriteWaveFile("saw.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a square wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = AdvanceOscilator_Square(fPhase,fFrequency,(float)nSampleRate); } WriteWaveFile("square.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a triangle wave for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { pData[nIndex] = AdvanceOscilator_Triangle(fPhase,fFrequency,(float)nSampleRate); } WriteWaveFile("triangle.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make some noise or... make... some... NOISE!!! for(int nIndex = 0; nIndex 0 ? pData[nIndex-1] : 0.0f); } WriteWaveFile("noise.wav",pData,nNumSamples,nNumChannels,nSampleRate); //make a dumb little song for(int nIndex = 0; nIndex < nNumSamples; ++nIndex) { //calculate which quarter note we are on int nQuarterNote = nIndex * 4 / nSampleRate; float fQuarterNotePercent = (float)((nIndex * 4) % nSampleRate) / (float)nSampleRate; //intentionally add a "pop" noise mid way through the 3rd quarter note if(nIndex == nSampleRate * 3 / 4 + nSampleRate / 8) { pData[nIndex] = -1.0f; continue; } //do different logic based on which quarter note we are on switch(nQuarterNote) { case 0: { pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,0),(float)nSampleRate); break; } case 1: { pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,2),(float)nSampleRate); break; } case 2: case 3: { pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,5),(float)nSampleRate); break; } case 4: { pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate); break; } case 5: { pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,3 + fQuarterNotePercent * 2.0f),(float)nSampleRate); break; } case 6: { pData[nIndex] = AdvanceOscilator_Sine(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate) * (1.0f - fQuarterNotePercent); break; } case 8: { pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,0),(float)nSampleRate); break; } case 9: { pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,2),(float)nSampleRate); break; } case 10: case 11: { pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,5),(float)nSampleRate); break; } case 12: { pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate); break; } case 13: { pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,3 + fQuarterNotePercent * 2.0f),(float)nSampleRate); break; } case 14: { pData[nIndex] = AdvanceOscilator_Saw(fPhase,CalcFrequency(4,5 - fQuarterNotePercent * 2.0f),(float)nSampleRate) * (1.0f - fQuarterNotePercent); break; } default: { pData[nIndex] = 0; break; } } } WriteWaveFile("song.wav",pData,nNumSamples,nNumChannels,nSampleRate); //free our data buffer delete[] pData; }
Pingback: DIY Synth 1: Sound Output | The blog at the bottom of the sea
So many thanks for this! I’ve implemented these types of waves myself over the last couple of days, but your solutions seem so much simpler than messing about with timings and time relative positions, they also run slightly faster (as in 0.1% CPU difference, but still) than mine.
A few things I wanted to mention:
1) In the triangle wave generator, first whatever gets assigned to fRet is multiplied *2, and then in the return line whatever is in fRet is multiplied *2 again… why not just multiply *4 right away? One multiplication less.
2) I work with references to local state variables for return values, rather than copies, seems more efficient. If you give the square wave a float state=0.f; and a kind of float[2] wavetable = {-1.f,1.f}; then you can evaluate the oscillator in a one-liner like state=wavetable[std::ceil(phase – 0.5)] * amplitude; and just return that. (I don’t like branching where it isn’t necessary.)
3) Is there a reason why you use while-statements rather than if-branches?
4) To save superfluous multiplications, it would pay off to add a few stateful variables to holds the current Fc,Fs and (Fc/Fs) values. Just something like float srate=0.f; float freq=0.f; float fraction=0.f; and then add a function that handles value updates only when they’re necessary, like a void setup (const float& SR, const float& Hz) { bool updated=false; if (SR!=srate){srate=SR;updated=true;} if (Hz!=freq){freq=Hz;updated=true;} if (updated==true){fraction=freq/srate;updated=false;} } and then just call setup(SR,Hz) in the per-sample tick method before any calculations. If the values don’t change, the “did the values change” checks should/could be faster than calculating (Fc/Fs) over and over again.
LikeLike
5) For the saw wave oscillator, you could add a float direction=1.f; that can be set to 1.0 or -1, and multiply the (Fc/Fs) frequency fraction by it when adding it fPhase at the beginning of the tick method. At 1.0, you’d get the regular rising saw wave, but at -1 you’d get a falling (=reversed) saw wave. Intermediate values are less useful, they just mess with the phase/width.
6) You can easily make a pulse wave from a square wave and a width factor. Considering that the phase value is scaled between 0.0 and 1.0, the positive pulse will start at phase 0.0 and be +1.0 while phase < width, and the negative pulse will start at phase 0.5 and remain -1.0 as long as phase 1.0, if <0.0, etc.) do this:
if (phase = 0.5f) and (phase < 0.5f + width)) {return -amplitude;}
else {return 0.f;}
Hope someone finds this useful… 🙂
LikeLike
Looks like part of that comment went missing… the correct code snippet to put after the phase range wrapping for a pulse wave would be:
if (phase = 0.5) and (phase < 0.5 + width)) { return -amplitude; }
else { return 0.0; }
(Sorry for spam… 🙂 )
LikeLike
Wow, something weird is going on… the code is still not correct, but I made 100% sure that I entered it correctly… I guess something’s cutting out anything between opening and closing “tag” brackets, i.e. greater than and less than symbols… there should be the word “test” here:
LikeLike
I think your suggestions are all decent & appropriate. I think the motivation for the while loop was that phase might change by a whole lot or start out of range. It was 6 years ago though (!) so who knows what i was thinking.
It looks like this post got broken in a hosting migration too, I’ll have to fix it up.
Thanks for the comments Rob!
LikeLike