DPCM playback rate does really correlate with note freqs

Discuss NSF files, FamiTracker, MML tools, or anything else related to NES music.

Moderator: Moderators

bucky o'hare
Posts: 160
Joined: Fri Sep 22, 2006 9:52 am
Location: philly

Post by bucky o'hare »

Sorry for the bump, but there's good discussion here and I'd rather not clutter up the board with a closely-related topic. :)

I was wondering if someone can answer this question:
I'm curious about the gaps in the scale when downtuning. Know of any reasons it was designed that way?

How would you explain to a layperson what a "timer system" is, and why there are larger gaps up at the higher pitches? Is there a reason it starts off with a major scale and then eventually starts taking bigger leaps?
User avatar
rainwarrior
Posts: 8758
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Post by rainwarrior »

Well, there are some decisions in the 2A03 design that are extremely arbitrary. Take a look at the possible length counter values, for instance: http://wiki.nesdev.com/w/index.php/APU_Length_Counter.

I don't think there's really a good explanation for the DPCM frequencies. I think they're just frequencies somebody at Nintendo thought might be useful.
tepples
Posts: 22819
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples »

My wild guess about the larger gaps at the higher pitches is that there is less precision in the sample periods once it goes that high.
User avatar
Jarhmander
Formerly ~J-@D!~
Posts: 570
Joined: Sun Mar 12, 2006 12:36 am
Location: Rive nord de Montréal

Post by Jarhmander »

Tepples' totally right.

Here my GNU Octave code (can work in Matlab with few modifications):

Code: Select all

0; % if you want to put in a m file
function hz = midi2freq(note)
  hz = 440 * 2 .^((note - 69) / 12);
end

nesfreq = 1.7897725e6;
dmcperiods = [0x36; 0x48; 0x54; 0x6A; 0x80; 0x8E; 0xA0; 0xBE; 0xD6; 0xE2; 0xFE; 0x11E; 0x140; 0x154; 0x17C; 0x1AC];

notes = [12*12; 11*12+7; 11*12+4; 11*12; 10*12+9; 10*12+7; 10*12+5; 10*12+2; 10*12; 9*12+11; 9*12+9; 9*12+7; 9*12+5; 9*12+4; 9*12+2; 9*12];

most_tuned_dmcperiods = round(nesfreq ./ midi2freq(notes));

fprintf("difference between original DMC periods and the most tuned possible DMC periods:\n")

disp( most_tuned_dmcperiods - dmcperiods)
Output:

Code: Select all

difference between original DMC periods and the most tuned possible DMC periods:
  -1
  -1
   1
   1
  -1
   1
   0
   0
   0
   0
   0
  -1
   0
  -1
   1
   0
As you can see, they didn't get the most tuned possible values, but they're very close to it. And if you change the 'round' by 'floor' (simple truncation) it outputs:

Code: Select all

difference between original DMC periods and the most tuned possible DMC periods:
  -1
  -1
   0
   0
  -1
   0
   0
   0
  -1
   0
   0
  -1
   0
  -1
   0
  -1
which shows that it is even closer to that algorithm.
tepples
Posts: 22819
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples »

Notice that all the values in dmcperiods are even. This means the counter is probably triggered by the APU clock, which is half the CPU clock. (Quietust traced the circuit, and it turned out that pulse, noise, and DMC period dividers are clocked by the APU clock.) So what happens when you round most_tuned_dmcperiods to the nearest even period? I don't know Octave, so I have no idea whether the following is working syntax:

Code: Select all

most_tuned_dmcperiods = 2 * round(nesfreq ./ (2 * midi2freq(notes))); 
And there's an exact formula for nesfreq: 315000000/176
User avatar
Jarhmander
Formerly ~J-@D!~
Posts: 570
Joined: Sun Mar 12, 2006 12:36 am
Location: Rive nord de Montréal

Post by Jarhmander »

You know Tepples, Octave is really like Matlab, with just less restrictions on the language and of course, Octave is not backed with powerful toolboxes; nevertheless, you should apt-get installthat, and also some useful packages like octave-control and octave-signal.

And yeah, you're right again:

Code: Select all

0; % if you want to put in a m file
function hz = midi2freq(note) 
  hz = 440 * 2 .^((note - 69) / 12); 
end 

