Skip to content

Instantly share code, notes, and snippets.

@undisbeliever
Last active March 16, 2024 03:58
Show Gist options
  • Save undisbeliever/f81c1ac0927924da19f882d76f5e4dc4 to your computer and use it in GitHub Desktop.
Save undisbeliever/f81c1ac0927924da19f882d76f5e4dc4 to your computer and use it in GitHub Desktop.
Draft Terrific Audio Driver S-CPU API

The Terrific Audio Driver ca65 API has been implemented and can be found at https://github.com/undisbeliever/terrific-audio-driver/tree/main/audio-driver/ca65-api

The ca65 API used in Terrific Audio Driver might differ from this document.



Terrific Audio Driver Draft S-CPU API

DRAFT

Last updated: 15 February 2024

UNDECIDED

  • Should I gate Tad_QueueCommand_Pause, Tad_QueueCommand_Unpause, etc behind a .ifdef?
  • Should I add Tad_PauseCommandOverride, Tad_UnpauseCommandOverride, etc functions?
  • Should Tad_Init detect if the loader is running?
    • Less likely to deadlock if Tad_Init accidentally called.
    • This will require a multi-scanline delay waiting for the loader and IPL to be ready.
      (No easy way to detect if there was a delay between reset and Tad_Init call (as copiers do not start at scanline 0))
  • Should Tad_Init disable interrupts?

Register State

All functions in this document are called with the following register state:

  • Native mode
  • 8 bit Memory
  • 16 bit Index (unless otherwise stated)
  • D = 0
  • DBR accesses low-RAM (0x7e or 0x00..=0x3F or 0x80..=0xBF)
  • Not in an interrupt ISR

Data

The following is embedded (uncompressed) in the ROM:

  • Loader
  • Audio Driver
  • Blank Song

The following data is returned by the LoadSongData callback:

  • Common Audio Data
    • samples, pitch table and sound effects
  • Song Data
    • only 1 song is loaded into audio-RAM at any given time

Variables

All variables MUST ONLY be written to by the TAD subroutines.

  • max_transfer_per_frame - The maximum number of bytes to transfer to Audio-RAM per Tad_Process call.
  • next_song - the next song to load after the common audio data is loaded into memory

Queues

There are 4 queues, in order of priority:

  • The remaining data to transfer into Audio-RAM
    • The data is allowed to cross bank boundaries.
    • If data_ptr + max_transfer_per_frame crosses a bank boundary:
      • 0x10000 - (data_ptr & 0xffff) bytes will be transferred in the Tad_Process call.
      • data_ptr is advanced to the start of the next bank (based on the memory map)
      • Subsequent calls to Tad_Process will continue the transfer until data size == 0.
    • Only accessed in LOADING_* states
  • The next song to load into Audio-RAM
    • If this value is 0, a blank (silent) song will be used
    • Accessed after common audio data is loaded into Audio-RAM
  • The next command to send to the audio driver (and command parameter)
    • The queue is empty of next_command is negative
  • The next sound effect to play (and sfx pan)
    • Lower sound effect indexes take priority over higher sound effect indexes (as defined by the project file sound effect export order)
    • No queued sound effect = 0xff

All four queues have a maximum size of 1.

States

  • NULL
  • WAITING_FOR_LOADER
  • LOADING_COMMON_AUDIO_DATA
  • LOADING_SONG_DATA
  • PAUSED
    • Must be second last so I can use cmp and return carry in Tad_IsSongLoaded
  • PLAYING
    • Must be last so I can use cmp and return carry in Tad_IsPlaying

Flags

  • Stereo
    • If set, the next song will be played in stereo
    • If clear, the next song will be played in mono
    • Default: Mono
  • PlaySongImmediatly
    • If set, the audio driver will play the song after the next song is loaded into Audio-RAM
    • If clear, the audio driver will be paused after the next song is loaded into Audio-RAM
    • Default: Set
  • ReloadCommonAudioData
    • If set, the common audio data will be loaded into audio-RAM the next time a song is requested.
    • If clear, the common audio data will remain unchanged.

