Friday, March 13, 2015

Chiptunes

A while back, I wrote a series of posts on some of the graphical techniques used by early 90's video game consoles. For an encore, I have decided to write about the audio techniques of said consoles. I will specifically talk about the NES, as I have studied in detail its audio processing unit.

I will start this series by saying that I love the music from the NES era. Some of it is very primitive, like the underground theme from Super Mario Bros., while some of it is actually very intricate, like the clockwork theme from Castlevania III. Of course, the softest spot in my cold, hard heart is for the intro music for Snake Rattle 'n' Roll.

The impressive part of all three of those tracks, from a technical perspective, is that they are all playing on the exact same (very limited) hardware. The audio subsystem of the NES's 2a03 processor had only five channels: two pulse channels, one triangle channel, one noise channel, and one (rarely used) pulse-code modulated channel. These five (four) channels made up all of the sound in an NES game, which is the reason why, when sound effects were also playing, parts of the background music would go silent.

Over the next few posts, I will attempt to reproduce the NES's classic sound on a computer, which will require a fair bit of digression into some pretty advanced math in order to get it to sound just right.

Unlike my series on the NES graphics, however, you can't really appreciate the sounds in this series unless you can hear them. I will try to upload some samples, but it works best if you program it on your own machine, so that you can also tinker with the program and hear the result.

To start us off, we first just need to be able to synthesize an audio signal, and play it to the computer speakers. The most primitive way I have found to do that is to just dump a digitized signal into a .wav file. This is pretty simple to do, using the following helper method:

using System.IO;
using System.Text;

public static class WaveFileFormatExtension
{
 // --- This class only supports one format: 44.1 kHz, 16-bit, mono PCM ---
 const int SAMPLE_RATE = 44100;
 const int BITS_PER_SAMPLE = 16;
 const int BYTES_PER_SAMPLE = BITS_PER_SAMPLE / 8;
 const int NUM_CHANNELS = 1;
 const int PCM_FORMAT = 1;

 public static void WriteWaveFile(this Stream destination, short[] data)
 {
  var writer = new BinaryWriter(destination);

  // --- RIFF chunk ---

  writer.Write(Encoding.ASCII.GetBytes("RIFF"));
  writer.Write((uint)(36 + (data.Length * BYTES_PER_SAMPLE * NUM_CHANNELS)));
  writer.Write(Encoding.ASCII.GetBytes("WAVE"));


  // --- fmt chunk ---

  writer.Write(Encoding.ASCII.GetBytes("fmt "));
  writer.Write((uint)16);

  writer.Write((short)PCM_FORMAT);                                    // wFormatTag
  writer.Write((short)NUM_CHANNELS);                                  // nChannels
  writer.Write((int)SAMPLE_RATE);                                     // nSamplesPerSec
  writer.Write((int)SAMPLE_RATE * BYTES_PER_SAMPLE * NUM_CHANNELS);   // nAvgBytesPerSec
  writer.Write((short)(BYTES_PER_SAMPLE * NUM_CHANNELS));             // nBlockAlign
  writer.Write((short)BITS_PER_SAMPLE);                               // wBitsPerSample


  // --- data chunk ---

  writer.Write(Encoding.ASCII.GetBytes("data"));
  writer.Write((uint)(data.Length * BYTES_PER_SAMPLE * NUM_CHANNELS));

  foreach (var datum in data)
   writer.Write(datum);
 }
}

The method above will produce a wave file with one 16-bit channel with a frequency of 44.1 kHz. I will leave it as an exercise to the reader if you want to modify it to support other channel counts, bit depths or frequencies. This detailed specification of the wave file format will be of some assistance in this regard.

Using this helper method, you can write a quick and dirty application that will play one second worth of simple audio and then close:

using System.IO;
using System.Linq;
using System.Media;

public static class Program
{
 public static void Main()
 {
  var buffer = new MemoryStream();
  buffer.WriteWaveFile(SynthesizeSquareWave(44100));
  buffer.Seek(0, SeekOrigin.Begin);

  var player = new SoundPlayer(buffer);
  player.Load();
  player.PlaySync();
 }

 public static short[] SynthesizeSquareWave(int numSamples)
 {
  var result = new short[numSamples];

  foreach (var i in Enumerable.Range(0, numSamples))
   result[i] = (short)((i % 440) >= 220 ? -4096:4095);

  return result;
 }
}

If you run this program, it should sound a little something like this.

No comments:

Post a Comment