nesfreq = 315000000 / 176; 
dmcperiods = [0x36; 0x48; 0x54; 0x6A; 0x80; 0x8E; 0xA0; 0xBE; 0xD6; 0xE2; 0xFE; 0x11E; 0x140; 0x154; 0x17C; 0x1AC]; 

notes = [12*12; 11*12+7; 11*12+4; 11*12; 10*12+9; 10*12+7; 10*12+5; 10*12+2; 10*12; 9*12+11; 9*12+9; 9*12+7; 9*12+5; 9*12+4; 9*12+2; 9*12]; 

most_tuned_dmcperiods = 2 * round(nesfreq ./ (2 * midi2freq(notes))); 

fprintf("difference between original DMC periods and the most tuned possible DMC periods:\n") 

disp( most_tuned_dmcperiods - dmcperiods) 
Output:

Code: Select all

difference between original DMC periods and the most tuned possible DMC periods:
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
   0
So now it's apparent that it really correlate with notes, no doubts. That wasn't random at all.
User avatar
rainwarrior
Posts: 8758
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Post by rainwarrior »

So what about the "gaps" after the first several chromatic pitches? How many cents off are they? (Would the best case for the skipped pitches be really bad?)
User avatar
Jarhmander
Formerly ~J-@D!~
Posts: 570
Joined: Sun Mar 12, 2006 12:36 am
Location: Rive nord de Montréal

Post by Jarhmander »

Data is posted on the (old) 2A03.org forums, but since it has proved instable, I'll post the results here, and recalc them to be sure, posting at the same time my GNU Octave code to get that data, so anyone can verify how the result was obtained.

Code: Select all

0; % if you want to put in a m file

function hz = midi2freq(note) 
  hz = 440 * 2 .^((note - 69) / 12); 
end 

% an helper
function notestruct = make_notestruct(octave, scale, cents)
  notestruct = struct("octave", octave, "scale", scale, "cents", cents);
end

% note that it can output an invalid MIDI notes (values outside 0~127)
% as well as non integer values.
function midinote = freq2midi(freq)
  midinote = log2(freq ./ 440) .* 12 + 69;
end

function notestruct = midi2notestruct(midinote)
  closestnote = round(midinote);
  oct_offs    = floor(closestnote/12);
  closestnote = closestnote - oct_offs*12;
  octave      = oct_offs - 1;
  scale       = closestnote;
  cents       = (midinote - oct_offs*12 - scale) * 100;

  notestruct = make_notestruct(octave, scale, cents);
end

function str = scale2disp(scale)
   scaledisp = ["C";"C#";"D";"D#";"E";"F";"F#";"G";"G#";"A";"A#";"B"];
   str = scaledisp(scale+1);
end

nesfreq = 315000000 / 176; 
dmcperiods = [0x36; 0x48; 0x54; 0x6A; 0x80; 0x8E; 0xA0; 0xBE; 0xD6; 0xE2; 0xFE; 0x11E; 0x140; 0x154; 0x17C; 0x1AC]; 

dmcfreqs = nesfreq ./ dmcperiods;

dmcmidi = freq2midi(dmcfreqs);

dmcnotes = midi2notestruct(dmcmidi);

fprintf("DMC period  octave     scale   cents\n");
fprintf("-----------------------------------------\n");
for i=1:length(dmcperiods)
  fprintf("0x%03X       %2d        %2s       %+2.6g\n", dmcperiods(i), ...
                                                        dmcnotes.octave(i), ...
                                                        scale2disp(dmcnotes.scale)(i), ...
                                                        dmcnotes.cents(i));
end
Results:

Code: Select all

DMC period  octave     scale   cents
-----------------------------------------
0x036       11         C       -17.8827
0x048       10         G       -15.9277
0x054       10         E       +17.2014
0x06A       10         C       +14.4778
0x080        9         A       -12.0177
0x08E        9         G       +8.28576
0x0A0        9         F       +1.66859
0x0BE        9         D       +4.15558
0x0D6        9         C       -1.77808
0x0E2        8         B       +3.76755
0x0FE        8         A       +1.56068
0x11E        8         G       -3.8633
0x140        8         F       +1.66859
0x154        8         E       -3.28682
0x17C        8         D       +4.15558
0x1AC        8         C       -1.77808
User avatar
rainwarrior
Posts: 8758
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Post by rainwarrior »

I don't have octave, but I wrote a similar python program to answer the question I was asking. What I wanted to know was what are the best-fit values that could have been used for any given note.

