-
Notifications
You must be signed in to change notification settings - Fork 1
Quick Start
The purpose of this guide is to help you start using bquence as quickly as possible. As such, it does not detail setting up bquence and its dependencies for use in your project. Please refer to the full setup tutorial if you would like setup help.
If you're an experienced C++ developer who doesn't want the whole setup tutorial, go download miniaudio, QueueWorld, SoundTouch, and bquence, and create a new project including all of those libraries. (miniaudio, QueueWorld, and bquence are all included in source form - SoundTouch is the only library that should be linked externally).
If you would rather just read code than a tutorial, the quick-start example is available on GitHub.
The bq::World
class encapsulates a bquence sequencing session, along with all the other state bquence needs to maintain for proper functioning (e.g. file decoders and output settings). However, to facilitate maximum flexbility and a variety of use cases, bquence requires that you set up an audio device and a thread to host its I/O activity by yourself. This section of the tutorial will explain how to initialize all these components, using miniaudio to open an audio output device, and the standard C++ threading library to host I/O activity. (You're free to use whichever audio and threading libraries you prefer with bquence - miniaudio and std::thread
are simply used for simplicity in this example).
To begin with, include required libraries:
#include <iostream>
#include <thread>
#include <miniaudio.h>
#include <bqWorld.h>
The only include file necessary to use bquence is bqWorld.h
, which provides the bq::World
API encapsulating all of bquence's functionality. Other files are included above only for the sake of this example.
Next, you'll need to create audio and I/O thread callbacks. This will also involve establishing user data structures which will be passed to the callbacks by the audio and thread management libraries.
struct AudioUserData {
bq::World *world = nullptr;
};
struct IOUserData {
bq::World *world = nullptr;
bool running = false;
};
void audio_callback(ma_device *device, void *out_frames,
const void *in_frames, ma_uint32 num_frames)
{
AudioUserData *user_data = static_cast<AudioUserData *>(device->pUserData);
bq::World *world = user_data->world;
if (!world) {
return;
}
// Render audio
}
void io_callback(IOUserData *user_data)
{
while (user_data->running && user_data->world) {
user_data->world->pump_io_thread();
user_data->world->decode_chunks(0, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
In this example, the audio callback only needs access to the current bq::World
. The I/O callback also needs access to the world, but it needs an additional flag to indicate whether its message handling loop should continue running, since this thread will be joined to the main thread at the end of the program before the current world is destroyed. (This is the easiest way to clean up bquence world data without worrying about if the I/O thread will be accessing the world while it is destroyed).
The signature of the audio callback function is, in this case, defined by the miniaudio library. Thus, it is your responsibility to extract the user data structure from the device argument provided, and to locate the world in the user data structure. The commented section will soon be replaced by code to render a track/playhead combination's audio to the playback device.
The I/O callback is fairly self-explanatory - as long as the thread should be running and the world has been initialized (i.e. is not a null pointer), it repeatedly calls the IO engine's message handler, decodes all chunks necessary for the current active playhead/track combination (i.e. playhead 0 and track 0), and then sleeps for a short while (to avoid wasting CPU usage). Frequently calling pump_io_thread
and decode_chunks
with appropriate arguments is an essential part of bquence usage, since this maintains streaming data from the disk, and performing and pushing sequence edits to the audio thread.
A good (and crucial) rule of thumb: Whatever playhead/track pairs are passed to pull_audio
calls in the audio thread, should also be passed to decode_chunks
calls in the IO thread. The inverse also applies: whatever playhead/track pairs are not pulled in the audio thread, should not be decoded in the IO thread.
The last step in initialization is to create a main function which initializes the world, audio device, and I/O thread.
int main(int argc, char *argv[])
{
const ma_uint32 NUM_CHANNELS = 2;
const ma_uint32 SAMPLE_RATE = 44100;
AudioUserData audio_user_data;
IOUserData io_user_data;
ma_device_config device_cfg = ma_device_config_init(ma_device_type_playback);
device_cfg.playback.format = ma_format_f32;
device_cfg.playback.channels = NUM_CHANNELS;
device_cfg.sampleRate = SAMPLE_RATE;
device_cfg.dataCallback = audio_callback;
device_cfg.pUserData = &audio_user_data;
ma_device device;
if (ma_device_init(nullptr, &device_cfg, &device) != MA_SUCCESS) {
std::cerr << "Unable to open playback device" << std::endl;
return 1;
}
if (ma_device_start(&device) != MA_SUCCESS) {
std::cerr << "Unable to start playback device" << std::endl;
ma_device_uninit(&device);
return 1;
}
bq::World *world = new bq::World(NUM_CHANNELS, SAMPLE_RATE);
audio_user_data.world = world;
io_user_data.world = world;
io_user_data.running = true;
std::thread io_thread = std::thread(io_callback, &io_user_data);
// Edit sequence
// Wait for user input to exit the application
std::cout << "Press enter to quit" << std::endl;
std::string in_line;
std::getline(std::cin, in_line);
ma_device_uninit(&device);
io_user_data.running = false;
io_thread.join();
delete world;
return 0;
}
A large portion of this code sets up the miniaudio device output. There is only one line specific to bquence - that is:
bq::World *world = new bq::World(NUM_CHANNELS, SAMPLE_RATE);
It is important to pass into the bq::World
constructor a number of output channels and output sample rate that matches that of the open audio output device. These settings cannot be changed after the world constructor is called, so you'll need to destroy the current world and create a new one if you want to reconfigure these settings. Details on this process can be found in the how-to FAQ under the section titled Reconfigure a bquence session.
It is also critical to note the destruction process that takes place once the program ends - first the audio device is destroyed, then the I/O thread, then the bquence world. Always, before destroying a bquence world, first stop the audio callback and then the I/O thread. Otherwise, those threads might try to access the world while it is being destroyed, which would most likely cause memory exceptions or undefined behavior.
Once the world has been initialized, you can start editing each track's sequence. Replace the // Edit sequence
comment in the example main function with the following code:
unsigned int song_1_id = world->add_song("fastsong.mp3", 44100.0, 130.0);
unsigned int song_2_id = world->add_song("slowsong.mp3", 44100.0, 100.0);
world->insert_clip(0, 0.0, 8.0, 0.125, 0.125, 1, 0, song_1_id);
world->insert_clip(0, 8.0, 16.0, 0.125, 0.125, -1, 0, song_2_id);
world->erase_clips_range(0, 6.0, 10.0);
The add_song
function loads a source audio file into the world so that it can be referenced by clips. The code above loads two source audio files, one from fastsong.mp3
, which has a sample rate of 44100.0
and a tempo of 130.0
BPM, and one from slowsong.mp3
, which has a sample rate of 44100.0
and a tempo of 100.0
BPM. You can replace the song paths, sample rates, and tempos with those of actual audio files you have on your hard drive.
The insert_clip
function inserts a clip into a track's sequence. In this sample, a clip is inserted into the first track (index 0
) beginning at beat 0.0
and ending at beat 8.0
. It has a fade-in that lasts for 0.125
beats (i.e. one-eight of a beat) and a fade-out that lasts for 0.125
beats. These kinds of short fades are great for preventing popping artifacts at the beginning and end of clips, but bquence supports longer fades if you so desire. It's pitch is shifted up by 1.0
semitones, and it starts from frame 0
of the source audio file, which happens to be the first loaded song (song_1_id
). Afterwards, another clip is inserted into the first track, beginning at beat 8.0
and ending at beat 16.0
. It's pitch is shifted down by 1.0
semitones, and it starts from frame 0
of the source audio file, which in this case is the second loaded song (song_2_id
).
Finally, the erase_clips_range
function is used to remove the last two beats of the first clip (beats 6.0
through 8.0
) and the first two beats of the second clip (beats 8.0
through 10.0
).
The last step in this example is writing an audio callback that uses bquence to render audio from the world into the device's output buffer. Replace the // Render audio
comment in the example audio callback with the following code:
world->pump_audio_thread();
float *float_frames = static_cast<float *>(out_frames);
world->pull_audio(0, 0, float_frames, num_frames);
world->pull_done_advance_playhead(0, num_frames);
pump_audio_thread
updates the audio engine according to any messages remaining in its queue. This should be run once per audio callback before any other functions are called on the world object. Once all pending messages have been process, pull_audio
is called, in this case to pull num_frames
frames of rendered audio from the first playhead and the first track (both index 0
) to the buffer specified by float_frames
. Finally, the first playhead is advanced by num_frames
.
pull_audio
may be called up to once per audio callback for each playhead and track combination you want to render. pull_done_advance_playhead
should be called once all playhead and track combinations for the given playhead have been rendered. This prevents the playhead from advancing between track renders, which would put the tracks out of sync from each other.
You have finished the bquence quick-start tutorial! You now know all the concepts you need to begin using bquence in production. Nevertheless, you might still be wondering how to implement more complex behaviors (such as mixing tracks or synchronizing sequence data with a user interface) with these basic building blocks. If that's the case, the how-to FAQ should explain whatever scenario you have in mind.
Have fun with bquence! Feel free to report issues or improvements in our issue tracker - your feedback is greatly appreciated!