Skip to content

mooinglemur/zsmkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZSMKit

Advanced music and sound effects engine for the Commander X16

Due to bugs in the audio bank routine psg_write in earlier ROMs, the minimum earliest Commander X16 ROM release supported is R44.

Overview

ZSMKit is a ZSM playback library for the Commander X16. It aims to be an alternative to ZSound, but with several new features and advantages. Features shared with ZSound include:

  • Playback of ZSM files from high RAM
  • Looping
  • Pausing and resuming playback
  • ZSM tick rates other than 60 are normalized to 60
  • Ability to alter tick rate to change tempo
  • ZCM playback support

It also has these features that ZSound currently lacks:

  • Playback of ZSM files streamed from open files on SD card (disabled by default, requires editing Makefile to enable)
  • Four playback slots with priorities (0-3)
  • Multiple simultaneous slot playback, with priority-based channel arbitration and automatic restore of state when higher priorities end playback
  • "Master volume" control for each playback slot
    • Individual voices' master volumes can also be overridden
  • ZSM files with PCM tracks are now handled and their PCM data is played back
  • ZSM synchronization messages are passed into an optional callback routine
  • Uses YM chip detection routine in ROM >= R44 and redirects LFO reset writes to register $09 if the chip type is OPP/YM2164.

These features are planned but not yet implemented

  • Feature to suspend specific channels for all priorities, allowing the channel/voice to be used outside of ZSMKit, such as simple in-game sound effects, for instance.

Support

The main discussion area for help with ZSMKit is in the Commander X16 Discord server in the ZSMKit thread.

Priority system

In the code and documentation, a song slot is also known as a priority. There are four priorities, numbered from 0 to 3.

Priority 0 is the lowest priority, and thus can be interrupted by any other priority. It would typically used for playback of background game music, as an example. Priority 0 is also the only slot in which LFO parameters are honored (YM2151 registers < $20)

Priorities 1-3 would typically be used for short jingles and sound effects.

When composing/arranging your music and sound effects, keep channel use in mind. For more seamless playback, sound effects are best written to be played on channels that are not used by your main BGM music, or choose channels whose absence in your BGM are less noticeable if they are taken over by the higher priority playback.

In addition, when a song that is currently playing has channels that are restored from being suspended, either by calling zsm_play after being paused, or via a higher priority song ending or being stopped, notes that were supposed to be playing on the YM2151 during the suspension are not restored mid-note. The channel will sound again once the next key down event occurs. Please be aware of this when putting lengthy legatos in your BGM as such passages may stay silent longer than you'd expect if they're interrupted.

The behavior in the previous paragraph is however not a concern on VERA PSG as notes are simply defined by their channel volume. A VERA channel being un-suspended will immediately play sound if the priority on the restored channel calls for it.

Building and using in your project

This library was written for the cc65 suite. As of the writing of this documentation, it is geared toward including in assembly language projects.

To build the library, run
make
from the main project directory. This will create lib/zsmkit.lib, which you can build into your project.

You will likely want to include the file src/zsmkit.inc into your project as well for the library's label names.

Alternative builds

For non-ca65/cc65 projects, there is another option. The build can produce binary blobs lib/8010.bin and lib/8030.bin by calling make incbin One of these files can be included at origin $0810 or $0830 in your project. The jump table addresses can be found the file src/zsmkit8010.inc (or src/zsmkit8030.inc).

There is also another binary blob build target make basicbin which is predominantly for the BASIC integration. In order for this build to work, the line which reads DEFINES += -D ZSMKIT_ENABLE_STREAMING must be commented out of the Makefile. This disables streaming support, saving about 1.5kB of space. This target builds zsmkit-8c00.bin, which is otherwise functionally the same as the other binary blobs, and can be used for projects other than BASIC.

Prerequisites

This library requires a custom linker config in order to specify two custom segments. This documentation assumes you are familiar with creating custom cc65 linker configs.

