APU period table: Difference between revisions

From NESdev Wiki
Jump to navigationJump to search
(moved the program below the output because most people are looking for something to CNP; added link to Celius' tables with more explanation)
(Port script to Python 3 because Python 2 is no longer supported by Python Software Foundation)
 
(10 intermediate revisions by 4 users not shown)
Line 1: Line 1:
[[Category:APU]]
[[APU Pulse]] and [[APU Triangle]] use "period" values to set the pitch of the note.
[[APU Pulse]] and [[APU Triangle]] use "period" values to set the pitch of the note.
But some people might not know the [[wikipedia:piano key frequencies|piano key frequencies]] or how to convert them to periods for the NES.
But some people might not know the [[wikipedia:piano key frequencies|piano key frequencies]] or how to convert them to periods for the NES.
Line 4: Line 5:


== Lookup table ==
== Lookup table ==
Here's a lookup table from note numbers (low A = 0) to the values to write to the pulse and triangle period registers.
Here's a lookup table from note numbers to the values to write to the pulse and triangle period registers.
Triangle notes will sound one octave lower.
For the triangle channel, the first value corresponds to the lowest key on a standard piano (an A).
The pulse waves sound one octave higher.
<pre>
<pre>
; NTSC period table generated by mktables.py
; NTSC period table generated by mktables.py
Line 30: Line 32:
== Table generator ==
== Table generator ==
This Python program generated the above lookup table.
This Python program generated the above lookup table.
You can use it to make a table for PAL, or you can adjust it for other [[wikipedia:musical tuning]] systems:
You can use it to make a table for a PAL NES, which has a different CPU [[Cycle_reference_chart#Clock_rates|clock rate]].
<pre>
<pre>
#!/usr/bin/env python
#!/usr/bin/env python3
#
#
# Lookup table generator for note periods
# Lookup table generator for note periods
# Copyright 2010 Damian Yerrick
# Copyright 2010, 2020 Damian Yerrick
#
#
# Copying and distribution of this file, with or without
# Copying and distribution of this file, with or without
Line 42: Line 44:
# This file is offered as-is, without any warranty.
# This file is offered as-is, without any warranty.
#
#
from __future__ import with_statement, division
assert str is not bytes
import sys
import sys


ntscOctaveBase = 39375000.0/(22 * 16 * 55)
lowestFreq = 55.0
palOctaveBase = 266017125.0/(10 * 16 * 16 * 55)
ntscOctaveBase = 39375000.0/(22 * 16 * lowestFreq)
palOctaveBase = 266017125.0/(10 * 16 * 16 * lowestFreq)
maxNote = 80
maxNote = 80


Line 53: Line 56:
     octaveBase = palOctaveBase if pal else ntscOctaveBase
     octaveBase = palOctaveBase if pal else ntscOctaveBase
     relFreqs = [(1 << (i // 12)) * semitone**(i % 12)
     relFreqs = [(1 << (i // 12)) * semitone**(i % 12)
                 for i in xrange(maxNote)]
                 for i in range(maxNote)]
     periods = [int(round(octaveBase / freq)) - 1 for freq in relFreqs]
     periods = [int(round(octaveBase / freq)) - 1 for freq in relFreqs]
     systemName = "PAL" if pal else "NTSC"
     systemName = "PAL" if pal else "NTSC"
Line 84: Line 87:
def main(argv):
def main(argv):
     if len(argv) >= 2 and argv[1] in ('/?', '-?', '-h', '--help'):
     if len(argv) >= 2 and argv[1] in ('/?', '-?', '-h', '--help'):
         print "usage: %s TABLENAME FILENAME" % argv[0]
         print("usage: %s TABLENAME FILENAME" % argv[0])
         print "known tables:", ' '.join(sorted(tableNames))
         print("known tables:", ' '.join(sorted(tableNames)))
     elif len(argv) < 3:
     elif len(argv) < 3:
         print "mktables: too few arguments; try %s --help" % argv[0]
         print("mktables: too few arguments; try %s --help" % argv[0])
     elif argv[1] in tableNames:
     elif argv[1] in tableNames:
         tableNames[argv[1]](argv[2])
         tableNames[argv[1]](argv[2])
     else:
     else:
         print "mktables: no such table %s; try %s --help" % (argv[1], argv[0])
         print("mktables: no such table %s; try %s --help" % (argv[1], argv[0]))


if __name__=='__main__':
if __name__=='__main__':
     main(sys.argv)
     main(sys.argv)
</pre>
</pre>
The following are exercises for the reader:
* Adapt to [[FDS audio]] and other mapper sound chips by changing <code>ntscOctaveBase</code> and the formula for <code>periods</code>
** Hint: <code>fdsOctaveBase = lowestFreq * 65536 * 64 * 22.0 / 39375000.0</code> and <code>octaveBase * freq</code>
* Adapt to other [[wikipedia:musical tuning|musical tuning]] systems by changing the formula for <code>relFreqs</code>
* Adapt to assemblers other than ca65


== See also ==
== See also ==
*[http://www.freewebs.com/the_bott/NotesTableNTSC.txt Celius' NTSC table]
*[[Celius_NTSC_table|Celius' NTSC table]]
*[http://www.freewebs.com/the_bott/NotesTablePAL.txt Celius' PAL table]
*[[Celius_PAL_table| Celius' PAL table]]

Latest revision as of 20:05, 22 January 2020

APU Pulse and APU Triangle use "period" values to set the pitch of the note. But some people might not know the piano key frequencies or how to convert them to periods for the NES. Fortunately, this has been done for you.

Lookup table

Here's a lookup table from note numbers to the values to write to the pulse and triangle period registers. For the triangle channel, the first value corresponds to the lowest key on a standard piano (an A). The pulse waves sound one octave higher.

; NTSC period table generated by mktables.py
.export periodTableLo, periodTableHi
.segment "RODATA"
periodTableLo:
  .byt $f1,$7f,$13,$ad,$4d,$f3,$9d,$4c,$00,$b8,$74,$34
  .byt $f8,$bf,$89,$56,$26,$f9,$ce,$a6,$80,$5c,$3a,$1a
  .byt $fb,$df,$c4,$ab,$93,$7c,$67,$52,$3f,$2d,$1c,$0c
  .byt $fd,$ef,$e1,$d5,$c9,$bd,$b3,$a9,$9f,$96,$8e,$86
  .byt $7e,$77,$70,$6a,$64,$5e,$59,$54,$4f,$4b,$46,$42
  .byt $3f,$3b,$38,$34,$31,$2f,$2c,$29,$27,$25,$23,$21
  .byt $1f,$1d,$1b,$1a,$18,$17,$15,$14
periodTableHi:
  .byt $07,$07,$07,$06,$06,$05,$05,$05,$05,$04,$04,$04
  .byt $03,$03,$03,$03,$03,$02,$02,$02,$02,$02,$02,$02
  .byt $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
  .byt $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
  .byt $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
  .byt $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
  .byt $00,$00,$00,$00,$00,$00,$00,$00

Table generator

This Python program generated the above lookup table. You can use it to make a table for a PAL NES, which has a different CPU clock rate.

#!/usr/bin/env python3
#
# Lookup table generator for note periods
# Copyright 2010, 2020 Damian Yerrick
#
# Copying and distribution of this file, with or without
# modification, are permitted in any medium without royalty
# provided the copyright notice and this notice are preserved.
# This file is offered as-is, without any warranty.
#
assert str is not bytes
import sys

lowestFreq = 55.0
ntscOctaveBase = 39375000.0/(22 * 16 * lowestFreq)
palOctaveBase = 266017125.0/(10 * 16 * 16 * lowestFreq)
maxNote = 80

def makePeriodTable(filename, pal=False):
    semitone = 2.0**(1./12)
    octaveBase = palOctaveBase if pal else ntscOctaveBase
    relFreqs = [(1 << (i // 12)) * semitone**(i % 12)
                for i in range(maxNote)]
    periods = [int(round(octaveBase / freq)) - 1 for freq in relFreqs]
    systemName = "PAL" if pal else "NTSC"
    with open(filename, 'wt') as outfp:
        outfp.write("""; %s period table generated by mktables.py
.export periodTableLo, periodTableHi
.segment "RODATA"
periodTableLo:\n"""
                    % systemName)
        for i in range(0, maxNote, 12):
            outfp.write('  .byt '
                        + ','.join('$%02x' % (i % 256)
                                   for i in periods[i:i + 12])
                        + '\n')
        outfp.write('periodTableHi:\n')
        for i in range(0, maxNote, 12):
            outfp.write('  .byt '
                        + ','.join('$%02x' % (i >> 8)
                                   for i in periods[i:i + 12])
                        + '\n')

def makePALPeriodTable(filename):
    return makePeriodTable(filename, pal=True)

tableNames = {
    'period': makePeriodTable,
    'palperiod': makePALPeriodTable
}

def main(argv):
    if len(argv) >= 2 and argv[1] in ('/?', '-?', '-h', '--help'):
        print("usage: %s TABLENAME FILENAME" % argv[0])
        print("known tables:", ' '.join(sorted(tableNames)))
    elif len(argv) < 3:
        print("mktables: too few arguments; try %s --help" % argv[0])
    elif argv[1] in tableNames:
        tableNames[argv[1]](argv[2])
    else:
        print("mktables: no such table %s; try %s --help" % (argv[1], argv[0]))

if __name__=='__main__':
    main(sys.argv)

The following are exercises for the reader:

  • Adapt to FDS audio and other mapper sound chips by changing ntscOctaveBase and the formula for periods
    • Hint: fdsOctaveBase = lowestFreq * 65536 * 64 * 22.0 / 39375000.0 and octaveBase * freq
  • Adapt to other musical tuning systems by changing the formula for relFreqs
  • Adapt to assemblers other than ca65

See also