Thanks David, so if the sample rate was 44100, 16bit, that would be 44100
samples - each sample being 2 bytes. With a buffer size of 512 samples
(ie. 1024 bytes), it would be called 44100/512 approx 86 times per
second.
Yep, sounds right.
So… if I had a music player (sequencer) that needs to plays notes at 120
BPM, a quarter note “resolution” would be 480 quarter notes per minute, or
480/60 = 8 quarter notes per second. (For some reason this doesn’t sound
right to me - so please correct me if I’m wrong )
Actually, a “beat” is generally a quarter note, so that’s 120 quarter notes
per minute, or two beats per second.
The “480 per minute” would be 16ths - and 8 16ths per second sounds
reasonable for 120 BPM.
If I wanted to create a sequencer that would service new notes at a rate of
8 quarter notes per second, I’d either need to load new sample data within
the callback approx every 10 calls,
I think it’s a better idea to count audio samples rather than callbacks.
Buffer size is a configuration option that can potentially be set very high if
latency is not critical (such as in a plain music player, or during cut
scenes), so you don’t want that to have any impact on timing accuracy.
Also, looking at MIDI sequencers and MIDI (as in, the old 31250 bps wires) in
general, millisecond accuracy or better is desirable. I’d say sample accuracy
whenever possible!
or better still, have a seperate
thread that loads sample data into a buffer 8 times per second.
I would strongly recommend against that! It’s inefficient, inaccurate, and
hardly even works at all on general purpose operating systems. Do it all by
means of logic in the audio callback instead, and get reliabl sample accurate
timing for “free”.
This is how I handle it in ChipSound, the sound engine I use in Kobo II (some
"noise" removed) :On Wednesday 27 April 2011, at 22.35.02, “SparkyNZ” wrote:
static void cs_AudioCallback(void *ud, Uint8 *stream, int len)
{
CS_state *st = &cs_globalstate;
Sint16 *devicebuf = (Sint16 *)stream;
int remain = len / 4;
...
cs_ProcessMIDI(st);
while(remain)
{
unsigned frag = remain > CS_MAXFRAG ? CS_MAXFRAG : remain;
memset(masterbuf, 0, frag * sizeof(int));
cs_ProcessVoices(&st->groups[0], masterbuf, frag);
cs_ProcessMaster(st, masterbuf, devicebuf, frag);
devicebuf += frag * 2;
remain -= frag;
}
...
}
Basically, it processes CS_MAXFRAG samples at a time, until there is room for
less than CS_MAXFRAG frames, where it processes a “short” fragment to complete
the buffer. This way, I can set CS_MAXFRAG to whatever I like.
(And, I’ve set it rather low - 64 samples - to reduce CPU cache footprint.
This can be a major performance win, as most DSP code will run a lot faster
than the RAM can keep up with. Just don’t set it so low that entry/exit
overhead eats the profit.
Internally, cs_ProcessVoices() further subdivides fragments as needed (same
idea; you just use “frames until next event” instead of CS_MAXFRAG), allowing
everything to be sample or sub-sample accurate.
(Tech trivia: ChipSound is driven by a “microthreading” realtime scripting
engine that runs one thread per voice, controlling all parameters with sample
or sub-sample accuracy, sending messages and stuff. The language was really
intended for low level “chip style” sound programming only, but all Kobo II
sfx and music so far is written entirely in it, using a plain text editor.
Couldn’t resist the temptation to try it the 8-bit way! )
…and I suppose the best way to avoid changing the sample data when the
callback is being called would be double -buffer somehow and switch
between buffers once the sample data is committed to the buffer that the
callback would be using?
There is no need to bother with that, unless you have very good reasons to do
the actual processing outside the audio callback. Buffering and doing the work
in another thread can be a good idea when dealing with compressed streams with
large, fixed size chunks, or if you have a very complex sequencer that needs
dynamic memory management and stuff - but for a “normal” synth/sound/music
engine, I think it’s just complicating things and limiting your options. Real
time control options, more specifically; using “songs” as sound effects and
that sort of stuff.
BTW, ChipSound does use dynamic memory management, so I wouldn’t say that’s
a showstopper either. The scripting engine needs call stacks, voices are
allocated dynamically and so on… The objects that are actually allocated and
freed in the realtime context are preallocated and pooled using LIFO stacks.
It can refill the pools from the realtime context in case of emergency (which
usually works without causing drop-outs), but tuning the startup parameters
for the project at hand should keep that from ever happening in a finished
product.
–
//David Olofson - Consultant, Developer, Artist, Open Source Advocate
.— Games, examples, libraries, scripting, sound, music, graphics —.
| http://consulting.olofson.net http://olofsonarcade.com |
’---------------------------------------------------------------------’