APU period table

From NESdev Wiki
Revision as of 23:29, 3 April 2010 by Tepples (talk | contribs) (and if you have no python...)
Jump to navigationJump to search

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. This Python program generates lookup tables in an assembly language file that you can .include into your music engine:

#!/usr/bin/env python
#
# Lookup table generator for note periods
# Copyright 2010 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.
#
from __future__ import with_statement, division
import sys

ntscOctaveBase = 39375000.0/(22 * 16 * 55)
palOctaveBase = 266017125.0/(10 * 16 * 16 * 55)
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 xrange(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)

Output

Not everybody has Python installed, so here's an example of the program's output for NTSC:

; 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