http://rainwarrior.ca/projects/nes/dpcm_freqs.py

Code: Select all

DMC   Octave   Note   Cents        
-----------------------------------
0x002 16       A      -12.01769334
0x004 15       A      -12.01769334
0x006 15       D      -13.97269421
0x008 14       A      -12.01769334
0x00A 14       F      +1.66859279
0x00C 14       D      -13.97269421
0x00E 13       B      +19.15640019
0x010 13       A      -12.01769334
0x012 13       G      -15.92769507
0x014 13       F      +1.66859279
0x016 13       D#     +36.66436429
0x018 13       D      -13.97269421
0x01A 13       C      +47.45464489
0x01C 12       B      +19.15640019
0x01E 12       A#     -0.28640807
0x020 12       A      -12.01769334
0x022 12       G#     -16.97310284
0x024 12       G      -15.92769507
0x026 12       F#     -9.53070947
0x028 12       F      +1.66859279
0x02A 12       E      +17.20139932
0x02C 12       D#     +36.66436429
0x030 12       D      -13.97269421
0x032 12       C#     +15.35487893
0x036 12       C      -17.88269594   *****
0x038 11       B      +19.15640019
0x03C 11       A#     -0.28640807
0x040 11       A      -12.01769334
0x044 11       G#     -16.97310284
0x048 11       G      -15.92769507   *****
0x04C 11       F#     -9.53070947
0x050 11       F      +1.66859279
0x054 11       E      +17.20139932   *****
0x05A 11       D#     -2.24140894
0x060 11       D      -13.97269421
0x064 11       C#     +15.35487893
0x06A 11       C      +14.47776118   *****
0x072 10       B      -11.48571034
0x078 10       A#     -0.28640807
0x080 10       A      -12.01769334   *****
0x086 10       G#     +8.67527811
0x08E 10       G      +8.28576325    *****
0x098 10       F#     -9.53070947
0x0A0 10       F      +1.66859279    *****
0x0AA 10       E      -3.28681671
0x0B4 10       D#     -2.24140894
0x0BE 10       D      +4.15557666    *****
0x0CA 10       C#     -1.87147264
0x0D6 10       C      -1.77807702    *****
0x0E2 9        B      +3.76755176    *****
0x0F0 9        A#     -0.28640807
0x0FE 9        A      +1.56068253    *****
0x10E 9        G#     -4.19640980
0x11E 9        G      -3.86329747    *****
0x12E 9        F#     +1.89661947
0x140 9        F      +1.66859279    *****
0x154 9        E      -3.28681671    *****
0x168 9        D#     -2.24140894
0x17C 9        D      +4.15557666    *****
0x194 9        C#     -1.87147264
0x1AC 9        C      -1.77807702    *****
0x1C4 8        B      +3.76755176
0x1E0 8        A#     -0.28640807
0x1FC 8        A      +1.56068253
-----------------------------------
Does it shed any light on why they picked the particular notes they did? I dunno, by this the choices still look kinda arbitrary to me.
lidnariq
Site Admin
Posts: 11606
Joined: Sun Apr 13, 2008 11:12 am

Post by lidnariq »

Musically the note choice makes a fair amount of sense-

The entire Ionian octave at the bottom.
Most of the next octave, missing the 3rd and 7th scale degrees
The major triad of the next octave up
The root scale degree at top.

Because any desired detune and transposition can be encoded when the sample is made, this allows any song to play notes over most of two octaves in Ionian or Dorian modes.

Is it be as good as a fully programmable divisor as on the triangle and pulse wave channels? Obviously not. Would other decisions that only used 4 bits for specifying period make more sense? Yes; I think including one octave of a full chromatic scale at plus a few higher notes would probably have been a better design. But I think this is far more explicable than the restrictions on noise frequency or the duplicate pulse duty (1/4 and 3/4 have the same sound).
User avatar
rainwarrior
Posts: 8758
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Post by rainwarrior »

You can build large samples at any frequency, at whichever samplerate from the table is good for you, but interestingly it doesn't seem to be based around doing that kind of thing. In that sort of situation it would be more useful to keep your samplerates in tune with each other, rather than trying to keep them in tune with an arbitrary standard like A440 (which is clearly in use here). Note that only the lowest two Cs are exactly an octave apart; the rest are jittered up and down trying to match to A440 rather than matching each other.

