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:
psg_sound_table. Watch out: this trashes RAM
$0004, so be careful to save this data if you are calling this method from game logic.
pently_resume_musicwithout having first called
pently_start_musicor after a
fineresults in undefined behavior.
Your makefile will need to assemble
ntscPeriods.s, and link them into your program. If using
getCurBeatFraction, additionally include
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_dalsegno_callback. These are called before each row is processed and when a
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
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:
|27||C4 (middle C)||261.4|
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
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
drumdef. This value is exported.
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
$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
instdef name, duty, volume, decayrate, earlycut, attackptr, attacklen
0for 12.5% (sharp);
1for 25% (smooth), or
2for 50% (hollow). Instruments for the triangle channel MUST use
attacklenis larger than 0.
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
pently_sfx_tableto play when this drum is triggered.
pently_sfx_tableto 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
musicseq.s names the song ID (to pass to
pently_start_music) and points to a conductor track.
songdef name, conductor_addr
Some examples of conductor patterns:
tvSystemvariable (zero: 60.1 Hz, nonzero: 50 Hz).
playPatSq2 4, 27, FLUTE
Sq2), transposed up 15 semitones (base middle C), with instrument
playPatTri 5, 15, 0
Tri), transposed up 15 semitones (base C3), with instrument
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.
segno. If no
segnowas seen, the position moves to the start of the piece; in music, this is called da capo (from the head).
D_4, a quarter note (four rows). This has no audible effect, but
rowsPerBeatas 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
songdef name, 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|
|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
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_D2 followed by
|Code||Duration name||Length in rows|
|Quarter + sixteenth||5|
|Quarter + dotted eighth||7|
|Half + sixteenth||9|
|Half + eighth||10|
|Half + dotted eighth||11|
|Dotted half + sixteenth||13|
|Dotted half + eighth||14|
|Dotted half + dotted eighth||15|
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.
|Turn off arpeggio|
|Minor chord, root|
|Major chord, first inversion|
|Major chord, root|
|Minor chord, first inversion|
|Minor chord, second inversion|
|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
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_DB, N_EB, N_GB, N_AB, N_BB, N_DBH, N_EBH, N_GBH, N_ABH, N_BBH
INSTRUMENT, ARPEGGIO, LEGATO_ON, LEGATO_OFF, TRANSPOSE, PATEND
No music engine is perfect. These problems exist:
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.