Hammond Organ - The OG Synthesizer

Human voices are insanely complicated. For instance, here’s a waveform of of JFK saying “mister speaker”.

mr_speaker.png

Even ignoring the hilariously extreme Boston accent, there’s a lot going on here. You get a similar sense if you look at waveforms for music: that it’s this completely inscrutable thing and that instruments are magic.

While I agree that instruments are magic, there are actually some instruments that produce reasonably decipherable waveforms. The classic example (and one of my favorite instruments) is the Hammond Organ:

Hammond_c3_Emilio_Muñoz.jpg

Hammond Organs used a pretty remarkable set of technologies to produce their sound. There were tone generators that were capable of emanating relatively clean sine wave tones, and drawbars, that the organist could flick at will, which combined these sine waves together to achieve different types of sounds. Other conventional instruments, like a flute or a clarinet, produce sound based on a fundamental frequency and a combination of “harmonics”. The Hammond Organ sought to imitate this by having each drawbar represent a different harmonic.

unnamed.jpg

So let’s create a Hammond Organ!


For reference, here’s a waveform from a Hammond Organ with drawbar settings 888000000. This image is from an amazing series of articles about synthesizing audio that I highly recommend.

hammond_organ_waveform.jpg

We can use this waveform to check our work later.

First, we’ll produce one of the most basic sounds using some C++. A4. A4’s frequency is 440hz. So we can produce a pure A4 by creating a sine wave that oscillates 440 times in one second. Let’s do it:

#include <math.h>
 
#include <vector>
 
const double twoPi = 6.28318530718;
// 44Khz is a common audio format. This means there are 44,100
// samples in each second of audio.
const double sampleRate = 44100.0;
// this is the duration of time between samples
const double sampleStep = 1.0 / 44100.0;
 
std::vector<double> sine(double freq, double duration) {
  std::vector<double> samples;
  for (double t = 0; t < duration; t += sampleStep) {
    samples.push_back(sin(t * twoPi * freq));
  }
  return samples;
}
 
int main(int argc, char* argv[]) {
  std::vector<double> a4_440 = sine(440.0, 1.0);
  return 0;
}

Great! The vector will contain 1 second’s worth of an A4 tone. But how do we listen to it!?!

Luckily, that’s not too hard. We’ll produce a wav file. Wav files are ultra basic, like a bitmap for images. They are simply a sequence of samples, uncompressed. However, they have an annoying header that you need to write, and some of the values in the header are in “little-endian”. I don’t want to get into the bothersome details at this time, but here’s the code to write out a wav file given a vector of -1.0 to 1.0 doubles. Note that there are most certainly libraries that do this and probably have awesome optimizations, but for our purposes, we’ll jerry-rig it:

// writes a 2 byte little endian int
void write2(std::ostream& stream, int data) {
  std::vector<char> buf;
  buf.push_back((data)&0xFF);
  buf.push_back((data >> 8) & 0xFF);
  stream.write(buf.data(), buf.size());
}
 
// writes a 4 byte little endian int
void write4(std::ostream& stream, int data) {
  std::vector<char> buf;
  buf.push_back((data)&0xFF);
  buf.push_back((data >> 8) & 0xFF);
  buf.push_back((data >> 16) & 0xFF);
  buf.push_back((data >> 24) & 0xFF);
  stream.write(buf.data(), buf.size());
}
 
void exportToWav(std::ostream& stream, const std::vector<double>& sampleData) {
  int channels = 1;  // mono, not stereo
  int sampleRate = 44100;
 
  stream.write("RIFF", 4);  // Wav files always start with the word RIFF
 
  // 36 is the size of this silly header.
  int chunkSizeBytes = 36 + sampleData.size() * 2;
 
  write4(stream, chunkSizeBytes);  // How long is the data
  stream.write("WAVE", 4);         // WAVE chunk
  stream.write("fmt ", 4);         // fmt chunk
  write4(stream, 16);              // size of fmt chunk
  write2(stream, 1);               // Format = PCM. Don't know what this means
  write2(stream, channels);        // # of Channels. Again, we're mono.
  write4(stream, sampleRate);      // Sample Rate
  write4(stream, sampleRate * 2);  // Byte rate. 16 bits = 2 bytes per sample
  write2(stream, 2);        // Frame size, we're mono, so frame size is 2 bytes
  write2(stream, 16);       // Bits per sample
  stream.write("data", 4);  // The word data...
  write4(stream, sampleData.size() * 2);  // data chunk size in bytes
  for (size_t i = 0; i < sampleData.size(); i++) {
    // 32767 is the max value of a signed two byte int (i.e. 16 bits per
    // sample from above).
    short int val = (short int)(32767.0 * sampleData.at(i));
    write2(stream, val);
  }
}

