ThreadSafe Note Scheduling Code (Was Re: Threading)
ThreadSafe Note Scheduling Code (Was Re: Threading)
- Subject: ThreadSafe Note Scheduling Code (Was Re: Threading)
- From: Bill Stewart <email@hidden>
- Date: Thu, 7 Aug 2003 18:41:40 -0700
OK - So this code is kinda pulled out of context - but it gives the
general idea: (Ideally we really would like to have a more complete
abstract class for people to implement synths - but we don't just yet!)
Basically, the idea is this:
If you call the APIs of the MusicDevice to start or stop a note, then
those API's should be sensitive to the thread upon which they are being
called:
(1) If you are calling these on the render thread, then you can
schedule the new note (or turn one off) straight away
(2) If you are calling these from any other thread, then these note
events have to be queued (and then when it next renders, it is
de-queued and applied as part of the job of rendering notes)
(The API's affected by this are MusicDeviceStartNote,
MusicDeviceStopNote, MusicDeviceMIDIEvent, where the MIDI event is a
note-on or off)
Thus:
In your Render call, you get the thread ID (pthread_self), of the
thread that you are being asked to render on.
In your API calls for starting and stopping notes you compare that
thread ID with the thread ID you are being called on to start/stop a
note, If it is the render thread, then you go through and do the work
immediately, if not, you queue a start/stop note
We have a NoteScheduler class that we use in the DLS Synth that, well,
schedules notes:)... So, the client of that class (the AUBase derived
SynthInstance), has the responsibility to determine if the note is
being scheduled from the render or another thread) - based on that
decision, the note is either queued or scheduled immediately.
(Basically, and I've added this to the MusicDeviceBase and MIDIBase
classes - where the MusicDeviceStartNote API checks for a zero
velocity, and dispatches accordingly to StartNote or StopNote, and the
MIDIBase class I think already dispatched to HandleNoteOn (non-zero
velocity) and HandleNoteOff)
HandleNoteOn and Off translate the MIDI message into the extended
parlance and dispatch to another call ExtendedNoteOn/Off (which
StartNote/StopNote do as well)
We also break up the notes into the two events (note on, note off) -
and share a structure for dealing with both of these types of events:
When you are asked to render you de-queue any start/stop note requests.
(An Implementation note - we factor ALL of the Start and Stop note and
MIDI Commands for Starting and Stopping notes to a single method -
SynthInstance::ExtendedNoteOn/Off - which makes the calls listed below)
This call takes the extended semantic of the Start Note call, but as
described in previous emails, MIDI notes can be fully expressed within
this extended API.
OK - enough preamble - here is the relevant code.
struct ExtendedNoteParams
{
enum {
kUnused = 0,
kNoteOn = 1,
kNoteOff = 2
};
UInt32 instrumentNumber;
UInt32 groupID;
long scheduledSampleFrames;
Float32 noteNumber;
Float32 velocity;
NoteInstanceID instanceID;
UInt32 eventType;
};
Then we define an array of these that will be used for queueing notes
events (hereafter notes):
enum {kMaxQueueEvents = 512};
ExtendedNoteParams mNoteOnQueue[kMaxQueueEvents];
int mQueueReadIndex;
int mQueueWriteIndex;
The methods that queue the notes manipulate the write index. The method
that dequeues the notes manipulate the read index - so without locking
there is a high degree of thread-safeness - and we're only guaranteeing
the operation of this with *one* additional queueing thread (we don't
guard against 2 thread trying to simultaneously write to the queue
So - what do the queue methods look like:
This guy finds the first empty slot to write too:
ExtendedNoteParams* NoteScheduler::FindUnusedQueueSlot ()
{
// find an available slot
ExtendedNoteParams *params = &mNoteOnQueue[mQueueWriteIndex];
if(params->eventType != ExtendedNoteParams::kUnused )
{
params = NULL;
}
return params;
}
Its used by both the queue calls for a note on and a note off (I'm
including both here for completeness):
(The GetNewNoteID will generate a noteID based on whether the note is a
float or an int)
// the args here represents the union of what we need for both the ext
and midi flavours of a note on (and off below)
void
NoteScheduler::QueueExtendedNoteOn( UInt32 inInstrumentNumber,
UInt32 inGroupID,
NoteInstanceID &outNoteInstanceID,
long inScheduledSampleFrame,
const MusicDeviceNoteParams &inParams)
{
ExtendedNoteParams* ptr = FindUnusedQueueSlot();
if (ptr)
{
ExtendedNoteParams ¶ms = *ptr;
params.eventType = ExtendedNoteParams::kNoteOn;
// if note-on, then generate instanceID here, so later it will get
passed
// into ExtendedNoteOn() (via ScheduledQueuedEvents() )
Float32 note = inParams.mPitch;
outNoteInstanceID = Note::GetNewNoteID(note);
params.instrumentNumber = inInstrumentNumber;
params.groupID = inGroupID;
params.scheduledSampleFrames = inScheduledSampleFrame;
params.noteNumber = note;
params.velocity = inParams.mVelocity;
params.instanceID = outNoteInstanceID;
// increment write index last!
mQueueWriteIndex = (mQueueWriteIndex + 1) % kMaxQueueEvents;
}
}
void NoteScheduler::QueueExtendedNoteOff(UInt32 inGroupID,
NoteInstanceID inNoteInstanceID,
long inScheduledSampleFrame)
{
ExtendedNoteParams* ptr = FindUnusedQueueSlot();
if (ptr)
{
ExtendedNoteParams ¶ms = *ptr;
params.eventType = ExtendedNoteParams::kNoteOff;
params.groupID = inGroupID;
params.instanceID = inNoteInstanceID;
params.scheduledSampleFrames = inScheduledSampleFrame;
// increment write index last!
mQueueWriteIndex = (mQueueWriteIndex + 1) % kMaxQueueEvents;
}
}
OK - so that is the queueing part.
When the scheduler renders, it first checks to see if there is anything
to dequeue
void NoteScheduler::ScheduleQueuedEvents()
{
while(mQueueReadIndex != mQueueWriteIndex )
{
ExtendedNoteParams ¶ms = mNoteOnQueue[mQueueReadIndex ];
// There are two cases:
// note on => EventType is kNoteOn
// note off => EventType is kNoteOff
if (params.eventType == ExtendedNoteParams::kNoteOn)
{
MusicDeviceNoteParams noteParams;
noteParams.argCount = 2;
noteParams.mPitch = params.noteNumber;
noteParams.mVelocity = params.velocity;
NoteInstanceID noteID;
ExtendedNoteOn( params.instrumentNumber,
params.groupID,
noteID, //it will be ignored in this case (because the queueing
above has already generated it)
params.scheduledSampleFrames,
noteParams,
params.instanceID);
}
else
{
// is note off
ExtendedNoteOff (params.groupID, params.instanceID,
params.scheduledSampleFrames);
}
mNoteOnQueue[mQueueReadIndex ].eventType =
ExtendedNoteParams::kUnused; // free this slot
mQueueReadIndex = (mQueueReadIndex + 1) % kMaxQueueEvents;
}
}
The one subtlety about this is that when you are queueing you already
will have had to generate the unique note instance ID - so when
dequeueing, the params.instanceID will already have a value that
ExtendedNoteOn will see and understand. When the SynthInstance calls
ExtendedNoteOn itself (ie. it doesn't have to queue), then the value of
this last argument will default to -1, so we know that there hasn't
been a note instance ID generated yet, and ExtNoteOn will generate one.
That's it...
Hope that helps
Bill
--
mailto:email@hidden
tel: +1 408 974 4056
________________________________________________________________________
__
"Much human ingenuity has gone into finding the ultimate Before.
The current state of knowledge can be summarized thus:
In the beginning, there was nothing, which exploded" - Terry Pratchett
________________________________________________________________________
__
_______________________________________________
coreaudio-api mailing list | email@hidden
Help/Unsubscribe/Archives:
http://www.lists.apple.com/mailman/listinfo/coreaudio-api
Do not post admin requests to the list. They will be ignored.