Specifically, this library requires the ZSMKITLIB segment and the ZSMKITBANK segment. The ZSMKITLIB must be located in low RAM, and the ZSMKITBANK segment is meant to point to high RAM. The linker config is only responsible for assembly of the addresses in the lib. The bank that's assigned to zsmkit is chosen at runtime, and ZSMKit assumes that it has full control of that 8k bank.

NOTE: this is an incomplete linker config file, but rather a relevant example of what must be in a custom one. You can copy the stock cx16.cfg one and make sire it includes the HIRAM region and the two custom segments.

MEMORY {
    ...
    MAIN:     file = %O, start = $0801,  size = $96FF;
    HIRAM:    file = "", start = $A000,  size = $2000;
    ...
}

SEGMENTS {
    ...
    CODE:       load = MAIN,     type = ro;
    ZSMKITLIB:  load = MAIN,     type = ro;
    ZSMKITBANK: load = HIRAM,    type = bss, define = yes;
    ...
}

API Quick Reference

API calls for main part of the program (ZSM)

All calls except for zsm_tick are meant to be called from the main loop of the program. zsm_tick is the only routine that is safe to call from IRQ.


zsm_init_engine

Inputs: .A = RAM bank to assign to ZSMKit

This routine must be called once before any other library routines are called in order to initialize the state of the engine.


zsm_setmem

Inputs: .X = priority, .A .Y = memory location (lo hi), $00 = RAM bank

Prior to calling, set the active RAM bank ($00) to the bank where the ZSM data starts.

This function sets up the song pointers and parses the header based on a ZSM that was previously loaded into RAM. If the song is deemed valid, it marks the priority slot as playable.


zsm_setfile

  • This function requires optional streaming support to be enabled in the build
Inputs: .X = priority, .A .Y = pointer (lo hi) in low RAM to null-terminated filename

This is an alternate song-loading method. It sets up a priority slot to stream a ZSM file from disk (SD card). The file is opened and stays open for as long as the song is playable (i.e. until zsm_close is called, or another song is loaded into the priority). Instead of holding the entire ZSM in memory, it is streamed from the file in small chunks and held in a small ring buffer inside the bank assigned to ZSMKit.

For ZSM files that contain PCM data, the song will play without triggering the PCM events unless zsm_loadpcm is called after zsm_setfile.

Whenever this method is used to play a song, zsm_fill_buffers must be called in the main part of the program in between ticks.

See zsm_setlfs for LFN/device/SA defaults that are used by the engine.


zsm_loadpcm

  • This function requires optional streaming support to be enabled in the build
Inputs: .X = priority, .A .Y = memory location (lo hi), $00 = RAM bank
Outputs: .A .Y = next memory location after end of load, $00 = RAM bank

For streamed ZSM files that have PCM data, this routine can be used to load the PCM data into memory at the specified memory location. This should be done immediately after calling zsm_setfile and before zsm_play.


zsm_close

Inputs: .X = priority