Once you have that, you can call it in main like so:

int main(int argc, char* argv[]) {
  std::vector<double> a4_440 = sine(440.0, 1.0);
 
  std::ofstream out;
  out.open("a4.wav", std::ios::out | std::ios::binary);
  exportToWav(out, a4_440);
  out.close();
 
  return 0;
}

And you can run the program and play the resulting file like this:

windows:

g++ .\synth.cpp;.\a.exe;(New-Object Media.Soundplayer "<path_to_file>/a4.wav").Play()

mac:

g++ .\synth.cpp;.\a.out;open "<path_to_file>/a4.wav"

You should be bathed in the glory of the dulcet tone of a pure sine wave. You probably hear some clipping noises. That’s because our waveform ends super abruptly like this:

abrupt_end.png

Let’s add a simple fade in and fade out at the beginning and end of our waveform:

std::vector<double> sine(double freq, double duration, double fade) {
  std::vector<double> samples;
 
  double fadeStart = (duration - fade);
 
  for (double t = 0; t < duration; t += sampleStep) {
    double val = sin(t * twoPi * freq);
    if (t < fade) {
      val *= t / fade;
    } else if (t > fadeStart) {
      val *= (1.0 - (t - fadeStart) / fade);
    }
    samples.push_back(val);
  }
  return samples;
}

So now we can string notes together and there shouldn’t be any clicking:

int main(int argc, char* argv[]) {
  std::vector<double> allSamples;
  std::vector<double> a4_440 = sine(440.0, 1.0, .05);
  allSamples.insert(allSamples.end(), a4_440.begin(), a4_440.end());
  std::vector<double> c4 = sine(261.63, 1.0, .05);
  allSamples.insert(allSamples.end(), c4.begin(), c4.end());
 
  std::ofstream out;
  out.open("two_notes.wav", std::ios::out | std::ios::binary);
  exportToWav(out, allSamples);
  out.close();
 
  return 0;
}

Alright, now we’re ready to build our organ. I mentioned before that the Hammond Organ makes sound by combining sine waves which are turned on and off by drawbars. Each of these sine waves are a harmonic of the fundamental frequency (btw, the fundamental frequency is the key you’re pressing). Luckily for us, harmonics are quite easy to figure out because they are simply integer multiples of the fundamental frequency. The wiki page for Hammond Organ says:

“the drawbar marked "16′" is an octave below, and the drawbars marked "4′", "2′" and "1′" are one, two and three octaves above, respectively. The other drawbars generate various other harmonics and subharmonics of the note.”

And we can get a full understanding of the various drawbars here:

drawbar_data.gif

This image shows the values for each drawbar when you’re hitting the C that matches up with the 8’ drawbar. Let’s write a function to calculate these values. We’ll first create a struct to model the current settings of the drawbar. The hammond organ had 8 amplitude settings per drawbar, so we’ll emulate that:

struct DrawbarSettings {
  int d16;
  int d513;
  int d8;
  int d4;
  int d223;
  int d2;
  int d135;
  int d113;
  int d1;
};
 