Defines

  • HIROM - The game uses HiROM mapping
  • LOROM - The game uses LoROM mapping

Commands

  • Pause (no parameter)

    • Stop song and sound effect execution
    • The audio driver can still process commands while paused
  • Unpause (no parameter)

    • Unpauses the audio driver
    • Resets the S-SMP timer counters
    • Can cause issues if this command is repeatedly sent to the audio driver
  • Stop sound effects (no parameter)

    • Stop all active sound effects
  • Set Main Volume (signed i8 parameter)

    • NOTE: The main volume is reset whenever a new song is loaded into Audio-RAM
  • Set Enabled Channels (8 channel u8 bitmask)

    • Useful for disabling channels in a song
    • NOTE: The enabled channels bitmask is reset whenever a song is loaded
    • NOTE: Channels 6 & 7 are used by sound effects
  • Set Song tempo (u8 TIMER0 register value)

    • NOTE: The song can still change the tempo

Subroutines

Tad_Init

Initialises the audio driver:

  • Loads the loader into Audio-RAM
  • Loads the audio driver into Audio-RAM
  • Sets the song to 0 (silence)
  • Resets the state and flags
  • Queues a common audio data transfer

TIMING:

  • Should be called more than 40 scanlines after reset.
    • Otherwise Tad_Init will need to wait until the S-SMP IPL is ready.
  • MUST be called ONCE
    • Calling Tad_Init more than once will hardlock.
  • MUST be called with INTERRUPTS DISABLED
  • MUST be called while the S-SMP is running the IPL.
  • MUST be called after the LoadAudioData callback is setup (if necessary)
  • Tad_Init MUST be called before any other TAD subroutine.

Tad_Process

Processes the next queue. This function will do one of the following, depending on the state:

  • Transfer up to max_transfer_per_frame bytes of common audio data or song data to Audio-RAM.
    • May call LoadAudioData.
  • Send a command to the audio driver.
  • Send a play-sound effect command to the audio driver.

NOTES: The command and sound-effect queues will be reset after a new song is loaded into Audio-RAM.

TIMING:

  • Should be called once per frame.
  • MUST be called after Tad_Init.
  • MUST NOT be called in an interrupt.

Tad_FinishLoadingData

Finish loading the data into audio-RAM.

This function can be safely called by LoadAudioData.

This function may require multiple frames of execution time.

Tad_QueueCommand

Adds a command to the queue if the queue is empty.

MUST NOT be used to send a play-sound-effect command.

The command queue can only hold 1 command.

INPUT:

  • A = command
  • X = command parameter (if any). Only the lower 8 bits will be sent to the Audio Driver.

OUTPUT:

  • Carry set if the command was added to the queue

REGISTER STATE:

  • 8 bit Memory
  • 8 or 16 bit Index

Tad_QueueCommandOverride

Adds a command to the queue, overriding the any previously queued commands.

MUST NOT be used to send a play-sound-effect command.

CAUTION: This subroutine can override any PAUSE or UNPAUSE commands.

INPUT:

  • A = command
  • X = command parameter (if any). Only the lower 8 bits will be sent to the Audio Driver.

OUTPUT:

  • Carry set

REGISTER STATE:

  • 8 bit Memory
  • 8 or 16 bit Index

Tad_QueuePannedSoundEffect

Only 1 sound effect can be queued at a time. Sound effects with a lower sound effect index will override the queue.

Queued sound effects might not be played. They could be overridden by a future QueueSoundEffect call or the audio driver might not have a free channel to play the sound effect with.

INPUT:

  • A - sound effect index (as determined by the sound effect export order in the project file)
  • X - pan (only the lower 8 bits are used. If pan is > MAX_PAN, the sound effect will use center pan)

REGISTER STATE:

  • 8 bit Memory
  • 8 or 16 bit Index

Tad_QueueSoundEffect

Calls QueuePannedSoundEffect with center pan (MAX_PAN/2).

REGISTER STATE:

  • 8 bit Memory
  • 8 or 16 bit Index

Tad_LoadSong

Disables the audio driver, starts the loader and queues a song transfer.

