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!
Flange is a pretty interesting audio effect. It can give character to the most monotonous of sounds and it can also make good sounds even better.
Before we dive into it, check out these raw and flanged sound files to get a glimpse of what we’re talking about. The flanged files were created with the simple c++ sample code at the end of this chapter. Only standard header files used, so you too will be able to flange sounds by the end of this post!
A clip from the movie “legend”
Raw: legend.wav
Flanged: legend_f.wav
A drum loop
Raw: cymbal.wav
Flanged: cymbal_f.wav
How Does it Work?
The idea behind flange is actually pretty simple. All you do is you mix a sound with itself, but have one of the copies speed up and slow down (like, on a sine wave) instead of letting it play at normal speed. This makes the sounds mix together at different (but similar) points in time. Since sound is made up of peaks (positive numbers) and valleys (negative numbers), mixing the sound with the time offset sound causes some of the peaks and valleys to grow larger, and causes others to get smaller as they cancel each other out. This results in the distinctive flange sound.
The simple way to flange a file would be to load all of the audio samples into memory and do something like this:
for (int i = 0; i = flangeSampleDepth) output[i] += input[i - offset]; }
It’s important to note though that for better quality flanging sounds, you should actually use a flange with sub-sample accuracy. That way if your sine wave says it wants sample 3.6, it means your resulting sample should sample[3] * 0.4 + sample[4] * 0.6. That is just doing a linear interpolation to get the “inbetween” data of the samples, which works well enough for my needs, but higher quality flangers will use higher quality interpolation techniques and curve fitting.
Who invented the flanger is apparently not agreed on, but it’s origin is back in the days of tape deck based audio recording studios. If you put your finger on one of the tape flanges and slow it down, if you then mix that result with an undelayed version of the same sound, you’d start to hear the flanging effect.
These days we rely on hardware and software to emulate that.
If you have ever accidentally played too many copies of the same sound too closely together, you’ve probably heard a flange-like effect. It sounds fairly similar, but you don’t get the sweeping effect that you do with flange.
Some flanges also feed their output back into their input to further the effect and add some resonance. We aren’t doing that in this post, but feel free to experiment with that on your own! (check the links section for more info)
It’s important to note that you can use the same process on LIVE music to do flanging in real time. If you have a “delay buffer” to hold the last N seconds of sound, you can use the sine wave to control what part of that delay buffer mixes with the current sound coming out.
Flange Parameters
Flangers often have two parameters (at least). One parameter controls the frequency of the LFO (low frequency oscilator) sine wave. The other parameter controls it’s “depth” which means how far backwards or forwards in time the non-real-time sound can go.
Good frequency values of the oscilator depends entirely on the sound you are flanging as well as the style you are going for, but usually small values like less than 5 hz works best. I usually will use a value less than 1, and for best results I like to make it a value that isn’t likely to line up with the tempo of the music – such as perhaps 0.374.
The reason for this is that flange adds some interesting flavor to your sound, and if you had a value like 0.25 for your flanger, every 4 notes would always sound the same and line up with the flange effect. if instead, you have it at something like 0.374, you can play a repeating melody SEVERAL times over and over, and due to the flange effect, each time through the notes will sound different and accoustically interesting.
The best values of the other parameter (the flange depth), also varies depending on your source sounds and the sound you are going after. People usually suggest doing no more than 20ms though. I personally really enjoy the sound of a much smaller value, such as 1ms. Play around with different values and see what you like!
Flanging Basic Wave Forms
Here are some more flange samples of the basic wave forms, to give you an idea of how flange behaves with the various wave forms:
Triangle:
Raw: triangle.wav
Flanged: triangle_f.wav
Bandlimited Triangle:
Raw: triangleBL.wav
Flanged: triangleBL_f.wav
Saw:
Raw: saw.wav
Flanged: saw_f.wav
Bandlimited Saw:
Raw: sawBL.wav
Flanged: sawBL_f.wav
Square:
Raw: square.wav
Flanged: square_f.wav
Bandlimited Square:
Raw: squareBL.wav
Flanged: squareBL_f.wav
Sine:
Raw: sine1.wav
Flanged: sine_f.wav
Sample Code
This sample code reads in “in.wav” flanges it at 4hz with a 1ms depth, and writes out “out.wav”. Note, the wave file reading code is not bullet proof, sorry! It seems to work well with mono 16 bit wave files, but if you need better sound file reading, i suggest looking at libsndfile (link in links section!)
#define _CRT_SECURE_NO_WARNINGS #include #include #include #include #include #include #define _USE_MATH_DEFINES #include //===================================================================================== // SNumeric - uses phantom types to enforce type safety //===================================================================================== template struct SNumeric { public: explicit SNumeric(const T &value) : m_value(value) { } SNumeric() : m_value() { } inline T& Value() { return m_value; } inline const T& Value() const { return m_value; } typedef SNumeric TType; typedef T TInnerType; // Math Operations TType operator+ (const TType &b) const { return TType(this->Value() + b.Value()); } TType operator- (const TType &b) const { return TType(this->Value() - b.Value()); } TType operator* (const TType &b) const { return TType(this->Value() * b.Value()); } TType operator/ (const TType &b) const { return TType(this->Value() / b.Value()); } TType& operator+= (const TType &b) { Value() += b.Value(); return *this; } TType& operator-= (const TType &b) { Value() -= b.Value(); return *this; } TType& operator*= (const TType &b) { Value() *= b.Value(); return *this; } TType& operator/= (const TType &b) { Value() /= b.Value(); return *this; } TType& operator++ () { Value()++; return *this; } TType& operator-- () { Value()--; return *this; } // Extended Math Operations template T Divide(const TType &b) { return ((T)this->Value()) / ((T)b.Value()); } // Logic Operations bool operatorValue() < b.Value(); } bool operatorValue() (const TType &b) const { return this->Value() > b.Value(); } bool operator>= (const TType &b) const { return this->Value() >= b.Value(); } bool operator== (const TType &b) const { return this->Value() == b.Value(); } bool operator!= (const TType &b) const { return this->Value() != b.Value(); } private: T m_value; }; //===================================================================================== // Typedefs //===================================================================================== typedef uint8_t uint8; typedef uint16_t uint16; typedef uint32_t uint32; typedef int16_t int16; typedef int32_t int32; // type safe types! typedef SNumeric TFrequency; typedef SNumeric TTimeMs; typedef SNumeric TSamples; typedef SNumeric TFractionalSamples; typedef SNumeric TDecibels; typedef SNumeric TAmplitude; typedef SNumeric TChannelCount; typedef SNumeric TPhase; //===================================================================================== // Constants //===================================================================================== static const float c_pi = (float)M_PI; static const float c_twoPi = c_pi * 2.0f; //===================================================================================== // Structs //===================================================================================== struct SSoundSettings { TSamples m_sampleRate; TTimeMs m_lengthMs; TChannelCount m_numChannels; TSamples m_currentSample; }; //===================================================================================== // Conversion Functions //===================================================================================== inline TDecibels AmplitudeToDB(TAmplitude volume) { return TDecibels(log10(volume.Value())); } inline TAmplitude DBToAmplitude(TDecibels dB) { return TAmplitude(pow(10.0f, dB.Value() / 20.0f)); } TSamples SecondsToSamples(const SSoundSettings &s, float seconds) { return TSamples((int)(seconds * (float)s.m_sampleRate.Value())); } TSamples MilliSecondsToSamples(const SSoundSettings &s, float milliseconds) { return SecondsToSamples(s, milliseconds / 1000.0f); } TTimeMs SecondsToMilliseconds(float seconds) { return TTimeMs((uint32)(seconds * 1000.0f)); } TFrequency Frequency(float octave, float note) { /* frequency = 440×(2^(n/12)) Notes: 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 TFrequency((float)(440 * pow(2.0, ((double)((octave - 4) * 12 + note)) / 12.0))); } template T AmplitudeToAudioSample(const TAmplitude& in) { const T c_min = std::numeric_limits::min(); const T c_max = std::numeric_limits::max(); const float c_minFloat = (float)c_min; const float c_maxFloat = (float)c_max; float ret = in.Value() * c_maxFloat; if (ret c_maxFloat) return c_max; return (T)ret; } TAmplitude GetLerpedAudioSample(const std::vector& samples, TFractionalSamples& index) { // get the index of each sample and the fractional blend amount uint32 a = (uint32)floor(index.Value()); uint32 b = a + 1; float fract = index.Value() - floor(index.Value()); // get our two amplitudes float ampA = 0.0f; if (a >= 0 && a = 0 && b < samples.size()) ampB = samples[b].Value(); // return the lerped result return TAmplitude(fract * ampB + (1.0f - fract) * ampA); } void NormalizeSamples(std::vector& samples, TAmplitude maxAmplitude) { // nothing to do if no samples if (samples.size() == 0) return; // 1) find the largest absolute value in the samples. TAmplitude largestAbsVal = TAmplitude(abs(samples.front().Value())); std::for_each(samples.begin() + 1, samples.end(), [&largestAbsVal](const TAmplitude &a) { TAmplitude absVal = TAmplitude(abs(a.Value())); if (absVal > largestAbsVal) largestAbsVal = absVal; } ); // 2) adjust largestAbsVal so that when we divide all samples, none will be bigger than maxAmplitude // if the value we are going to divide by is <= 0, bail out largestAbsVal /= maxAmplitude; if (largestAbsVal = TAmplitude(1.0f)) { int ijkl = 0; } } ); } void ResampleData(std::vector& samples, int srcSampleRate, int destSampleRate) { //if the requested sample rate is the sample rate it already is, bail out and do nothing if (srcSampleRate == destSampleRate) return; //calculate the ratio of the old sample rate to the new float fResampleRatio = (float)destSampleRate / (float)srcSampleRate; //calculate how many samples the new data will have and allocate the new sample data int nNewDataNumSamples = (int)((float)samples.size() * fResampleRatio); std::vector newSamples; newSamples.resize(nNewDataNumSamples); //get each lerped output sample. There are higher quality ways to resample for(int nIndex = 0; nIndex < nNewDataNumSamples; ++nIndex) newSamples[nIndex] = GetLerpedAudioSample(samples, TFractionalSamples((float)nIndex / fResampleRatio)); //free the old data and set the new data std::swap(samples, newSamples); } void ChangeNumChannels(std::vector& samples, int nSrcChannels, int nDestChannels) { //if the number of channels requested is the number of channels already there, or either number of channels is not mono or stereo, return if(nSrcChannels == nDestChannels || nSrcChannels 2 || nDestChannels 2) { return; } //if converting from mono to stereo, duplicate the mono channel to make stereo if(nDestChannels == 2) { std::vector newSamples; newSamples.resize(samples.size() * 2); for (size_t index = 0; index < samples.size(); ++index) { newSamples[index * 2] = samples[index]; newSamples[index * 2 + 1] = samples[index]; } std::swap(samples, newSamples); } //else converting from stereo to mono, mix the stereo channels together to make mono else { std::vector newSamples; newSamples.resize(samples.size() / 2); for (size_t index = 0; index < samples.size() / 2; ++index) newSamples[index] = samples[index * 2] + samples[index * 2 + 1]; std::swap(samples, newSamples); } } float PCMToFloat(unsigned char *pPCMData, int nNumBytes) { switch(nNumBytes) { case 1: { uint8 data = pPCMData[0]; return (float)data / 255.0f; } case 2: { int16 data = pPCMData[1] << 8 | pPCMData[0]; return ((float)data) / ((float)0x00007fff); } case 3: { int32 data = pPCMData[2] << 16 | pPCMData[1] << 8 | pPCMData[0]; return ((float)data) / ((float)0x007fffff); } case 4: { int32 data = pPCMData[3] << 24 | pPCMData[2] << 16 | pPCMData[1] << 8 | pPCMData[0]; return ((float)data) / ((float)0x7fffffff); } default: { return 0.0f; } } } //===================================================================================== // Wave File Writing Code //===================================================================================== struct SMinimalWaveFileHeader { //the main chunk unsigned char m_szChunkID[4]; //0 uint32 m_nChunkSize; //4 unsigned char m_szFormat[4]; //8 //sub chunk 1 "fmt " unsigned char m_szSubChunk1ID[4]; //12 uint32 m_nSubChunk1Size; //16 uint16 m_nAudioFormat; //18 uint16 m_nNumChannels; //20 uint32 m_nSampleRate; //24 uint32 m_nByteRate; //28 uint16 m_nBlockAlign; //30 uint16 m_nBitsPerSample; //32 //sub chunk 2 "data" unsigned char m_szSubChunk2ID[4]; //36 uint32 m_nSubChunk2Size; //40 //then comes the data! }; //this writes a wave file template bool WriteWaveFile(const char *fileName, const std::vector &samples, const SSoundSettings &sound) { //open the file if we can FILE *file = fopen(fileName, "w+b"); if (!file) return false; //calculate bits per sample and the data size const int32 bitsPerSample = sizeof(T) * 8; const int dataSize = samples.size() * sizeof(T); SMinimalWaveFileHeader waveHeader; //fill out the main chunk memcpy(waveHeader.m_szChunkID, "RIFF", 4); waveHeader.m_nChunkSize = dataSize + 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 = sound.m_numChannels.Value(); waveHeader.m_nSampleRate = sound.m_sampleRate.Value(); waveHeader.m_nByteRate = sound.m_sampleRate.Value() * sound.m_numChannels.Value() * bitsPerSample / 8; waveHeader.m_nBlockAlign = sound.m_numChannels.Value() * bitsPerSample / 8; waveHeader.m_nBitsPerSample = bitsPerSample; //fill out sub chunk 2 "data" memcpy(waveHeader.m_szSubChunk2ID, "data", 4); waveHeader.m_nSubChunk2Size = dataSize; //write the header fwrite(&waveHeader, sizeof(SMinimalWaveFileHeader), 1, file); //write the wave data itself, converting it from float to the type specified std::vector outSamples; outSamples.resize(samples.size()); for (size_t index = 0; index < samples.size(); ++index) outSamples[index] = AmplitudeToAudioSample(samples[index]); fwrite(&outSamples[0], dataSize, 1, file); //close the file and return success fclose(file); return true; } //loads a wave file in. Converts from source format into the specified format // TOTAL HONESTY: some wave files seem to have problems being loaded through this function and I don't have // time to investigate why. It seems to work best with 16 bit mono wave files. // If you need more robust file loading, check out libsndfile at http://www.mega-nerd.com/libsndfile/ bool ReadWaveFile(const char *fileName, std::vector& samples, int16 numChannels, int32 sampleRate) { //open the file if we can FILE *File = fopen(fileName,"rb"); if(!File) { return false; } //read the main chunk ID and make sure it's "RIFF" char buffer[5]; buffer[4] = 0; if(fread(buffer,4,1,File) != 1 || strcmp(buffer,"RIFF")) { fclose(File); return false; } //read the main chunk size uint32 nChunkSize; if(fread(&nChunkSize,4,1,File) != 1) { fclose(File); return false; } //read the format and make sure it's "WAVE" if(fread(buffer,4,1,File) != 1 || strcmp(buffer,"WAVE")) { fclose(File); return false; } long chunkPosFmt = -1; long chunkPosData = -1; while(chunkPosFmt == -1 || chunkPosData == -1) { //read a sub chunk id and a chunk size if we can if(fread(buffer,4,1,File) != 1 || fread(&nChunkSize,4,1,File) != 1) { fclose(File); return false; } //if we hit a fmt if(!strcmp(buffer,"fmt ")) { chunkPosFmt = ftell(File) - 8; } //else if we hit a data else if(!strcmp(buffer,"data")) { chunkPosData = ftell(File) - 8; } //skip to the next chunk fseek(File,nChunkSize,SEEK_CUR); } //we'll use this handy struct to load in SMinimalWaveFileHeader waveData; //load the fmt part if we can fseek(File,chunkPosFmt,SEEK_SET); if(fread(&waveData.m_szSubChunk1ID,24,1,File) != 1) { fclose(File); return false; } //load the data part if we can fseek(File,chunkPosData,SEEK_SET); if(fread(&waveData.m_szSubChunk2ID,8,1,File) != 1) { fclose(File); return false; } //verify a couple things about the file data if(waveData.m_nAudioFormat != 1 || //only pcm data waveData.m_nNumChannels 2 || //must not have more than 2 waveData.m_nBitsPerSample > 32 || //32 bits per sample max waveData.m_nBitsPerSample % 8 != 0 || //must be a multiple of 8 bites waveData.m_nBlockAlign > 8) //blocks must be 8 bytes or lower { fclose(File); return false; } //figure out how many samples and blocks there are total in the source data int nBytesPerBlock = waveData.m_nBlockAlign; int nNumBlocks = waveData.m_nSubChunk2Size / nBytesPerBlock; int nNumSourceSamples = nNumBlocks * waveData.m_nNumChannels; //allocate space for the source samples samples.resize(nNumSourceSamples); //maximum size of a block is 8 bytes. 4 bytes per samples, 2 channels unsigned char pBlockData[8]; memset(pBlockData,0,8); //read in the source samples at whatever sample rate / number of channels it might be in int nBytesPerSample = nBytesPerBlock / waveData.m_nNumChannels; for(int nIndex = 0; nIndex = TPhase(1.0f)) phase -= TPhase(1.0f); while (phase 0.5f ? 1.0f : -1.0f); } TAmplitude AdvanceOscilator_Triangle(TPhase &phase, TFrequency frequency, TSamples sampleRate) { AdvancePhase(phase, frequency, sampleRate); if (phase > TPhase(0.5f)) return TAmplitude((((1.0f - phase.Value()) * 2.0f) * 2.0f) - 1.0f); else return TAmplitude(((phase.Value() * 2.0f) * 2.0f) - 1.0f); } TAmplitude AdvanceOscilator_Saw_BandLimited(TPhase &phase, TFrequency frequency, TSamples sampleRate) { AdvancePhase(phase, frequency, sampleRate); // sum the harmonics TAmplitude ret(0.0f); for (int harmonicIndex = 1; harmonicIndex <= 4; ++harmonicIndex) { TPhase harmonicPhase = phase * TPhase((float)harmonicIndex); ret += TAmplitude(sin(harmonicPhase.Value()*c_twoPi) / (float)harmonicIndex); } //adjust the volume ret *= TAmplitude(2.0f / c_pi); return ret; } TAmplitude AdvanceOscilator_Square_BandLimited(TPhase &phase, TFrequency frequency, TSamples sampleRate) { AdvancePhase(phase, frequency, sampleRate); // sum the harmonics TAmplitude ret(0.0f); for (int harmonicIndex = 1; harmonicIndex <= 4; ++harmonicIndex) { float harmonicFactor = (float)harmonicIndex * 2.0f - 1.0f; TPhase harmonicPhase = phase * TPhase(harmonicFactor); ret += TAmplitude(sin(harmonicPhase.Value()*c_twoPi) / harmonicFactor); } //adjust the volume ret *= TAmplitude(4.0f / c_pi); return ret; } TAmplitude AdvanceOscilator_Triangle_BandLimited(TPhase &phase, TFrequency frequency, TSamples sampleRate) { AdvancePhase(phase, frequency, sampleRate); // sum the harmonics TAmplitude ret(0.0f); TAmplitude signFlip(1.0f); for (int harmonicIndex = 1; harmonicIndex <= 4; ++harmonicIndex) { float harmonicFactor = (float)harmonicIndex * 2.0f - 1.0f; TPhase harmonicPhase = phase * TPhase(harmonicFactor); ret += TAmplitude(sin(harmonicPhase.Value()*c_twoPi) / (harmonicFactor*harmonicFactor)) * signFlip; signFlip *= TAmplitude(-1.0f); } //adjust the volume ret *= TAmplitude(8.0f / (c_pi*c_pi)); return ret; } //===================================================================================== // Main //===================================================================================== int main(int argc, char **argv) { //our desired sound parameters SSoundSettings sound; sound.m_sampleRate = TSamples(44100); sound.m_lengthMs = SecondsToMilliseconds(4.0f); sound.m_numChannels = TChannelCount(1); // flange effect parameters const TFrequency c_flangeFrequency(0.4f); const TSamples c_flangeDepth(MilliSecondsToSamples(sound, 1.0f)); // load the wave file if we can std::vector inputData; if (!ReadWaveFile("in.wav", inputData, sound.m_numChannels.Value(), sound.m_sampleRate.Value())) { printf("could not load wave file!"); return 0; } // allocate space for the output file std::vector samples; samples.resize(inputData.size()); TSamples envelopeSize = MilliSecondsToSamples(sound, 50.0f); //apply the phase effect to the file TPhase flangePhase(0.0f); for (TSamples index = TSamples(0), numSamples(samples.size()); index < numSamples; ++index) { // calculate envelope at front and end of sound. TAmplitude envelope(1.0f); if (index (numSamples - envelopeSize)) envelope = TAmplitude(1.0f) - TAmplitude((float)(index - (numSamples - envelopeSize)).Value() / (float)envelopeSize.Value()); // make a sine wave that goes from -1 to 0 at the specified frequency TAmplitude flangeSine = AdvanceOscilator_Sine(flangePhase, c_flangeFrequency, sound.m_sampleRate) * TAmplitude(0.5f) - TAmplitude(0.5f); // use that sine wave to calculate an offset backwards in time to sample from TFractionalSamples flangeOffset = TFractionalSamples((float)index.Value()) + TFractionalSamples(flangeSine.Value() * (float)c_flangeDepth.Value()); // mix the sample with the offset sample and apply the envelope for the front and back of the sound samples[index.Value()] = (inputData[index.Value()] + GetLerpedAudioSample(inputData, flangeOffset)) * envelope; } // normalize the amplitude of the samples to make sure they are as loud as possible without clipping // give 3db of headroom NormalizeSamples(samples, DBToAmplitude(TDecibels(-3.0f))); // save as a wave file WriteWaveFile("out.wav", samples, sound); return 0; }
Links
More DIY synth stuff coming soon, I have like 5 more posts I want to make right now, with the last couple being about some pretty awesome stuff I learned about recently!
Wikipedia: Flanging
The difference between flange, phaser & chorus
What is a chorus effect?