for my first game, I used the table found at https://wiki.nesdev.com/w/index.php/APU_period_table. To my ear though some of the intervals in it seem really out of tune. I wondered if I could create a pitch table that is more in tune with itself.
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.
what this program does is try to find a pitch table where the rounded integer values are as close as possible to their floating point counterparts. It generates different series of pitches based on a wide range of starting notes and measures how closely that series overall matches its expected value.
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:
It's pretty close to the A440 tuning, but it's a little flatter. I think it sounds a lot more pleasing, but I only tried it out in the one game I've written, which only has one song (in F major). Out of curiosity, I tried to replicate the A440 tuning table from the wiki with my program , and I couldn't. no matter what values I used, some notes ended up sharper and some ended up flatter, with a variance of over 7 (which was pretty high). I think that this system produces a much more internally consistent set of pitch values for the NES.
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.
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:
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();
}
}
}
}
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();
}
}
}
}
what this program does is try to find a pitch table where the rounded integer values are as close as possible to their floating point counterparts. It generates different series of pitches based on a wide range of starting notes and measures how closely that series overall matches its expected value.
Code:
rawPitch = lowestPitch / Math.Pow(2, interval / (double)12);
This line is the formula for equal tuning. given a starting pitch, you can get any higher pitch by changing the interval value. You could also get lower pitches by changing it from divide to multiply, but I decided I always wanted to start from the lowest pitch because that's where the NES APU is most accurate.Code:
variance += Math.Pow(rawPitch - roundedPitch, 2);
Variance is a measurement of how well a data set fits its expected value. find the difference between each floating point value calculated by the above formula, and the rounded integer value that the NES will actually use. We square that difference because we want all the numbers to be positive, and also squaring adds weight to larger difference values. The sum of all of that is the variance for the data set. The smaller the variance, the better the data set fits the expected values. Or in other words, the closer the notes on the NES are to the frequencies they are supposed to be.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:
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
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
It's pretty close to the A440 tuning, but it's a little flatter. I think it sounds a lot more pleasing, but I only tried it out in the one game I've written, which only has one song (in F major). Out of curiosity, I tried to replicate the A440 tuning table from the wiki with my program , and I couldn't. no matter what values I used, some notes ended up sharper and some ended up flatter, with a variance of over 7 (which was pretty high). I think that this system produces a much more internally consistent set of pitch values for the NES.
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.