The Well-Tempered NES

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
The Well-Tempered NES
by on (#232846)
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.

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();
            }
        }
    }
}


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


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.
Re: The Well-Tempered NES
by on (#232850)
I don't the tuning terrible or anything, but i think i'm percieving that duad harmonies have different flavours when played in different keys. On some subtle level that informs (and limits) how i compose for the APU.

Some small things to maybe consider:
-do you get further or closer to the crackling A note?
-how is it faring in relation to the DPCM channel?
Re: The Well-Tempered NES
by on (#232851)
gravelstudios wrote:
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.
The VIC-20 only has a 7-bit divisor so you're basically always constrained to the topmost octave out of the NES. At some point I wanted to figure out what the optimal tuning was there. To do this, I calculated whether every possible pair of divisors were in tune (I think I used a threshold of 6 cents), and asked graphviz to draw a graph of this. Specifically I used graphviz's "fdp" to generate clusters showing what things are in tune.

By far, the largest cluster of in-tune notes is the one that includes the ÷48 pitch. On the NTSC VIC-20, this is 17 cents sharp¹ relative to A440, approximately A444. On the NTSC NES, this is 14 cents flat² relative to A440, or approximately A436.


¹: NTSC VIC-20 pitch equation: 45MHz÷44÷16÷{4,8,16}÷divisor. When divisor=48, that's 333Hz or some octaves lower, the almost-E that's 717 cents above A3=A220.
²: NTSC NES pitch equation: 39375000Hz÷22÷16÷divisor. When divisor=48, that's 2330Hz, the almost-D that's 714 cents below A7=A3520
Re: The Well-Tempered NES
by on (#232852)
gravelstudios wrote:
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.

The relationship of pitch values for the NES tables have an inherent +1 on the period. You can't just divide by an interval and round to the nearest whole number to produce that interval, you need a +1 before, and a -1 after.

Also I think you're only calculating variation for one octave, but the variation will affect each octave differently, especially the higher ones.

This is actually also why just intonation is a real pain. You can't just multiply by 2 to drop an octave. Here's a slightly more verbose note about this, and an NSF example, but in general it's not even possible to construct much of a reasonable JI scale even in a single key on the NES.

As for finding a base 12-TET tuning pitch that minimizes error, I guess you could do that, but however you slice it the error is going to be unevenly distributed. You could minimize overall chromatic error, but that's going to favour some keys or octaves differently than others... you might be missing another tuning that scored slightly worse but might sound slightly better for the specific music you're going to play with it.

A440 probably isn't the "best" but I doubt any other tuning would be a significant general improvement, only specific notes will be better (esp. in the high octaves) and that really must depend on which of those notes you actually want to use in your music.


I don't remember if I've directly verified the current table on the wiki, but I've seen the exact same a440 tables in a bunch of games and music engines, usually rounding down, but sometimes (better) rounding to nearest. The "sometimes" means mostly only in newer homebrew engines. The difference is pretty subtle, something I could measure with some audible methods, but I don't think I could ever notice just by listening to a tune.
Re: The Well-Tempered NES
by on (#232854)
Quote:
The relationship of pitch values for the NES tables have an inherent +1 on the period. You can't just divide by an interval and round to the nearest whole number to produce that interval, you need a +1 before, and a -1 after.


I didn't realize this detail. I may look into how I could accommodate this.
Re: The Well-Tempered NES
by on (#232855)
You don't need to do anything special... just subtract one from the calculated value for the value for your lookup table.
Re: The Well-Tempered NES
by on (#232875)
You're right, I just have to subtract 1. Thanks, that was simpler than I thought it would be. Here is the revised table:

Code:
        .dw $07F5, $0783, $0717, $06B1, $0651, $05F6, $05A0, $054F, $0503, $04BB, $0477, $0437
        .dw $03FA, $03C1, $038B, $0358, $0328, $02FA, $02D0, $02A7, $0281, $025D, $023B, $021B
        .dw $01FD, $01E0, $01C5, $01AB, $0193, $017D, $0167, $0153, $0140, $012E, $011D, $010D
        .dw $00FE, $00EF, $00E2, $00D5, $00C9, $00BE, $00B3, $00A9, $00A0, $0096, $008E, $0086
        .dw $007E, $0077, $0070, $006A, $0064, $005E, $0059, $0054, $004F, $004B, $0046, $0042
        .dw $003F, $003B, $0038, $0035, $0032, $002F, $002C, $002A, $0027, $0025, $0023, $0021
        .dw $001F, $001D, $001B, $001A, $0018, $0017, $0016, $0014, $0013, $0012, $0011, $0010


And as two separate tables for low and high bytes if that's what you prefer (which I do):

Code:
PitchTableLo:
        .db $F5, $83, $17, $B1, $51, $F6, $A0, $4F, $03, $BB, $77, $37
        .db $FA, $C1, $8B, $58, $28, $FA, $D0, $A7, $81, $5D, $3B, $1B
        .db $FD, $E0, $C5, $AB, $93, $7D, $67, $53, $40, $2E, $1D, $0D
        .db $FE, $EF, $E2, $D5, $C9, $BE, $B3, $A9, $A0, $96, $8E, $86
        .db $7E, $77, $70, $6A, $64, $5E, $59, $54, $4F, $4B, $46, $42
        .db $3F, $3B, $38, $35, $32, $2F, $2C, $2A, $27, $25, $23, $21
        .db $1F, $1D, $1B, $1A, $18, $17, $16, $14, $13, $12, $11, $10
      
PitchTableHi:
        .db $07, $07, $07, $06, $06, $05, $05, $05, $05, $04, $04, $04
        .db $03, $03, $03, $03, $03, $02, $02, $02, $02, $02, $02, $02
        .db $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01
        .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
        .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
        .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
        .db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00


I spent some time really listening to this table with ascending octaves, fifths, and major triads and compared it to the A440 table. It's just my subjective opinion, but I still do think this is better in tune (especially on the higher notes), but the difference if there is one is incredibly small. I don't think most people would notice or care. And no matter what you do, there are going to be some really bad intervals on the NES. It's just the limits of the hardware, but that's part of what gives the NES its characteristic tone, and that's what I like.

with the program modified to subtract 1 from each value, I went back to see if I could replicate the A440 table, and I actually got it very close. There were just a couple values that were off by 1 from what my program produced. it's variance was still higher than my tuning, so I think I can still say that my system is measurably better in tune (by a probably unnoticeably small amount). Does this really contribute anything significant to the world of NES audio? not really. I had fun though. Thanks everybody for taking the time to read this and for your feedback.
Re: The Well-Tempered NES
by on (#232882)
Our of curiosity, why did you only search the longest period from 2048 to 2032? That's only 14 cents, scarcely two Just-Noticeable Differences....
Re: The Well-Tempered NES
by on (#232895)
I actually searched a wider space than that, but those were just the values that happened to be in the program when I copied and pasted it.

When I ran the program initially I started testing between 3000 and 1000. there were a few data sets with slightly better variance, but they tended to start at a higher pitch than I wanted (<$7000). This surprised me because it shifted the entire set up, and higher notes tend to be less accurate than lower ones. I wanted the scale to begin as close to the bottom of the range as reasonably possible. Also, I think it doesn't really matter where exactly you begin the search as long as the range covers at least a half step, because if the increment is small enough, you should 'catch' every possible chromatic scale starting from that lowest pitch. the variance of most data sets when all octaves are included is around 6-7. I think anything <6 is pretty good, and the best ones tend to be around 5.5.

One thing that I noticed is that if you exclude the highest couple of octaves, things even out considerably. If you limit the search space to the lowest 4 octaves, just about any set of equal tempered values will sound the same. It's the really high notes where the largest variance values come from, because they are the most coarsely tuned. if those high notes are used sparingly, I don't think it matters much.

Quote:
Some small things to maybe consider:
-do you get further or closer to the crackling A note?
-how is it faring in relation to the DPCM channel?


I did not consider either of these, and they are good points. In the music I wrote for my previous game, I didn't use vibrato, or the DPCM channel at all, so I just didn't think about this. I've been learning more about DPCM lately, and can see the difficulty in tuning that to the rest of the channels. My understanding of the DPCM channel is that it very roughly follows a major scale. I'd assume that whatever pitch your sample is played at on value $0, you'd get a major scale in that key. Is that more or less true? If that's the case, I would have to make sure that my 'base' sample was in tune with all my other pitches at $0.
Re: The Well-Tempered NES
by on (#232897)
gravelstudios wrote:
My understanding of the DPCM channel is that it very roughly follows a major scale. I'd assume that whatever pitch your sample is played at on value $0, you'd get a major scale in that key. Is that more or less true? If that's the case, I would have to make sure that my 'base' sample was in tune with all my other pitches at $0.
The DPCM sample rates are tuned to A440, albeit not very precisely. But since the sample rate often isn't an integer multiple of the corresponding recorded audio pitch, it often doesn't matter.

Even short looped samples (e.g. my triangle pack) don't necessarily require that you stay on A440 tuning, because the length of the looped sample lets you change the DPCM sample rates by some integer divisor. But in practice, it remains easier if you do.
Re: The Well-Tempered NES
by on (#232898)
The DPCM pitches, along with how out of tune they are, are documented on the wiki:
http://wiki.nesdev.com/w/index.php/APU_DMC#Pitch_table

They're very dodgy, but yeah you can play some pitches that are close enough to an equal tempered scale, excepting the one really bad note ($4) on PAL machines.

I made a little tune a while back as an experiment. I shifted the pitch from A440 to something that would be as in-tune with the DPCM scale as I could. There's some more description here:
https://forums.nesdev.com/viewtopic.php?f=6&t=17441

The big thing that bothers me about the DPCM frequencies is how few of its octave relationships are in tune. Their tuning system was made by trying to round the sampling frequencies to an A440 scale, rather than considering their tuning relationships against each other. The theory is that they intended this to make it useful for the looped DPCM mode to play small looped samples as tones, but that failed because there's an accidental +1 on the length implementation that throws off its tuning by 17/16. Really bizarre.
Re: The Well-Tempered NES
by on (#232950)
I can see why DMC is usually used for percussion and other non-pitched sounds.