CAUTION: The audio driver starts in the paused state if the PlaySongImmediatly flag is false.

CAUTION: Tad_Process will call LoadAudioData (which can take a while if it decompresses song data).

INPUT:

  • A = 0 - Play a blank (silent) song
  • A >= 1 - Play song number A

Tad_ReloadCommonAudioData

If this subroutine is called, the common audio data will be reloaded into Audio-RAM. This will not take effect until the next LoadSong call.

Tad_SetMono

Clears the stereo flag. This will not take effect until the next LoadSong call.

Tad_SetStereo

Sets the stereo flag. This will not take effect until the next LoadSong call.

Tad_GetStereoFlag

Reads the mono/stereo flag

OUTPUT:

  • Carry set if stereo

Tad_SongsStartImmediatly

Sets the PlaySongImmediatly flag

Tad_SongsStartPaused

Clears the PlaySongImmediatly flag

Tad_SetTransferSize

Sets the number of bytes to transfer to Audio-RAM per Tad_Process call.

The value will be clamped from MIN_TRANSFER_PER_FRAME to MAX_TRANSFER_PER_FRAME.

INPUT: X = number of bytes to transfer on every Tad_Process call.

Tad_IsLoaderActive

OUTPUT:

  • Carry set if the loader is still using data returned by LoadAudioData (state == LOADING_*)

Tad_IsSongLoaded

OUTPUT:

  • Carry set if the song is loaded into memory (state >= PAUSED)

Tad_IsSongPlaying

OUTPUT:

  • Carry set if state == PLAYING

Miscellaneous Subroutines

The following subroutines will call Tad_QueueCommand (returning carry set if command queued successfully):

  • Tad_QueueCommand_Pause
  • Tad_QueueCommand_Unpause
  • Tad_QueueCommand_StopSoundEffects
  • Tad_QueueCommand_SetMainVolume
  • Tad_QueueCommand_SetEnableChannels
  • Tad_QueueCommand_SetSongTempo

Callbacks

LoadAudioData

This callback will be called by Tad_Process when the common audio data or song data need to be loaded into Audio-RAM.

This subroutine is responsible for determining if the input is a valid song. If the input is common audio data (A = 0) this function MUST return data with carry set.

This subroutine can be generated by tad-compiler. Alternatively, the developer can create their own LoadAudioData subroutine if they wish to use their own resources subsystem or want to use compressed song data.

The data is allowed to cross bank boundaries. When the data crosses a bank boundary, data_ptr is advanced to start of the next bank (as determined by the bank byte and memory map).

Called with JSL long addressing (return with RTL).

INPUT:

  • A = 0 - load common audio data (This function MUST return carry set with a valid address/size if A = 0)
  • A >= 1 - load song data

OUTPUT:

  • Carry set if input (A) was valid
  • A:X = far address (only read if carry set)
  • Y = data size (only read if carry set)

LIFETIME:

  • The data MUST remain in memory while it is being transferred to Audio-RAM (which may take several frames).
  • The data can be freed on the next LoadAudioData call.
  • The data can be freed when the state changes to PAUSED or PLAYING.
  • The data can be freed if the Tad_IsLoaderActive function returns false.
  • The Tad_FinishLoadingData subroutine can be used to flush decompressed memory into audio-RAM. The data can be freed immediately after a Tad_FinishLoadingData call.

This function MUST NOT call Tad_Process or Tad_LoadSong. It is allowed to call Tad_FinishLoadingData.

tad-compiler output

tad-compiler ca65-enums will output an include file that contains:

  • Song enum
  • SoundEffect enum

tad-compiler will have also an option to output a single binary file and generate the LoadAudioData callback subroutine.

tad-compiler ca65-export will compile the entire project and output it to an uncompressed binary file, along with an assembly file that contains:

  • .incbin statements to load the binary file into the ROM
  • LoadAudioData callback
  • link-time .assert statements to ensure the memory map is correct

Alternatively, the developer can create their own LoadAudioData callback subroutine and populate the ROM with the output of the tad-compiler common and tad-compiler song commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment