Pently

This document describes Pently, the audio engine used in Pin Eight NES games since 2009.

Introduction

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.

API

The following methods, declared in the assembly language include file pently.inc, make up the public API:

pently_init
Initializes all sound channels. Call this at the start of a program or as a "panic button" before entering a long stretch of code where you don't call pently_update.
pently_start_sound
Plays a sound effect, element A from the 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
Updates the sound channels. Call this once each frame.
pently_start_music
Starts to a song, element A from the songTable.
pently_stop_music
Stops the song, allowing sound effects to continue.
pently_resume_music
Resumes the playing song. Calling pently_resume_music without having first called pently_start_music or after a fine results in undefined behavior.
pently_play_note
Plays note A (see pitch table) on channel X (0, 4, 8, 12, or 16) with instrument Y from pently_instruments.
getTVSystem
Waits for two NMIs, counting the time between them. Returns 0 in A for NTSC systems, 1 for PAL NES, or 2 for Dendy-style PAL famiclones. Make sure your NMI handler finishes within 1500 or so cycles (not taking the whole NMI or waiting for sprite 0) while calling this, or the result in A will be wrong.
getCurBeatFraction
Reads the fraction of the current beat. Returns a value from 0 to 95 in A.

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.

Pitch

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:

Pitch values
ValueNameFrequency (Hz)
0A155.0
1A#1/B♭158.3
2B161.7
3C265.4
4C#2/D♭269.3
5D273.4
6D#2/E♭277.8
7E282.4
8F287.3
9F#2/G♭292.5
10G298.0
11G#2/A♭2103.9
12A2110.0
13A#2/B♭2116.5
14B2123.5
15C3130.8
16C#3/D♭3138.6
17D3146.8
18D#3/E♭3155.6
19E3164.7
20F3174.5
21F#3/G♭3184.9
22G3195.9
23G#3/A♭3207.5
24A3220.2
25A#3/B♭3233.0
26B3246.9
27C4 (middle C)261.4
28C#4/D♭4276.9
29D4293.6
30D#4/E♭4310.7
31E4330.0
32F4349.6
33F#4/G♭4370.4
34G4392.5
35G#4/A♭4415.8
36A4440.4
37A#4/B♭4466.1
38B4495.0
39C5522.7
40C#5/D♭5553.8
41D5588.7
42D#5/E♭5621.4
43E5658.0
44F5699.1
45F#5/G♭5740.8
46G5782.2
47G#5/A♭5828.6
48A5880.8
49A#5/B♭5932.2
50B5989.9
51C61045.4
52C#6/D♭61107.5
53D61177.5
54D#6/E♭61242.9
55E61316.0
56F61398.3
57F#6/G♭61471.9
58G61575.5
59G#6/A♭61669.6
60A61747.8
61A#6/B♭61864.3
62B61962.5
63C72110.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.

Sound effects

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
The name of the sound effect, used for pently_start_sound and drumdef. This value is exported.
baseaddr
Starting address of sound effect data.
length
Length in steps of sound effect data.
period
Time in frames (1 to 16) to play each step of sound effect data.
channel
Which channel to play this sound effect on (0: pulse, 2: triangle, or 3: noise).

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.

Instruments

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
The name of the instrument, used for pently_start_sound and drumdef.
duty
Width of pulse waves. Options are 0 for 12.5% (sharp); 1 for 25% (smooth), or 2 for 50% (hollow). Instruments for the triangle channel MUST use 2.
volume
Starting volume of the sustain phase, from 0 to 15. Volume for the triangle channel is either off (0) or on (nonzero), but instrument volume is compared with sound effect volume.
decayrate
Rate of volume decrease in the sustain phase, in volume units per 16 frames. Optional; defaults to 0.
earlycut
If nonzero, the note shall be cut half a row before the next note. This allows leaving space between notes if there is no attack or decay, especially on triangle. Optional; defaults to 0.
attackptr
Pointer to attack data. Optional; used only if attacklen is larger than 0.
attacklen
Length in steps of attack data.

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
The name of the drum, used in sound effects.
sfx1
An entry in pently_sfx_table to play when this drum is triggered.
sfx2
An optional second entry in 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.

Conductor track

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
An identifier to pass to pently_start_music. Exported.
conductor_addr
The address of the start of this song's conductor data.

Some examples of conductor patterns:

setTempo 288
Sets the playback speed to 288 rows per minute. For example, this can represent 96 beats per minute where a beat is three rows, or 144 beats per minute where a beat is two rows. The speed defaults to 300 rows per minute and can be up to 2047 rows per minute, enough for thirty-second-note resolution at up to 255 quarter notes per minute. The player automatically adjusts the playback speed based on the value of the tvSystem variable (zero: 60.1 Hz, nonzero: 50 Hz).
playPatSq2 4, 27, FLUTE
Plays pattern 4 on the second square wave channel (Sq2), transposed up 15 semitones (base middle C), with instrument FLUTE.
playPatTri 5, 15, 0
Plays pattern 4 on the triangle wave channel (Tri), transposed up 15 semitones (base C3), with instrument BASS.
noteOnNoise $05, 4
Plays note $05 on the noise channel (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
Waits 48 rows before processing the next command. Use this to allow patterns to play through.
fine
Stops music playback. Use this at the end of a piece. Fine is Italian for "end". In sheet music, it directs the musician to stop playing in a piece of ternary (A-B-A) form. More generally, sheet music uses a "final barline" symbol 𝄂 to denote where a piece stops.
segno
Segno (pronounced sen-yo) is Italian for "sign". In sheet music, it refers to the symbol 𝄋 that marks the end of an introduction and the start of a large portion of a piece that should be repeated. This command has a similar function: setting the loop point in the conductor track.
dalSegno
Dal segno (D.S.) is Italian for "from the sign". It directs the musician to go back to the loop point. This command moves the current position in the conductor track to the most recent 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
Stops the pattern playing on the second square wave channel. Patterns ordinarily loop when they reach the end, so you'll need to stop the pattern if you're not going to start another.
attackOnSq1
Plays the attack track on the first square wave channel.
setBeatDuration D_D8
Sets the duration of one beat to a dotted eighth note (three rows). The default is 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.

Patterns

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
An identifier to pass to playPat commands.
patdata_addr
The address of the start of this pattern's data.

Each note's pitch is relative to the transposition base in the playPat command in the conductor track:

Note values in pattern data
CodeNote if base is CName of intervalInterval in semitones
N_CCUnison0
N_CS or N_DBC#/D♭Minor second1
N_DDMajor second2
N_DS or N_EBD#/E♭Minor third3
N_EEMajor third4
N_FFPerfect fourth5
N_FS or N_GBF#/G♭Tritone6
N_GGPerfect fifth7
N_GS or N_ABG#/A♭Minor sixth8
N_AAMajor sixth9
N_AS or N_BBA#/B♭Minor seventh10
N_BBMajor seventh11
N_CHHigh COctave12
N_CSH or N_DBHHigh C#/D♭13
N_DHHigh D14
N_DSH or N_EBHHigh D#/E♭15
N_EHHigh E16
N_FHHigh F17
N_FSH or N_GBHHigh F#/G♭18
N_GHHigh G19
N_GSH or N_ABHHigh G#/A♭20
N_AHHigh A21
N_ASH or N_BBHHigh A#/B♭22
N_BHHigh B23
N_CHHTop CTwo octaves24

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.

Note G played with each of 16 durations
CodeDuration nameLength in rows
N_GSixteenth1
N_G|D_8Eighth2
N_G|D_D8Dotted eighth3
N_G|D_4Quarter4
N_G|D_4, N_TIEQuarter + sixteenth5
N_G|D_D4Dotted quarter6
N_G|D_4, N_TIE|D_D8Quarter + dotted eighth7
N_G|D_2Half8
N_G|D_2, N_TIEHalf + sixteenth9
N_G|D_2, N_TIE|D_8Half + eighth10
N_G|D_2, N_TIE|D_D8Half + dotted eighth11
N_G|D_D2Dotted half12
N_G|D_D2, N_TIEDotted half + sixteenth13
N_G|D_D2, N_TIE|D_8Dotted half + eighth14
N_G|D_D2, N_TIE|D_D8Dotted half + dotted eighth15
N_G|D_1Whole16

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.

Some possible arpeggio values
CodeResult
ARPEGGIO,$00Turn off arpeggio
ARPEGGIO,$30Minor third
ARPEGGIO,$40Major third
ARPEGGIO,$50Perfect fourth
ARPEGGIO,$60Tritone
ARPEGGIO,$70Perfect fifth
ARPEGGIO,$C0Octave
ARPEGGIO,$37Minor chord, root
ARPEGGIO,$38Major chord, first inversion
ARPEGGIO,$47Major chord, root
ARPEGGIO,$49Minor chord, first inversion
ARPEGGIO,$57Sus4 chord
ARPEGGIO,$58Minor chord, second inversion
ARPEGGIO,$59Major 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:

Bugs and limits

No music engine is perfect. These problems exist:

License

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.