The C64 OS Programmer's Guide is being written
This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.
Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.
C64 OS PROGRAMMER'S GUIDE
Library Reference: sidplay.lib
Overview
sidplay.lib has routines to load and unload SID music in PSID format, to retrieve information about the music's name, author and release and copyright year, and to play, pause or stop the individual tunes found within the file. Music loaded with this library is integrated into the system's interrupt service handler and playing the music requires no other set up but to call the routines in this library.
sidplay.lib is capable of playing tunes that have been relocated to different places in memory. A file will only successfully load if the memory into which it is being loaded is available. There are no checks on which zero page addresses the file uses. If its ZP usage has not been relocated to be compatible with C64 OS, playing the music will almost certainly result in a crash.
Name | sidplay.lib |
---|---|
code | si |
size | 3 pages |
Link | Yes |
Unlink | Yes |
Emits Memory Warnings | Yes |
PSID Format Overview
SID music is code and data mixed together. The code is executed at some regular interval, such as on every screen refresh (50Hz or 60Hz) or it can be timed with a CIA timer.
PSID wraps raw SID data (code and music data) with a header that describes properties of the music. For example, the PSID header defines the initialization address, this is called once before starting to play any song so it can perform any necessary set up, such as populating zero page pointers.
The PSID header also defines the play address, this routine is executed 50 or 60 times a second to play the music. The PSID header indicates how many songs (tracks) are contained within the data, and which is the starting song. It also holds some metadata, such as the name, author and copyright.
Notably missing from the PSID headers is the time length of the songs. This information is provided by the High Voltage SID Collection in a separate database file, and this information is made available from the OpCoders SID database, but it is not yet handled by sidplay.lib.
PSID format
.text "PSID"|"RSID" .word $01|$02|$03|$04 version number .word $76|$7c|$7c|$7c sid data offset .word $00 load addr. $00 = data head .word $00 init addr. .word $00 play addr. .word $00 number of songs. .word $00 start song #. .byte 0,0,0,0 32b BE. Speed of each song. 0=vblank 1=60Hz CIA .text 32-bytes Tune Name .text 32-bytes Author .text 32-bytes Copyright ;Data Starts Here if Version $01 (Data offset $76) .word $00 flags .byte $00 startPage (relocStartPage) .byte $00 pageLength (relocPages) .byte $00 2nd SID Addr. .byte $00 3rd SID Addr. ;Data Starts Here if Version $02 through $04
Library Routines
Link and Unload
When this library is linked to the KERNAL it also patches a player routine into the SIDPlay IRQ vector.
spirqvec = $0334 ;$0335
It is critically important to use the slunload
flag when
unloading sidplay.lib.
This will automatically unload any loaded PSID and free its memory. It also clears the SIDPlay
IRQ vector before the library is unloaded. If the library is unloaded without the
slunload
flag, the Interrupt Service Routine will continue to call whatever
spirqvec
points at and this would soon lead to a crash.
Load Tune (loadtune_)
Purpose | Load a PSID file into memory |
---|---|
Jump offset | $06 |
Communications registers | A, X, Y |
Stack requirements | 16+ |
Zero page usage | $1c, $1d, $1e, $1f, $20, $21, $61, $62, $63, $64, $65, $66, $67, $68, $90, $ba, $fb, $fc |
Registers affected | A, X, Y |
Input parameters |
RegPtr → File reference to PSID file to load RegPtr → NULL to unload currently loaded PSID |
Output parameters |
When loadingC ← CLR = SID file loaded.A ← Number of songs loaded A ← 0 if not enough memory to load the data X ← Start song # C ← SET = SID not loaded. When unloadingC ← CLR = SID file unloaded.C ← SET = Nothing needed to be unloaded. |
Description: This routine loads a PSID file into memory. If a SID tune has previously been loaded into memory it starts by unloading it, then proceeds to load the new file. The new file is specified by passing a file reference in a RegPtr. However, you can also use this routine to explicitly unload the previous file without loading a new file by passing a null RegPtr (X → $00, Y $rarr; $00). When a file is unloaded, all memory that the file occupied is deallocated.
It starts by loading the PSID header data into statically allocated memory in the library. And then analyzes the header to know how much data needs to be loaded and to where in memory.
It first checks to see if the required block of memory is available, if it is not, it sends low memory warnings to the Application (but not to any Utility). It sends a warning, and if the Application reports that it freed some memory, it checks to see if the required block is available. If it is still not available, it repeats the memory warnings until either the block becomes available or the Application reports that it has no more memory to free.
The memory required for the data is unavailable, it returns with the Accumulator set to 0. This indicates that 0 songs are available. However, because the PSID headers were loaded in, you can still call tuneinfo to retrieve the metadata.
If the required block of memory is available, then the data is loaded into that area of memory, and that area of memory is marked as allocated. The accumulator returns the number of songs available.
The carry is used to indicate whether or not any part of the PSID file was loaded. For example, if the RegPtr points to a file which is not a valid or supported PSID file, the carry is returned set. When the carry is returned set, none of the other registers mean anything. If the file is valid and the PSID headers were loaded in, whether or not there was enough memory to load the file's data, the carry is returned clear.
Change Tune Status (stattune_)
Purpose | Play, pause or stop playing music |
---|---|
Jump offset | $09 |
Communications registers | A, X |
Stack requirements | 6+ |
Zero page usage | $61, $62, $63, $64, $65, $66, $67, $68 |
Registers affected | A, X, Y |
Input parameters |
A → Tune State | Tune # to init A → sps_skip X → seconds to skip |
Output parameters | None |
Description: This routine is used to change the state of the music. Either to initialize a song, to start playing, to pause or to stop (halt) a song.
If no song data is loaded into memory, calling this routine has no effect, it returns without generating an error or providing any response.
While the sidplay.lib is loaded, every time the interrupt service routine is executed it
jumps through the SIDPlay IRQ vector (spirqvec
.) This vector points at a
player routine that inside sidplay.lib. The player checks the play state variable, also
internal to sidplay.lib, to decide what to do.
The default non-playing state is sps_hold
. Every time the player routine is
called, if the state is sps_hold
it returns immediately.
To start playing a song that song must first be initialized. Load the song number
to initialize into the accumulator and call stattune
.
loadtune returns the number of songs in the loaded file. You
must hold onto this number locally as there is no way to retrieve it from library after
the PSID has been loaded. You should only initialize a tune by passing a valid song
number, from 0 to the number of songs available minus one.
State values that control the player routine start at the high end with values $ff, $fe, $fd
and so on. This distinguishes them from song numbers which start at the low end with values
from $00, $01, $02 etc. The highest selectable song number is $fb, because $fc is interpreted
as sps_skip
. In practice this should not be a problem as PSIDs will not generally
hold more than few tens of songs. If you pass a song number that is greater than the available
songs in the file the result is undefined, because it depends on how the init routine of
the currently loaded PSID handles that invalid condition. It could lead to a crash, so your
code should make sure not to allow the user to pick a song higher than what is available.
After calling stattune
and passing a song number, the song number is put into
the sidplay.lib's state and nothing happens until the ISR calls the player. The player
then loads the state into the accumulator, and if it's less than sps_skip
it
calls the PSID's init routine. It then changes the player's state to sps_play
and returns.
On subsequent ISR calls of the play routine, because the state is sps_play
it
calls the PSID's play routine and returns. And the music will be playing. Thus, initializing
a song also starts playing it automatically.
If sps_hold
is passed to stattune
it first silences all
three SID voices (only a single SID at $D400 is silenced,) then the sps_hold
state
is set so the ISR's subsequent calls to the play routine return immediately. The internal
state of the play routine is unaffected.
If sps_play
is passed to stattune
that state is saved, and when
the ISR calls the player it will resume playing by calling the PSID's play routine. Internal
state of where the song left off should allow the song to resume. However, in practice it
may not resume as expected, because the player routine may expect that one or more voices are
not silenced. If/when the player routine encounters data that re-enables a previously silenced
voice, then that voice will resume as normal. Preserving the state of the voices when
sps_hold
is passed in and restoring them when sps_play
is passed
in is probably an improvement that can be made to a future version of sidplay.lib.
Another improvement for a future version is to silence and restore the voices of additional SID chips, at the addresses specified in the extended PSID header.
Skip ahead
If sps_skip
is passed to stattune
, the number of seconds to skip
must be passed in the X register. The number of play calls per second is calculated by
multiplying by 50 if the song is chip is specified as PAL in the PSID headers or by 60 if
the chip is NTSC. The play routine is then called that many times, in a tight loop.
For example, suppose you pass sps_skip
and 10 in the X register, for 10 seconds.
If the PSID headers indicate this file is for a PAL chip, 10 is multiplied by 50, and the
play routine is then called 500 times in a tight loop.
The result of this is not always as expected, since the SID chip has envelopes that take some amount of real time to resolve. Also, depending on the file and how its play routine works, this may lead to other problems. Skip is therefore not reliable and it should either be avoided or only used with SIDs that are known to handle it without crashing.
Stopping or changing songs
The most reliable way to stop playing a song is to first send sps_halt
. Delay
at least one jiffy (one call of the ISR to the player routine,) and then to initialize a new
song by passing its song number to stattune
. This reinitialize the song and
starts playing it from the start.
State Constants
Constant | Value | Description |
---|---|---|
sps_hold | $ff | Pause playback without losing place |
sps_halt | $fe | Stop playback and lose place |
sps_play | $fd | Start playing a song |
sps_skip | $fc | Skip ahead some number of seconds |
Get Tune Information (tuneinfo_)
Purpose | Fetch PSID metadata |
---|---|
Jump offset | $0c |
Communications registers | A, X, Y |
Stack requirements | 2 |
Zero page usage | None |
Registers affected | A, X, Y |
Input parameters |
X → Tune Info Index |
Output parameters |
If X → spi_syst | spi_chipA ← Bit field responseIf X → spi_name | spi_auth | spi_copyRegPtr ← Pointer to string field |
Description: This routine is used to fetch metadata from the PSID headers. The information that you're requesting dictates the kind of data you'll get back and in what registers.
Call tuneinfo
and pass a tune info constant in the X register. If passing
spi_name, spi_auth or spi_copy, a RegPtr is returned that points to the string field that was
loaded directly from the PSID header. Each of these is a 32 character field. Usually these
fields are padded with spaces to the end and are therefore not NULL-terminated strings.
If you are going to output them or process them with something that requires the strings to be NULL terminated the only safe way is to explicitly copy up to 32 bytes (or less) to your own buffer, and give your own buffer a final NULL byte.
You could, alternatively, explicitly write a $00 at index 31 of the pointer that is returned. This would guarantee that the fields are null terminated, but if the field originally held a full 32 bytes of text it would also overwrite the final byte of data.
The field data is converted from ASCII to PETSCII automatically by loadtune
.
If you pass spi_syst
or spi_chip
into tuneinfo
, the
response is a bitfield returned in the accumulator. See tables of bitfield responses below.
Tune Info Constants
Constant | Value | Description |
---|---|---|
spi_name | $00 | The name/title of the music |
spi_auth | $01 | The artist or author of the music |
spi_copy | $02 | The copyright year or release event |
spi_syst | $03 | The video system, PAL or NTSC, for playing timing |
spi_chip | $04 | The SID chip, 6581 or 8580, the music was designed for |
Bitfield Tune Info
The following bitfields are returned in response to a request for spi_syst
.
Constant | Value | Description |
---|---|---|
spc_unkn | %0000 0000 | Video System Unknown |
spc_pal | %0000 0001 | PAL Video System |
spc_ntsc | %0000 0010 | NTSC Video System |
The following bitfields are returned in response to a request for spi_chip
.
Constant | Value | Description |
---|---|---|
spc_6581 | %0000 0001 | 6581 SID Chip |
spc_8580 | %0000 0010 | 8580 SID Chip |
spc_both | %0000 0011 | 6581 and 8580 SID chips |
Previous Section: Library Reference
Next Chapter: Using Toolkit
Table of Contents
This document is subject to revision updates.
Last modified: May 20, 2025