std::vector<double> organ(double freq, double duration, double fade,
                          const DrawbarSettings& drawbarSettings) {
  double drawbar16 = freq / 2.0;  // one octave below
  // the 5 1/3 drawbar, which is a "sub-third"
  double drawbar5AndOneThird = drawbar16 * 3.0;
  double drawbar8 = freq;
  double drawbar4 = freq * 2.0;  // one octave above
  double drawbar2AndTwoThirds = freq * 3.0;
  double drawbar2 = freq * 4.0;  // two octaves
  double drawbar1AndThreeFifths = freq * 5.0;
  double drawbar1AndOneThird = freq * 6.0;
  double drawbar1 = freq * 8.0;  // three octaves
 
  double fadeStart = (duration - fade);
 
  std::vector<double> samples;
  for (double t = 0; t < duration; t += sampleStep) {
    double val = 0;
    val += sin(t * drawbar16 * twoPi) * drawbarSettings.d16 / 8.0;
    val += sin(t * drawbar5AndOneThird * twoPi) * drawbarSettings.d513 / 8.0;
    val += sin(t * drawbar8 * twoPi) * drawbarSettings.d8 / 8.0;
    val += sin(t * drawbar4 * twoPi) * drawbarSettings.d4 / 8.0;
    val += sin(t * drawbar2AndTwoThirds * twoPi) * drawbarSettings.d223 / 8.0;
    val += sin(t * drawbar2 * twoPi) * drawbarSettings.d2 / 8.0;
    val += sin(t * drawbar1AndThreeFifths * twoPi) * drawbarSettings.d135 / 8.0;
    val += sin(t * drawbar1AndOneThird * twoPi) * drawbarSettings.d113 / 8.0;
    val += sin(t * drawbar1 * twoPi) * drawbarSettings.d1 / 8.0;
    if (t < fade) {
      val *= t / fade;
    } else if (t > fadeStart) {
      val *= (1.0 - (t - fadeStart) / fade);
    }
    samples.push_back(val);
  }
  normalize(samples);
  return samples;
}

You see all the harmonic calculation is pretty simple. There’s one odd exception, which is the 5 ⅓ drawbar. It’s the third harmonic of the 16 drawbar. This will also be a perfect fifth of the fundamental frequency, which is generally a nice tone to throw into the mix.

Since we’re just adding these values together, one thing we need to make sure is to normalize our wave before we export it to wav, otherwise we’ll get overflow and our result will sound like futuristic garbage. Here’s a normalization function:

void normalize(std::vector<double>& values) {
  double max = values.at(0);
  for (auto& s : values) {
    if (abs(s) > max) {
      max = abs(s);
    }
  }
  for (size_t i = 0; i < values.size(); i++) {
    values[i] = values.at(i) / max;
  }
}

Alright, so let’s try that out:

int main(int argc, char* argv[]) {
  std::vector<double> allSamples;
  DrawbarSettings drawbarSettings1 = {0, 8, 4, 0, 0, 0, 0, 4, 0};
  std::vector<double> a4_440 = organ(440.0, 1.0, .05, drawbarSettings1);
  allSamples.insert(allSamples.end(), a4_440.begin(), a4_440.end());
  DrawbarSettings drawbarSettings2 = {8, 8, 8, 0, 0, 0, 0, 0, 0};
  std::vector<double> c4 = organ(261.63, 1.0, .05, drawbarSettings2);
  allSamples.insert(allSamples.end(), c4.begin(), c4.end());
 
  std::ofstream out;
  out.open("organ_notes.wav", std::ios::out | std::ios::binary);
  exportToWav(out, allSamples);
  out.close();
 
  return 0;
}

Ahh. As Shakespeare would say “let the sounds of music creep in our ears!”

We can also check the waveform for an 888000000 drawbar configuration:

our_organ.png

Awesome, looks like a match.

Okay, now try this on for size:

std::vector<double> justALittleDitty() {
  std::vector<double> allSamples;
  DrawbarSettings drawbarSettings = {8, 7, 8, 7, 7, 7, 7, 7, 7};
  std::vector<double> note = organ(1760, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1567.98, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1760, 1.5, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  for (double t = 0; t < .5; t += sampleStep) {
    allSamples.push_back(0);
  }
  note = organ(1567.98, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1396.91, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1318.51, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1174.66, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1108.73, 1.1, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(1174.66, 2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  for (double t = 0; t < .5; t += sampleStep) {
    allSamples.push_back(0);
  }
  note = organ(880, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(783.991, 0.2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(880, 1.5, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  for (double t = 0; t < .5; t += sampleStep) {
    allSamples.push_back(0);
  }
  note = organ(659.255, 0.6, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(698.456, 0.6, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(554.365, 0.6, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  note = organ(587.33, 2, .01, drawbarSettings);
  allSamples.insert(allSamples.end(), note.begin(), note.end());
  return allSamples;
}

Chilling, right?

I’ll end this article on these notes of raw terror, but I hope you had fun doing a bit of recreational programming with me!

Jon Bedard2 Comments