- Furnace tracker file conversion
- WLA-DX and SDCC/SDAS support
- SN76489 PSG, YM2413 FM, and dual SN76489 support (Sega System E)
- Song playback
- Sound Effect playback
- only 1 SFX can be played back at a time
- only 1 channel can be included in the SFX
- using the queue system, SFX can be given a priority, so one SFX can override another if it's more important, or is ignored if it's not
- Queue system for playing back songs and SFX, so you only need to call
banjo_queue_song(song_number);
or dold a, song_number / ld (queue_song), a
and the next call tobanjo_update
will handle it.- this automatically changes bank 2 (0x8000 to 0xbfff) by writing to 0xffff
- When you want to handle banking and playback in a custom way, or integrate with other libraries, there are functions which can be used to play and update songs without the queue system
banjo_play_song
will start a song playing,banjo_update_song
can be called once a frame to update itbanjo_play_sfx
will start a sfx playing,banjo_update_sfx
can be called once a frame to update itbanjo_mute_song_channel
andbanjo_unmute_song_channel
can be used to mute song channels when playing back sfx with a different library
- Supports Song/SFX sizes up to 16kb (the song needs to be entirely in one bank)
NB: Compatibility Flags > Pitch/Playback > Pitch Linearity should be set to "None" in Furnace
- Volume macros (only "sequence" mode)
- YM2413 custom patches and custom drum pitches
- Effects:
- Arpeggios (00)
- Pitch slides up and down (01, 02)
- Portamento (03)
- Vibrato (04)
- SN76489 noise mode (20)
- Game Gear stereo panning (80)
This is a script to turn a furnace .fur file into a json representation
python furnace2json.py -o output.json input.fur
This is a script to turn the json output of furnace2json into a .c or .asm file
For songs, you use it like:
python json2sms.py -o output.asm -i song_name input.json
python json2sms.py -o output.c -i song_name input.json
"song_name" will then be used as the name of the variable in .c or label for the song in .asm
For sfx you use it with "-s" like:
python json2sms.py -o output.asm -i sfx_name -s 2 input.json
python json2sms.py -o output.asm -i sfx_name -s 2 input.json
SFX only run on one channel, specified by the -s parameter. So here "-s 2" the channel number which the sfx is in (zero indexed, so the first channel in the .fur is 0). Other channels will not be included in the export!
To make channel numbers line up properly with Song playback, your SFX .fur should also use the same chip and channel configuration as the Song .fur - i.e. if the Song uses both SN and FM, the SFX should also use both SN and FM so the channel numbers are the same in both cases.
For SDCC/SDAS, the .rel libraries are located in music_driver_sdas/lib Libraries with "_qo" in the filename don't include the queues for smaller footprint. Libraries which only use the SN76489 won't include code for the OPLL.
The major difference between the library files is that they reserve different amounts of space in RAM for song channels, to accommodate songs for different types of hardware. You should choose the library which provides enough space to handle all of the channels in your songs, for all supported configurations.
music_driver_ | FM | SN | SN_FM | DUAL_SN | FM_DRUMS | SN_FM_DRUMS |
---|---|---|---|---|---|---|
Largest song type supported | YM2413 | SN76489 | YM2413 + SN76489 | Dual SN76489 | YM2413 in percussion mode | SN76489 + YM2413 in percussion mode |
Channel count | 9 | 4 | 13 | 8 | 11 | 15 |
RAM used (bytes) | 288 | 128 | 416 | 256 | 352 | 480 |
As an example, music_driver_fm.rel has 9 channels so it has enough space to handle playback of FM, SN and Dual SN songs. If you used this to play back a song with FM percussion, it expects 11 channels worth of RAM (2 more than the alloted 9) so updates to the last two channels could spill over into RAM used for your software and cause havoc, so try to avoid doing that!
If you want to support multiple hardware configurations, you can use banjo_check_hardware();
or call banjo_check_hardware
to determine whether an FM Unit is present, and whether your software is running on a Game Gear in Game Gear mode. This call will set the banjo_fm_unit_present
and banjo_game_gear_mode
global variables.
After possibly using the outcome of hardware detection to decide which type of songs you'll be playing, you can then use banjo_init(MODE);
or ld a, MODE / call banjo_init
to intialise the sound driver. This initialises variables which are used by the driver, sets up pointers to channel data, and will switch the FM and SN sound generators on or off using the Audio Control Port (0xf2) depending on the mode. It also stores the mode in the banjo_mode
global variable.
Mode define | Value |
---|---|
MODE_FM | 1 |
MODE_SN | 2 |
MODE_SN_FM | 3 |
MODE_DUAL_SN | 4 |
MODE_FM_DRUMS | 5 |
MODE_SN_FM_DRUMS | 7 |
The numbers are organised so the bottom two bits can be isolated and sent to the Audio Control port (where bit 0 enables FM and bit 1 enables SN).
The sound driver uses tables of pointers to Song/SFX data. e.g. in C:
// References to Songs in other .c files which will be matched up at link time
extern song_data_t const title_screen_song;
extern song_data_t const town_song;
// Song table
// SONG_DEF(SONG_LABEL, SONG_BANK)
song_t const song_table[] = {
SONG_DEF(title_screen_song, 2),
SONG_DEF(town_song, 2),
};
The locations of these tables should be set with the banjo_set_song_table(song_table_ptr);
and banjo_set_sfx_table(sfx_table_ptr);
functions. Depending on what banjo_check_hardware
found you can decide which table to load (e.g. if no FM unit is present, use the SN song and sfx tables instead of the FM ones).
To queue up a song or sfx, you use banjo_queue_song(song);
or banjo_queue_song(sfx);
. The values passed to these should be the index of the song in the table. Using the above table in the C code as an example, banjo_queue_song(0);
would queue up the title_screen_song, banjo_queue_song(1);
the town_song.
If your software supports multiple hardware configurations, it would be best to have a Song and SFX table per hardware configuration.
The advantage of having separate tables for different hardware is that you could do something like this:
// do we have an fm unit? use the _fm table
if (banjo_fm_unit_present)
{
banjo_set_song_table(song_table_fm);
}
// otherwise use the _sn table
else
{
banjo_set_song_table(song_table_sn);
}
// queue title screen song
banjo_queue_song(0);
If for example both tables are set up so that song 0 is the title screen song (arranged specifically for each hardware configuration) you only need to set the tables once - all subsequent banjo_queue_
calls can be exactly the same.
The banjo_queue_
functions don't immediately start playback of the new SFX or Song - it happens in banjo_update()
:
banjo_update();
(or call banjo_update
) should be run at every vblank. It's better to run it after any code which updates the VDP, to give that code as much time in vblank as possible.
It's in this function that the queues are checked and playback of a new Song or SFX is started. For SFX, there is an added check of the SFX priority - SFX with lower priority won't start playing if an SFX with higher priority is already playing. If a Song or SFX isn't queued, it updates the ones which are currently playing.
banjo_update
will also change the bank for slot 2 (0x8000 - 0xbfff) so if you're relying on something being in bank 2 you'll have to change the bank afterwards.
The converted Song/SFX data is similar to the tracker data. Each Channel has an Order array which has an array of pointers to Patterns in the order they're to be played back.
A Pattern for a Channel is an array of bytes which map to commands. The end of a line is signified by a byte with bit 7 set, so anything >= 0x80 (LINE_WAIT). When this is reached it stops processing the channel's commands and waits for a variable number of Lines before processing begins again, with the number of lines to wait defined by the lower 7 bits. A list of commands in the pattern might look like:
INSTRUMENT_CHANGE, 3, ARPEGGIO, 87, NOTE_ON, 15, (LINE_END | 15), NOTE_OFF
Which would mean change the channel's instrument to 3, start an arpeggio that goes [0, 5, 7] (87 == 0x57), start playing note number 15, stop processing this line and wait for 15 lines, then stop playing the note.
// sets banjo_fm_unit_present to 1 if an fm unit is installed, 0 otherwise
// sets banjo_game_gear_mode to 1 if it's detected that wer're running on a game gear in game gear mode, 0 otherwise
void banjo_check_hardware(void);
// initialise banjo for the given mode
void banjo_init(unsigned char mode);
// Queues ON:
// handle song/sfx queues and update playing song/sfx
// this will change the bank for slot 2 (i.e. 0x8000 to 0xbfff (writes to mapper at 0xffff))
void banjo_update(void);
// queue song/sfx to be played back starting on next banjo_update
void banjo_queue_song(unsigned char song);
void banjo_queue_sfx(unsigned char sfx);
// set the loop mode for the song/sfx
// by default songs loop, but sfx don't
void banjo_queue_song_loop_mode(unsigned char loop);
void banjo_queue_sfx_loop_mode(unsigned char loop);
// set up pointers to the song and sfx tables
void banjo_set_song_table(const song_t *song_table_ptr);
void banjo_set_sfx_table(const song_t *sfx_table_ptr);
// Queues OFF:
// start playing song/sfx
// change to the song/sfx's bank before calling this
void banjo_play_song(const song_data_t *song_ptr, unsigned char loop_mode);
void banjo_play_sfx(const song_data_t *song_ptr, unsigned char loop_mode);
// update song/sfx if one is playing
// change to the song/sfx's bank before calling this
void banjo_update_song(void);
void banjo_update_sfx(void);
// Queues both ON and OFF:
// stop the currently playing song/sfx
void banjo_song_stop(void);
void banjo_sfx_stop(void);
// resume playback of a stopped song
void banjo_song_resume(void);
// mute the given song channel
void banjo_mute_song_channel(unsigned char chan);
// unmute the given song channel
// when handling your own banking and using opll fm with custom instruments
// change to the song's bank before calling this to properly restore the custom instrument patch
void banjo_unmute_song_channel(unsigned char chan);
// variables
// stores the mode after it's set in banjo_init
extern unsigned char banjo_mode;
// flags whether the FM unit is installed
extern unsigned char banjo_fm_unit_present;
// flags whether we're running on a Game Gear
extern unsigned char banjo_game_gear_mode;
// flags whether we're running on Sega System E
extern unsigned char banjo_system_e;
extern song_data_t song_state;
extern channel_t song_channels[];
extern song_data_t sfx_state;
extern channel_t sfx_channel;