It's not particularly good for short looped samples either, since the sample length is 16n+1 bytes. A 1 byte sample would be too quiet to be useful. If it was an even 16, with 16 bytes you could make a decent DPCM triangle 3-7 octaves down from the frequencies mentioned, and play it all over that major scale. However, it's 17 bytes, which detunes it by 16/17... which kind of makes the scale mostly pointless (unless you want to tune your square/triangle at A=414hz). Kinda seems like it's not intended for that either. The only place I've seen this tried was in Rob Hubbard's The Immortal, which used a 17 byte bass, but doesn't really bother to compensate for the 16/17 detune, just kinda uses it sparingly, and accompanies it with vibrato generally. 16/17 is most of a semitone off, so it's still close enough to be usable. (So for example, in track 2 he uses DPCM pitch 2, an E frequency, to play a tone close enough to E-flat to be acceptable.)

Honestly I think they're rather poorly chosen for any purpose. Sunsoft tried to make do with them, and they do an okay job, but you'll notice their basslines jump in octaves all the time and they bottom and top of the octave really never sound in tune with each other. They just live with the approximation. Note that they for the most part don't use the low samplerates where that pre-fab major scale resides, because they'd sound too muddy at such a low samplerate anyway. They're forced to use the more poorly tuned higher ones, and do the tuning via multiple samples.


If you ask what my guess is, the designer originally presumed sample lengths wouldn't have the +1, and designed the scale for short looped samples in an A440 tuning. After the first octave, just chose arbitrary pitches; F and G are good because they're in the middle but also an important scale note, I guess trying to space out the higher frequencies so there are more to work with? Other ones are just haphazardly chosen, in my opinion. Maybe they were chosen early on, intended for a different purpose, and then as a feature it was de-emphasized later on in the design process and it didn't ever get a rework.
lidnariq
Site Admin
Posts: 11606
Joined: Sun Apr 13, 2008 11:12 am

Post by lidnariq »

Sure; they evidently knew just enough to make decisions for the audio hardware that are correct at first glimpse and inadequate when a person who knows more about the situation looks at it. See also: the looped noise mode's interactions with the restrictions on noise rate.
tepples
Posts: 22819
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples »

At least on the Game Boy, looped noise makes acceptable C, D, F, and G# in each octave, which allows for passable covers of the Super Mario Bros. 2 and the end of "Wish" by Nine Inch Nails (which uses a D, C, G#, F descending scale).
User avatar
rainwarrior
Posts: 8758
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Post by rainwarrior »

On a whim I decided I would try to make an in-tune 17-byte looped DPCM sample. Or, well, at least as in tune as is possible on the NES.

http://rainwarrior.ca/projects/nes/lately_tuned.nsf

So, this is a short tune with a 17-byte looped DPCM sample. It makes sure to use all 16 possible pitches. The pitch tables used for the other channels have been custom built with A-flat = 440hz * 16 / 17, to compensate for the detune due to sample length.

As you can hear, for the high notes especially, the DPCM really doesn't have a lot of resolution to get pitches accurate anyway. I'm bent over as far as I can to try and sound in-tune with the DPCM's chosen scale, and even with this the results aren't particularly magical.

The NES has never really been an instrument for precision tuning anyway, I suppose, but if I did this correctly, this is a best-case example for DPCM tuning.


Info about how this NSF was constructed here: http://famitracker.com/forum/posts.php?id=3424
Last edited by rainwarrior on Tue Oct 06, 2020 4:02 pm, edited 1 time in total.
User avatar
rainwarrior
Posts: 8758
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Post by rainwarrior »

Just to point out how bad this tuning scheme actually is, check out the four Cs:

Code: Select all

0x036 12       C      -17.88269594
0x06A 11       C      +14.47776118
0x0D6 10       C      -1.77807702
0x1AC 9        C      -1.77807702
Note that 0x036 and 0x06A are a total of 31 cents apart, because one rounds up and the other down! This is almost comic. You cannot use the DPCM's pitches to play an in-tune octave, except between the lowest two (which are the least likely to be used since the samplerate is so low down there).

It would have been much better to just forget about trying to hit the A440 scale and make these power of 2 multiples of each other. Sunsoft games could have had much nicer tuning in their basslines.


Also, Tepples, I'd be interested in hearing that GB Wish cover, if you have a link.
Post Reply