This document describes Pently, the audio engine used in Pin Eight NES games since 2009.
Pently is a music and sound effect player code library for use in games for the Nintendo Entertainment System written in assembly language with ca65. It has seen use in NES games dating back to 2009, including Concentration Room, Thwaite, Zap Ruder, the menu of Action 53, Double Action Blaster Guys, RHDE: Furniture Fight, and Sliding Blaster.
The name comes from Polish pętla meaning a loop. It also reminds one of Greek πέντε (pénte) meaning "five", as it supports five tracks (pulse 1, pulse 2, triangle, drums, and attack injection) mapped onto the NES audio circuit's four tone generator channels.
The following methods, declared in the assembly language include file pently.inc
, make up the public API:
pently_init
pently_update
.pently_start_sound
psg_sound_table
. Watch out: this trashes RAM $0000
through $0004
, so be careful to save this data if you are calling this method from game logic.pently_update
pently_start_music
songTable
.pently_stop_music
pently_resume_music
pently_resume_music
without having first called pently_start_music
or after a fine
results in undefined behavior.pently_play_note
pently_instruments
.getTVSystem
getCurBeatFraction
Your makefile will need to assemble pentlysound.s
, pentlymusic.s
, musicseq.s
and ntscPeriods.s
, and link them into your program. If using getCurBeatFraction
, additionally include math.s
and bpmmath.s
. The file musicseq.s
contains the sound effects, instruments, songs, and patterns that you define; it should .include "pentlyseq.inc"
to use the macros described below.
If Pently is assembled with the command line switch -DPENTLY_USE_ROW_CALLBACK=1
, your code must provide and .export
two callback functions: pently_row_callback
and pently_dalsegno_callback
. These are called before each row is processed and when a dalSegno
or fine
is processed, respectively. They can be useful for synchronizing animations to music. For pently_dalsegno_callback
, carry is clear at the end of a track or set if looping.
The main program must .export
a 1-byte RAM variable called tvSystem
. Pently includes a subroutine that will automatically detect the TV system by measuring the frame length. The file paldetect.s
can be included in an NES program; call getTVSystem
and store the result into tvSystem
before the first call to pently_start_music
. In NSF, the NSF shell stores the TV system in tvSystem
.
Pently expresses pitch in terms of a built-in table of wave periods in equal temperament (12edo). The following values are valid for the square wave channels; the triangle wave channel always plays one octave lower. The player automatically compensates for different APU speeds based on bit 0 of the tvSystem
variable (0: NTSC NES or Dendy famiclone; 1: PAL NES).
Because of the NES's limited precision for wave period values, note frequencies become less precise at high pitches. These frequencies apply to NTSC playback:
Value | Name | Frequency (Hz) |
---|---|---|
0 | A1 | 55.0 |
1 | A#1/B♭1 | 58.3 |
2 | B1 | 61.7 |
3 | C2 | 65.4 |
4 | C#2/D♭2 | 69.3 |
5 | D2 | 73.4 |
6 | D#2/E♭2 | 77.8 |
7 | E2 | 82.4 |
8 | F2 | 87.3 |
9 | F#2/G♭2 | 92.5 |
10 | G2 | 98.0 |
11 | G#2/A♭2 | 103.9 |
12 | A2 | 110.0 |
13 | A#2/B♭2 | 116.5 |
14 | B2 | 123.5 |
15 | C3 | 130.8 |
16 | C#3/D♭3 | 138.6 |
17 | D3 | 146.8 |
18 | D#3/E♭3 | 155.6 |
19 | E3 | 164.7 |
20 | F3 | 174.5 |
21 | F#3/G♭3 | 184.9 |
22 | G3 | 195.9 |
23 | G#3/A♭3 | 207.5 |
24 | A3 | 220.2 |
25 | A#3/B♭3 | 233.0 |
26 | B3 | 246.9 |
27 | C4 (middle C) | 261.4 |
28 | C#4/D♭4 | 276.9 |
29 | D4 | 293.6 |
30 | D#4/E♭4 | 310.7 |
31 | E4 | 330.0 |
32 | F4 | 349.6 |
33 | F#4/G♭4 | 370.4 |
34 | G4 | 392.5 |
35 | G#4/A♭4 | 415.8 |
36 | A4 | 440.4 |
37 | A#4/B♭4 | 466.1 |
38 | B4 | 495.0 |
39 | C5 | 522.7 |
40 | C#5/D♭5 | 553.8 |
41 | D5 | 588.7 |
42 | D#5/E♭5 | 621.4 |
43 | E5 | 658.0 |
44 | F5 | 699.1 |
45 | F#5/G♭5 | 740.8 |
46 | G5 | 782.2 |
47 | G#5/A♭5 | 828.6 |
48 | A5 | 880.8 |
49 | A#5/B♭5 | 932.2 |
50 | B5 | 989.9 |
51 | C6 | 1045.4 |
52 | C#6/D♭6 | 1107.5 |
53 | D6 | 1177.5 |
54 | D#6/E♭6 | 1242.9 |
55 | E6 | 1316.0 |
56 | F6 | 1398.3 |
57 | F#6/G♭6 | 1471.9 |
58 | G6 | 1575.5 |
59 | G#6/A♭6 | 1669.6 |
60 | A6 | 1747.8 |
61 | A#6/B♭6 | 1864.3 |
62 | B6 | 1962.5 |
63 | C7 | 2110.6 |
The pitch table ntscPeriods.s
is generated by mktables.py
, a program written in Python. If you need higher pitches and are satisfied with them being slightly out of tune, you can modify this program to extend it up another octave (through about 75 semitones) and use it to produce a new period file.
At any moment, the mixer chooses to play either the music or the sound effect based on whatever is louder on each channel. If there is already a sound effect playing on the first square wave channel, another sound effect played at the same time will automatically be moved to the second, but a sound effect for the triangle or noise channel will not be moved. A sound effect will never interrupt another sound effect that has more frames remaining.
Sound effects are defined in pently_sfx_table
in musicseq.s
. Each is a sfxdef
line giving a pointer to the sound effect's data, the length in steps, how much to slow it down, and which channel to play it on.
sfxdef name, baseaddr, length, period, channel
name
pently_start_sound
and drumdef
. This value is exported.baseaddr
length
period
channel
Sound effect data consists of a stream of two-byte steps, each consisting of a duty/volume and a pitch value. It may be played at one entry per frame or more slowly for longer sound effects. Volume is in the range $01
through $0F
, and for square wave channels, it can be OR'd with $00
(1/8 duty, sharp), $40
(1/4 duty, smooth), or $80
(1/2 duty, hollow). For sound effects used on the triangle wave channel, always use $80
to keep the note from stopping early due to interaction with the linear counter. The noise channel ignores duty, instead using the upper bit of pitch to determine the type of sound.
Pitch on the square and triangle channels is specified in semitone offsets from the lowest possible pitch (0, a low A). C is 3, 15, 27, 39, 51, or 63. Triangle waves are always played an octave below square waves; middle C is 27 on a square wave channel or 39 on a triangle wave channel. Pitch on a noise channel is $03
(highest) to $0F
(lowest) for ordinary noise or $80
(highest) to $8F
(lowest) for metallic tones. Values $00 through $02 are also valid, but they sound identical to quieter versions of $03.
There can be up to 51 different instrument definitions in a soundtrack.
The envelope determines the volume and timbre of an instrument over time. We take a cue from the Roland D-50 and D-550 synthesizers that a note's attack is the hardest thing to synthesize. An instrument for the D-50 can play a PCM sample to sweeten the attack and leave the decay, sustain, and release to a subtractive synthesizer. Likewise in Pently, an envelope has two parts: attack and sustain.
An attack is like a short sound effect that specifies the duty, volume, and pitch for the first few frames of a note. It's analogous to the arpeggio, volume, and duty envelopes in FamiTracker, but in a more compact format almost identical to that of sound effects with one difference: instead of specifying an absolute pitch (as in FamiTracker's "Fixed" envelope), they specify an offset in semitones from the note's own pitch (as in FamiTracker "Absolute" envelope).
After the attack finishes, the channel continues into the sustain. The duty and initial volume of the channel are set, and then the volume gradually decreases if desired. Each instrument is defined by one line in pently_instruments
in musicseq.s
:
instdef name, duty, volume, decayrate, earlycut, attackptr, attacklen
name
pently_start_sound
and drumdef
.duty
0
for 12.5% (sharp); 1
for 25% (smooth), or 2
for 50% (hollow). Instruments for the triangle channel MUST use 2
.volume
decayrate
earlycut
attackptr
attacklen
is larger than 0.attacklen
Instruments for the noise channel are defined differently from instruments for the tone channels. A table pently_drums
maps up to 25 note codes to pairs of sound effects. A common pattern is for a kick or snare drum to have a triangle component and a noise component, each represented as its own sound effect. Entries are specified as follows:
drumdef name, sfx1, sfx2
name
sfx1
pently_sfx_table
to play when this drum is triggered.sfx2
pently_sfx_table
to play when this drum is triggered.The fifth channel can only play attacks, and it plays them on top of the pulse 1, pulse 2, or triangle channel, replacing the attack phase of that channel's instrument (if any). This is useful for playing staccato notes on top of something else, interrupting the notes much like sound effects do.
The conductor track determines which patterns are played when, how fast to play them, and how much of the song to repeat when reaching the end. This is the rough equivalent of an "order table" in a tracker. Each songdef
line in the list of songs at pently_songs
in musicseq.s
names the song ID (to pass to pently_start_music
) and points to a conductor track.
songdef name, conductor_addr
name
pently_start_music
. Exported.conductor_addr
Some examples of conductor patterns:
setTempo 288
tvSystem
variable (zero: 60.1 Hz, nonzero: 50 Hz).playPatSq2 4, 27, FLUTE
Sq2
), transposed up 15 semitones (base middle C), with instrument FLUTE
.playPatTri 5, 15, 0
Tri
), transposed up 15 semitones (base C3), with instrument BASS
.noteOnNoise $05, 4
Noise
), with instrument 4. Conductor notes always use the instrument system, not the sound effect system, even on the noise channel. This might be useful for, say, a crash cymbal.waitRows 48
fine
segno
dalSegno
segno
. If no segno
was seen, the position moves to the start of the piece; in music, this is called da capo (from the head).
stopPatSq2
attackOnSq1
setBeatDuration D_D8
D_4
, a quarter note (four rows). This has no audible effect, but pently_row_callback
can see rowBeatPart
and rowsPerBeat
as a convenience to synchronize animations or DPCM samples to the music.
The transpose values are in semitones. Pitch values such that the value N_C
in pattern code produces a C are 3, 15, 27, and 39. For example, with transpose 15 on a square wave channel or 27 on a triangle wave channel, N_CH
produces a middle C and N_C
produces the C an octave below it. Other values produce transpositions that can prove useful for fitting a melody into the two-octave range of a single pattern. The noise channel ignores both transpose and instrument.
The list of all conductor commands defined in pentlyseq.inc
follows; the meaning should ideally be self-explanatory given the above descriptions.
playPatSq1, playPatSq2, playPatTri, playPatNoise, playPatAttack
stopPatSq1, stopPatSq2, stopPatTri, stopPatNoise, stopPatAttack
noteOnSq1, noteOnSq2, noteOnTri, noteOnNoise
attackOnSq1, attackOnSq2, attackOnTri
fine, segno, dalSegno
setTempo, setBeatDuration, waitRows
A pattern represents a musical phrase as a sequence of notes with durations. Unlike in traditional trackers, patterns can be any length, with a shorter pattern on one track looping while a longer pattern on another track plays. Patterns are listed below pently_patterns
in musicseq.s
:
songdef name, patdata_addr
name
playPat
commands.patdata_addr
Each note's pitch is relative to the transposition base in the playPat
command in the conductor track:
Code | Note if base is C | Name of interval | Interval in semitones |
---|---|---|---|
N_C | C | Unison | 0 |
N_CS or N_DB | C#/D♭ | Minor second | 1 |
N_D | D | Major second | 2 |
N_DS or N_EB | D#/E♭ | Minor third | 3 |
N_E | E | Major third | 4 |
N_F | F | Perfect fourth | 5 |
N_FS or N_GB | F#/G♭ | Tritone | 6 |
N_G | G | Perfect fifth | 7 |
N_GS or N_AB | G#/A♭ | Minor sixth | 8 |
N_A | A | Major sixth | 9 |
N_AS or N_BB | A#/B♭ | Minor seventh | 10 |
N_B | B | Major seventh | 11 |
N_CH | High C | Octave | 12 |
N_CSH or N_DBH | High C#/D♭ | 13 | |
N_DH | High D | 14 | |
N_DSH or N_EBH | High D#/E♭ | 15 | |
N_EH | High E | 16 | |
N_FH | High F | 17 | |
N_FSH or N_GBH | High F#/G♭ | 18 | |
N_GH | High G | 19 | |
N_GSH or N_ABH | High G#/A♭ | 20 | |
N_AH | High A | 21 | |
N_ASH or N_BBH | High A#/B♭ | 22 | |
N_BH | High B | 23 | |
N_CHH | Top C | Two octaves | 24 |
Only one note can be played on a single track at once; playing a note cuts the one already playing. To stop a note without playing another, use a REST
.
Each note or rest is OR'd with a duration, or the number of rows to wait after the note is played. The durations are in fractions of a 16-row "whole note", following standard practice for describing durations in U.S. and Canadian English, most other Germanic languages, Chinese, and Greek. Available durations are 𝅘𝅥𝅯 sixteenth (default, 1 row), 𝅘𝅥𝅮 eighth (|D_8
, 2 rows), 𝅘𝅥 quarter (|D_4
, 4 rows), 𝅗𝅥 half (|D_2
, 8 rows), and 𝅝 whole (|D_1
, 2 rows). Augmented (or "dotted") versions of eighth, quarter, and half notes are 50 percent longer: 𝅘𝅥𝅮𝅭 dotted eighth (|D_D8
, 3 rows), 𝅘𝅥𝅭 dotted quarter (|D_D4
, 6 rows), and 𝅗𝅥𝅭 dotted half (|D_D2
, 12 rows). Not all durations can be expressed with one row, but anything up to 20 rows can be made from two tied notes: a note with D_4
, D_2
, or D_D2
followed by N_TIE
, N_TIE|D_8
, or N_TIE|D_D8
.
Code | Duration name | Length in rows |
---|---|---|
N_G | Sixteenth | 1 |
N_G|D_8 | Eighth | 2 |
N_G|D_D8 | Dotted eighth | 3 |
N_G|D_4 | Quarter | 4 |
N_G|D_4, N_TIE | Quarter + sixteenth | 5 |
N_G|D_D4 | Dotted quarter | 6 |
N_G|D_4, N_TIE|D_D8 | Quarter + dotted eighth | 7 |
N_G|D_2 | Half | 8 |
N_G|D_2, N_TIE | Half + sixteenth | 9 |
N_G|D_2, N_TIE|D_8 | Half + eighth | 10 |
N_G|D_2, N_TIE|D_D8 | Half + dotted eighth | 11 |
N_G|D_D2 | Dotted half | 12 |
N_G|D_D2, N_TIE | Dotted half + sixteenth | 13 |
N_G|D_D2, N_TIE|D_8 | Dotted half + eighth | 14 |
N_G|D_D2, N_TIE|D_D8 | Dotted half + dotted eighth | 15 |
N_G|D_1 | Whole | 16 |
A pattern can force a particular instrument to be used, such as when a pattern alternates between instruments. For this, use INSTRUMENT
followed by the instrument's name.
Legato, also called slur or HOPO, is an effect that skips the ordinary note-on process. A note played legato is played by changing the pitch of the existing note on a channel without restarting its envelope, and instruments set to note-off a half row early will not do so when legato is on. To slur a set of notes, put LEGATO_ON
after the first and LEGATO_OFF
after the last. Legato doesn't make sense in the attack track, which is played staccato by definition.
Arpeggio rapidly cycles a note among two or three different pitches, which produces the warbly chords heard in SIDs and NSFs by European composers. The arpeggio is specified as a hexadecimal number, similar to that used with the J47
effect in S3M or IT or the 047
effect in MOD, XM, or FTM. with a first and second nibble representing intervals in semitones. If the second nibble is 0, only two steps are used; otherwise, three steps are used. For example, ARPEGGIO,$47
makes a major chord in root position including 4 semitones (a major third) and 7 semitones (a perfect fifth) above the root note. There are three ways to make an interval, depending on how much the lower or higher note should dominate. For example, with an octave ARPEGGIO,$0C
is three steps low, low, and high; ARPEGGIO,$C0
is two steps low and high; and ARPEGGIO,$CC
is three steps low, high, and high. Arpeggio doesn't work in the attack track, and an arpeggio involving both a base note below middle C and an interval below an octave tends to sound muddy.
Code | Result |
---|---|
ARPEGGIO,$00 | Turn off arpeggio |
ARPEGGIO,$30 | Minor third |
ARPEGGIO,$40 | Major third |
ARPEGGIO,$50 | Perfect fourth |
ARPEGGIO,$60 | Tritone |
ARPEGGIO,$70 | Perfect fifth |
ARPEGGIO,$C0 | Octave |
ARPEGGIO,$37 | Minor chord, root |
ARPEGGIO,$38 | Major chord, first inversion |
ARPEGGIO,$47 | Major chord, root |
ARPEGGIO,$49 | Minor chord, first inversion |
ARPEGGIO,$57 | Sus4 chord |
ARPEGGIO,$58 | Minor chord, second inversion |
ARPEGGIO,$59 | Major chord, second inversion |
The transpose command changes the pitch of the rest of a pattern by a given number of semitones. For example, TRANSPOSE,5
moves the rest of the pattern up a perfect fourth. TRANSPOSE,<-12
moves down an octave, with the -
denoting negative and the <
working around ca65's lack of support for signed bytes. It's most often used to include notes more than two octaves apart in one pattern.
The grace command shortens the next two rows to one row's length. The next byte specifies the length in frames of the first note in the pair. Like the EDx
command in MOD/XM or the SDx
command in S3M/IT, it's designed for making an acciaccatura (grace note) or a set of triplets (3 notes in the time of 4). For example, to play a short C note for 4 frames followed by a B flat that is as long as a quarter note minus 4 frames, do GRACE,4,N_CH, N_BB|D_Q4.
Finally, to end the pattern, use PATEND
. This isn't strictly necessary if a pattern is always interrupted at its end, but if it isn't present, playback will fall through into the following pattern.
The following are all the symbols that are valid in pattern code:
N_C, N_CS, N_D, N_DS, N_E, N_F, N_FS, N_G, N_GS, N_A, N_AS, N_B
N_CH, N_CSH, N_DH, N_DSH, N_EH, N_FH, N_FSH, N_GH, N_GSH, N_AH, N_ASH, N_BH
N_CHH
N_DB, N_EB, N_GB, N_AB, N_BB, N_DBH, N_EBH, N_GBH, N_ABH, N_BBH
N_TIE, REST
D_8
(2 rows), D_D8
(3 rows), D_4
(4 rows), D_D4
(6 rows), D_2
(8 rows), D_D2
(12 rows), D_1
(16 rows)INSTRUMENT, ARPEGGIO, LEGATO_ON, LEGATO_OFF, TRANSPOSE, PATEND
No music engine is perfect. These problems exist:
pently_row_callback
.pently_play_note
.The Pently audio engine is distributed under the following terms:
Copyright 2010-2015 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 in all source code copies. This file is offered as-is, without any warranty.
This means that yes, you may use Pently in games that you are selling on cartridge. And no, you do not have to make your game free software; this is not a copyleft. But I'd appreciate a mention of Pently in the game's credits.