Dynamic Looping with the BASS Audio API
I find it easiest to write about something I've done recently, so I'm going to share about the dynamic music system I made for Chickens 4 Cash on Monday. I'll first explain how it came about, then explain how it works, with some (hopefully) helpful pointers on how to leverage the BASS API for such a system. For the sake of keeping things simple, I'm going to explain this as though we're accessing the API directly, despite that it's actually wrapped in my own code.
A friend of mine, off whom I often bounce my game ideas, suggested putting together a dynamic music system for Chickens 4 Cash. Initially, I imagined it being the way it had been at Volition, with all kinds of awesome orchestral cues with smooth, dynamic transitions. With that in mind, my initial reaction was that the amount of work required was prohibitive, given the scope the game.
I had been working on music for a boss, and it became apparent, not only that the stages of the boss (because every good shmup boss has stages) each needed to have their own musical cue, but also that the level could potentially continue on indefinitely. So I got to work on a new plan for the system.
Keeping it Simple
With the notion that I needed to keep it simple, and some exploration into how my audio API worked, the Slonersoft dynamic music system became as follows...
- Music is split into "sets" which contain a series of loops which can be played back end-to-end.
- Each set has one contiguous audio file, within which we will jump around to make the loops work.
- This played best with the API, which supports seamlessly jumping around within a playing stream.
- My initial plan was to have separate files for each loop, but they didn't line up seamlessly in playback.
- One set is loaded & played at a time.
The Loop Data
- A name & name hash for the loop.
- A start & end time (floating point seconds in the XML, QWORD of bytes in code)
The Set Data
- A name & name hash for the set.
- The filename for the audio.
- A list of loops.
- A handle for the playing audio channel (the stream handle in the case of BASS streams).
- An index of the default loop that should play if the queue ends or if the set is asked to play with no loop index parameter.
Using the API
Getting the Times in Bytes
You'll need the start and end times of your loops in terms of bytes. To convert from time in seconds to time in bytes, you'll use the function:
QWORD BASS_ChannelSeconds2Bytes(DWORD handle, double pos);
...passing in the channel/stream handle and the time in seconds.
To get notified that you have reached the end of the current loop, you'll use the function BASS_ChannelSetSync function and pass it a callback of type SYNC_PROC *. To sync to a specific time, you'll call it like so:
HSYNC loopSync = BASS_ChannelSetSync(channelHandle, BASS_SYNC_POS|BASS_SYNC_MIXTIME, timeInBytes, syncCallback, 0);
If you want to sync to the end of the stream (which you'll want to do for the last loop in the set), you call it like so:
HSYNC loopSync = BASS_ChannelSetSync(channelHandle, BASS_SYNC_END|BASS_SYNC_MIXTIME, 0, syncCallback, 0);
This function will pass back a value of type HSYNC, which you should store so that before you change it to something else, you can clear it with:
Sending it Back
Once you've reached the end of your loop (as denoted by a call of the callback), you'll either want to send it to the beginning of the current loop, or to the beginning of another loop. You'll do this like so:
BASS_ChannelSetPosition(channelHandle, timeInBytes, BASS_POS_BYTE);
Queuing Up Music
Once you've got the music playing, you'll want to be able to queue up loops for the changes. For this, I made a linked list queue which contains the index of the loop to play, combined with the number of times we want it to play. In the case that I want it to play indefinitely until another loop comes along, I store -1 here.
So with the code-side system all put together, all it needs is a set of script hooks. I'm using Lua for my scripting, so I'll share the Lua function prototypes I have for this.
function music_play_set(set_name, loop_name = nil)...
- Play a specified music set.
- set_name (string): The name of the music set to play.
- loop_name (string, optional): The name of the loop to start with.
function music_play_loop(loop_name, loop_times = -1, immediate_switch = false)...
- Switch to a specified loop in the currently-loaded set.
- loop_name (string): the name of the loop to switch to.
- loop_times (integer, default -1): -1 to loop until another comes along, a positive number to loop a finite number of times.
- immediate_switch (bool, default false): True to immediately switch to this loop. False to enqueue it.
There were a couple "gotchas" surrounding how you load the file. I load my files with
HSTREAM BASS_StreamCreateFile(BOOL mem, const void *file, QWORD offset, QWORD length, DWORD flags);
but the example seems to make it appear that you can also use...
HMUSIC BASS_MusicLoad(BOOL mem, const void *file, QWORD offset, DWORD length, DWORD flags, DWORD freq);
which, in my brief attempt, I didn't get to work right.
I also noted that if you pass BASS_SAMPLE_LOOP into the flags field, it will never hit the end-of-file callback you set up with BASS_SYNC_END.