Some things I considered:
1. I don't care if it's based on A440. I'm more concerned with how well all the pitches are in tune with each other overall.
2. Understanding that any tuning system on the NES is going to be a compromise, especially on the higher notes where there is a lower degree of precision.
3. The possibility of trying to use "stretched octaves" to better emulate the way real instruments are tuned.
Considering how pitches are represented on the NES hardware, I thought that the series of values that are most consistently in tune with each other would probably not conform to A440 tuning. Rather than worry about real-world frequencies, I decided to try to work with the hardware's limitations. I already knew that bit shifting the pitch value left or right would change the note by one octave down or up (by doubling or halving the period). I considered the possibility of trying to create a pitch system based on simple ratios (for the non-music nerds, pleasing intervals like octaves, fifths, and major thirds are based on simple integer ratios of frequencies like 1/2, 2/3, 5/3, etc...). This is called Just Intonation, and it sounds great, but for lots of reasons I won't go into here, implementing this kind of pitch system on the NES would be virtually impossible without having a different pitch table for each key (and possibly each chord change). That's not something I want to do. The limitations of the NES hardware would also tend to detune every interval anyway.
So I settled on an Equal Tempered system instead (that's a system in which all the frequencies are an equal distance apart on a logarithmic scale). I tried to work out what values each pitch in my table should have by hand with pen and paper at first. I was still kind of hanging onto the idea that the values should have some kind of nice repeating pattern. Again, I gave up on that. I thought "there's got to be an easier way". So I wrote a C# program to help me.
Code: Select all
using System;
namespace NESPitchTableGenerator
{
class Program
{
static void Main(string[] args)
{
double startingPitch = 2048;
double endingPitch = 2032;
double increment = 0.01;
int numberOfOctaves = 7;
double variance = 0;
double rawPitch;
int roundedPitch;
double bestVariance = double.PositiveInfinity;
double bestLowestPitch = startingPitch;
for (double lowestPitch = startingPitch; lowestPitch >= endingPitch; lowestPitch -= increment)
{
variance = 0;
for (int octave = 0; octave < numberOfOctaves; octave++)
{
for (int note = 0; note < 12; note++)
{
int interval = octave * 12 + note;
rawPitch = lowestPitch / Math.Pow(2, interval / (double)12);
roundedPitch = (int)Math.Round(rawPitch);
variance += Math.Pow(rawPitch - roundedPitch, 2);
}
}
Console.WriteLine(lowestPitch + " :\t" + variance);
if (variance < bestVariance)
{
bestVariance = variance;
bestLowestPitch = lowestPitch;
}
}
Console.WriteLine("best variance: " + bestVariance);
Console.WriteLine("best starting pitch: " + bestLowestPitch);
for (int octave = 0; octave < numberOfOctaves; octave++)
{
Console.Write("\t.dw ");
for (int note = 0; note < 12; note++)
{
int interval = octave * 12 + note;
rawPitch = bestLowestPitch / Math.Pow(2, interval / (double)12);
roundedPitch = (int)Math.Round(rawPitch);
Console.Write("$" + roundedPitch.ToString("X4"));
//Console.Write("%" + Convert.ToString(roundedPitch, 2));
if (note != 11)
Console.Write(", ");
}
Console.WriteLine();
}
}
}
}
Code: Select all
rawPitch = lowestPitch / Math.Pow(2, interval / (double)12);
Code: Select all
variance += Math.Pow(rawPitch - roundedPitch, 2);
I had to play around with the program a little to get results that I was happy with. First, I decided to limit my starting pitches to somewhat larger values because I wanted to get the greatest range possible. I also did this to limit how high notes could get to increase their accuracy.
In some tests, I also changed the number of octaves to exclude the highest notes. Up that high, the NES can't even approximate decent tuning, and I use those notes very sparingly anyway. Their difference values would be all over the place, and I didn't want that to skew the overall variance.
I also tried a Floor function rather than Round to get the integer values. This tends to push the notes sharp (my attempt at the 'stretched octaves' mentioned above). Some intervals really work well this way and others...not so much. (stretching octaves is really only an issue on acoustic instruments anyway. a synth doesn't have any reason to need it.)
This was the final result of my testing:
Code: Select all
best variance: 5.61272542025088
best starting pitch: 2038.23000000001
.dw $07F6, $0784, $0718, $06B2, $0652, $05F7, $05A1, $0550, $0504, $04BC, $0478, $0438
.dw $03FB, $03C2, $038C, $0359, $0329, $02FB, $02D1, $02A8, $0282, $025E, $023C, $021C
.dw $01FE, $01E1, $01C6, $01AC, $0194, $017E, $0168, $0154, $0141, $012F, $011E, $010E
.dw $00FF, $00F0, $00E3, $00D6, $00CA, $00BF, $00B4, $00AA, $00A1, $0097, $008F, $0087
.dw $007F, $0078, $0071, $006B, $0065, $005F, $005A, $0055, $0050, $004C, $0047, $0043
.dw $0040, $003C, $0039, $0036, $0033, $0030, $002D, $002B, $0028, $0026, $0024, $0022
.dw $0020, $001E, $001C, $001B, $0019, $0018, $0017, $0015, $0014, $0013, $0012, $0011
I'm really curious about everybody's opinion on this. Are there any flaws in the methodology? I'm proud of this work, and I'm definitely going to use this table in my future projects.