Cleans up any file I/O associated with a priority slot (if it's a song in streaming mode) and resets the state of the slot. In either streaming or normal mode, this routine can be used to permanently stop a song's playback.


zsm_play

Inputs: .X = priority

Starts playback of a song. If zsm_stop was called, this function continues playback from the point that it was stopped. If the file is being streamed rather than played back from memory, this routine will ensure that the ring buffer is at least partially filled.


zsm_stop

Inputs: .X = priority

Pauses playback of a song. Playback can optionally be resumed from that point later with zsm_play.


zsm_rewind

Inputs: .X = priority

Stops playback of a song (if it is already playing) and resets its pointer to the beginning of the song. Playback can then be started again with zsm_play.


zsm_setatten

Inputs: .X = priority, .A = attenuation value

Changes the master volume of a priority slot by setting an attenuation value. A value of $00 implies no attenuation (full volume) and a value of $3F is full mute.

Attenuation is set on all active channels for the priority, and will also affect PCM events played on the priority. The YM2151's attenuation (0.75 dB native) is scaled lower so that it matches the 0.5 dB per step of the VERA PSG. PCM attenuation is scaled to 1/4 the input value.


zsm_fill_buffers

  • This function requires optional streaming support to be enabled in the build
Inputs: none

If you are using the streaming mode of ZSMKit (with zsm_setfile), call this routine once per frame/tick from the main loop of the program (not in the interrupt handler!). This will, if necessary, read some data from open files to keep the ring buffers sufficiently primed so that the zsm_tick call has sufficient data to process for the tick.


zsm_setlfs

  • This function requires optional streaming support to be enabled in the build
Inputs: .A = lfn/sa, .X = priority, .Y = device

Sets the logical file number, secondary address, and IEC device for a particular priority

Must only be called from main loop routines.

Calling this function is not necessary if you wish to use defaults that have been set at engine init:

Priority 0: lfn/sa 11, device 8
Priority 1: lfn/sa 12, device 8
Priority 2: lfn/sa 13, device 8
Priority 3: lfn/sa 14, device 8

zsm_setcb

Inputs: .X = priority, .A .Y = pointer to callback, $00 = RAM bank

Sets up a callback address for ZSMKit to jsr into. The callback is triggered whenever a song loops or ends on its own, or a synchronization message is processed in the ZSM data.

Inside the callback, the RAM bank will be set to whatever bank was active at the time zsb_setcb was called. .X will be set to the priority, .Y will be set to the event type, and .A will be the parameter value.

Y A Meaning
$00 $00 Song has ended normally
$00 $80 Song has crashed
$01 LSB of loop number Song has looped
$02 any Synchronization message from ZSM (sync type 0)
$03 Signed byte: tuning offset in 256ths of a semitone Song tuning change from ZSM (sync type 1)

Since this callback happens in the interrupt handler, it is important that your program process the event and then return as soon as possible. In addition, your callback routine should not fire off any KERNAL calls, or update the screen.

The callback does not need to take care to preserve any registers before returning.

ZSM sync type 0 note: Furnace tracker can be used to create this type of event by placing the EExx effect in any VERA channel. However, please note that this effect will not be exported in ZSMs if placed in a YM2151 channel. For example, the effect EE64 will call the callback with Y = $02 and A = $64 at the moment of the event during playback. This can be useful for synchronization of game animations with the music, or for any other scenario when the application needs to do something at a certain point in the music.

ZSM sync type 1 note: ZSM exports from Furnace will have an event of sync type 1 in the first tick of the song (but not necessarily the first event). The only known user of this information is Melodius, which adjusts its visualizations based on this tuning. A real world example is that for Furnace projects that are tuned to, for instance, A=432 rather than A=440, the tuning information is used to make it so that the note visualizations are not all shown as pitch-bent.


zsm_clearcb

Inputs: .X = priority

Clears the callback assigned to the priority.

Note: The callback settings for a priority are not cleared if the priority switches songs. If you have run zsm_setcb on a priority, it persists until zsm_clearcb (or zsm_init_engine) is called.


zsm_getstate

Inputs: .X = priority
Outputs: .C = playing, Z = not playable, .A .Y = (lo hi) loop counter

Returns the playback state of a priority.

If the priority is currently playing, carry will be set.

If the priority is in an unplayable state, the Z flag will be set.

The loop counter will indicate the number of times the song has looped.


zsm_setrate

Inputs: .X = priority, .A .Y = (lo hi) new tick rate
Outputs: none

Sets a new tick rate for the ZSM in this priority.

Note: ZSMKit expects to have its tick subroutine called at approximately 60Hz. If a ZSM file contains PCM events, it's critical that ZSMKit's tick is run at approximately 60 times a second.

ZSM files have a tick rate which usually matches, at 60 Hz, but this isn't always the case. ZSMKit will scale the tempo based on the ratio between the ZSM's tick rate and 60 Hz. zsm_setrate can be used to override the value in the ZSM. It's mainly useful for changing the tempo of a song.


zsm_getrate

Inputs: .X = priority
Outputs: A .Y = (lo hi) tick rate

Returns the value of the tick rate for the ZSM in this priority.


zsm_setloop

Inputs: .X = priority, .C = whether to loop
Outputs: none

If carry is set, enable the looping behaivor. If carry is clear, disable looping.

By default, the ZSM file indicates whether it is meant to be looped, and will specify a loop point.

This routine can be used to override this behavior.


zsm_opmatten

Inputs: .X = priority, .Y = channel, .A = value
Outputs: none

Changes the volume of an individual OPM channel for a priority slot by setting an attenuation value. A value of $00 implies no attenuation (full volume) and a value of $3F is full mute.


zsm_psgatten

Inputs: .X = priority, .Y = channel, .A = value
Outputs: none

Changes the volume of an individual PSG channel for a priority slot by setting an attenuation value. A value of $00 implies no attenuation (full volume) and a value of $3F is full mute.


zsm_pcmatten

Inputs: .X = priority, .A = value
Outputs: none

Changes the volume of the PCM channel for a priority slot by setting an attenuation value. A value of $00 implies no attenuation (full volume) and a value of $3F is full mute.

Even though the PCM channel's volume has a 4-bit resolution, the attenuation value is scaled so that attentuation values affect all three outputs in a similar way.


zsm_set_int_rate

Inputs: .A = new rate (integer portion in Hz)
        .Y = new rate (fractional portion in 1/256th Hz)
Outputs: none

Sets a new global interrupt rate in Hz. This will be the number of times per second that ZSMKit expects to have its tick subroutine called to advance the music data.

Note: If you expect to play PCM sounds, either in songs or as ZCMs, you will still need to call zsm_tick with .A = 1 at a rate of approximately 60 times per second in order to keep the FIFO filled. See zsm_tick for details.

Calling zsm_init_engine will reset this value to 60.


API calls for main part of the program (ZCM)

ZCM files are PCM data files with an 8-byte header indicating their bit depth, number of channels, and length. In order for ZSMKit to play them, they must be loaded into memory first, and their location in memory given to ZSMKit via the zcm_setmem routine. ZSMKit can track up to 32 ZCMs in memory, slots 0-31, though it's likely you'd exhaust high RAM before having that many loaded at once.

zcm_setmem

Inputs: .X = slot, .A .Y = memory location (lo hi), $00 = RAM bank

Tells ZSMKit where to find a ZCM (PCM sample) image. This image has an 8-byte header followed by raw PCM


zcm_play

Inputs: .X = slot, .A = volume

Starts playback of a ZCM PCM sample. This playback will take priority over any other PCM events in progress until either playback finishes or it is explicitly stopped with zcm_stop.


zcm_stop

Inputs: none

If a ZCM is playing when this routine is called, playback is immediately stopped. If a non-ZCM PCM sound is playing, or nothing is playing on the PCM channel, this routine does nothing.

API calls for interrupt handler

This routine is the only one that is safe to call from an IRQ handler.


zsm_tick

Inputs: .A = 0 (tick music data and PCM)
        .A = 1 (tick PCM only)
        .A = 2 (tick music data only)

This routine handles everything that is necessary to play the currently active songs, and to feed the PCM FIFO if any PCM events are in progress. If required, it will handle restoring channel states if, for instance, a higher priority ZSM ends or is stopped while a lower priority one is also playing.

Call this routine once per tick. You will usually want to do this at the end of your interrupt handler routine. You will usually want to call this with .A = 0 to tick both music data and PCM. The other values are useful if you want to tick the music at a rate different than 60 Hz. You will still want to tick the PCM at ~60 Hz, then use, for instance, a VIA timer to tick the music data at a different rate.

ZSMKit will need to know how often you plan to call its music data tick routine if the value is not the default of 60 Hz. Call zsm_set_int_rate to change this value.

Miscellaneous API calls


zsmkit_setisr

Inputs: none

This sets up a default interrupt service routine that calls zsm_tick on every interrupt. The existing IRQ handler is called afterwards. This will work for most simple use cases of ZSMKit if there's only one interrupt per frame: VERA's VSYNC interrupt.


zsmkit_clearisr

Inputs: none

This routine removes the interrupt service routine that was injected by zsmkit_setisr

About

Advanced music and sound effects engine for the Commander X16

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published