MKConductor
ClassThe MKConductor
class defines the
mechanism that controls the timing of a MusicKit performance. This
control is divided between the class object and instances of
MKConductor
:
The MKConductor
class
itself represents an entire MusicKit
performance. The class methods perform global operations such as
setting characteristics that apply to all
MKConductor
instances, and starting and
stopping a performance.
Each MKConductor
instance embodies a message request queue, a list
of messages that are to be sent to particular objects at specific
times. Most of the instance methods are designed to affect a
MKConductor
's queue in some way. The most
commonly invoked of these are the methods that enqueue message
requests, and those that determine how quickly a
MKConductor
processes the requests in its queue
(in other words, the MKConductor
's
tempo).
MKConductor
s Created by the
MusicKitThe MusicKit automatically creates two
MKConductor
instances for you, the
clockConductor and the
defaultConductor:
As its name implies, the
clockConductor acts as a clock: It ticks away at
a steady and immutable 60.0 beats per minute. Any timing information
that's reckoned by the other MKConductor
instances is computed in reference to the clockConductor.
Many applications need only a single
MKConductor
instance (in addition to the
clockConductor); the
defaultConductor is created as a convenience to
meet this need. The defaultConductor is more
pliable than the clockConductor in that its tempo
can be altered and its activities can be temporarily suspended during
a performance.
The clockConductor is retrieved by sending the clockConductor message to the
MKConductor
class; similarly, defaultConductor retrieves the
defaultConductor.
Every instance of MKConductor
(this
includes the clockConductor and
defaultConductor) maintains a message request
queue. This queue consists of a list of structures, each of which
encapsulates a request for a message to be sent to some object. Every
request is given a timestamp that indicates when its message should be
sent. The requests in a message request queue are sorted according to
these timestamps. When a performance starts (through the startPerformance class method), the
MKConductor
instances begin processing their
message queues, sending the requested messages at the appropriate
times.
The structures in the message request queues are of type MKMsgStruct. All the fields of this structure are private: You can examine them, but you should never alter their values directly. Detailed knowledge of the MKMsgStruct isn't necessary. The structure is defined without further explanation in the file MusicKit/MKConductor.h. |
To enqueue a message request with a
MKConductor
, you invoke the sel:to:atTime:argCount: or sel:to:withDelay:argCount: method. The
arguments to these methods are similar:
Table 6-3. MKConductor
's message request arguments
Keyword | Argument |
---|---|
sel: | Selector that identifies the method you wish to invoke. |
to: | The object that implements the desired method. |
atTime: or withDelay: | The time at which you wish the method to be invoked. |
argCount: | The number of method arguments, followed by the arguments themselves, separated by commas. |
The difference between the two methods is the manner in which the time argument is interpreted. A message request enqueued through the ...atTime:... method is sent at the specified time measured from the beginning of the performance. If you use the ...withDelay:... method, the requested message is sent after the specified amount of time has elapsed since the sel:to:withDelay:argCount: method itself was invoked (given that a performance is in progress). Invoked before a performance begins, the two methods are identical.
Once you've made a message request through one of these methods, you can't rescind the action; if you need more control over message requests―for example, if you need to be able to reschedule or to remove a request―you should use the following C functions:
MKNewMsgRequest(double time, SEL selector, id receiver, int argCount, ...) creates a new MKMsgStruct structure and returns a pointer to it. The arguments are similar, although in a different order, to those of the sel:to:atTime:argCount: method.
MKScheduleMsgRequest(MKMsgStruct*aMsgStructPtr,id conductor) places the structure pointed to by
aMsgStructPtr, which was previously created
through MKNewMsgRequest()
, in
conductor's message request queue.
MKRepositionMsgRequest(MKMsgStruct*aMsgStructPtr, double time) repositions a message request within a
MKConductor
's queue. The value of the
time argument is absolute: It indicates the
request's new position as the number of beats since the beginning of
the performance.
MKCancelMsgRequest(MKMsgStruct*aMsgStructPtr) removes a message request.
The MKConductor
class provides two
special message request queues, one that contains messages that are
sent at the beginning of a performance and another for messages that
are sent after a performance ends. The class methods beforePerformanceSel:to:argCount: and afterPerformanceSel:to:argCount: enqueue
message requests in the before- and after-performance queues,
respectively.
As previously mentioned, a MusicKit performance starts when the
MKConductor
class receives the startPerformance message. This starts the
MKConductor
's clock ticking (as represented by
the clockConductor). If you're synthesizing music on the DSP or
sending messages to an external MIDI synthesizer, you should send the
run message to the
MKOrchestra
class or to your
MKMidi
object at virtually the same time that
you invoke startPerformance:
/* Start MKMidi, the DSP, and the performance at the same time. */ [aMidi run]; /* assuming aMidi was previously created */ [MKOrchestra run]; [MKConductor startPerformance]; |
When it receives startPerformance, the
MKConductor
class sends the messages in its
before-performance queue and then the
MKConductor
instances start processing their
individual message request queues. As a message is sent, the request
that prompted the message is removed from its queue. The performance
ends when the MKConductor
class receives
finishPerformance, at which time the
after-performance messages are sent. Any message requests that remain
in the individual MKConductor
s' message request
queues are removed. Note, however, that the before-performance queue
isn't cleared. If you invoke beforePerformanceSel:to:argCount: during a
performance, the message request will survive a subsequent finishPerformance and will affect the next
performance.
By default, if all the MKConductor
s'
queues become empty at the same time (not including the before- and
after-performance queues), finishPerformance is invoked automatically.
This is convenient if you're performing a
MKPart
or a MKScore
and
you want the performance to end when all the
MKNote
s have been played. However, for many
applications, such as those that create and perform
MKNote
s in response to a user's actions,
universally empty queues aren't necessarily an indication that the
performance is over. To allow a performance to continue even if all
the queues are empty, send setFinishWhenEmpty:NO to the MKConductor
class.
While a performance is in progress, you can pause all
MKConductor
's by sending pausePerformance to the
MKConductor
class. A paused performance is
resumed through the resumePerformance
method. Individual MKConductor
objects can be
paused and resumed through the pause
and resume methods.
The MKConductor
supports two alternative
protocols for tempo-management. The simpler of the two is called the
"Tempo Protocol". For more complex applications, the "Time Map
Protocol" is provided. The MKConductor
decides
which protocol to use based on what the delegate implements. If the
delegate implements beatToClock:from:
and clockToBeat:from: then the Time
Map Protocol is used. Otherwise, the Tempo Protocol is used.
With this protocol, a MKConductor
's tempo
controls the rate with which it processes the requests in its message
request queue. Two methods are provided for setting a
MKConductor
object's tempo:
setTempo:, which takes a double argument, sets the tempo in beats-per-minute.
setBeatSize: also takes a double, but it sets the tempo by defining the duration, in seconds, of a single beat.
Regardless of which method you use to set the tempo, the values returned by the retrieval methods tempo and beatSize are computed appropriately, as shown in the following example:
double bSize; /* Sets the defaultConductor's tempo. */ [[MKConductor defaultConductor] setTempo: 240.0]; /* Return the beat size; bSize will be 60.0/240.0, or 0.25. */ bSize = [[MKConductor defaultConductor] beatSize]; |
You can change a MKConductor
's tempo at
any time, even during a performance. If your application requires
multiple simultaneous tempi, you need to create more than one
MKConductor
, one for each tempo. A
MKConductor
's tempo is initialized to 60.0
beats per minute.
While the Tempo Protocol is fine for simply adding a tempo slider to a performance, you are better off using the Time Map Protocol if you want to apply a tempo track (a planned series of tempo changes) or if you are applying varying tempo changes in the context of a performance that is synchronized to MIDI time code.
A Time Map is a mapping from "beat time" (time as read by a human conductor reading a score) to "clock time" (the resulting time as performed by the conductor, after he has made any tempo modifications.) A few examples will help clarify this:
f(t) = t /* steady tempo of 60 bpm. */ f(t) = 2 * t /* steady tempo of 30 bpm. */ f(t) = 0.5 * t /* steady tempo of 120 bpm. */ f(t) = t ^ 2 /* tempo that continually slows down. */ f(t) = t ^ 0.5 /* tempo that continually speeds up. */ f(t) = t + sin(t) * .01 /* tempo that cyclically speeds up and slows down. */ |
For more information on Time Maps see the following reference:
Ensemble Timing in Computer Music. David A. Jaffe.
1985. Computer Music Journal, MIT Press, 9(4):38-48.
To implement the Time Map Protocol (an informal protocol), the delegate must implement two methods:
beatToClock:from:
takes two arguments. The first is a double that represents the current beat number.
The second argument is the MKConductor
that
sends the message. This delegate's implementation should return the
corresponding "clock time", the time after the tempo adjustments have
been made. In other words, this method implements the time
map.
clockToBeat:from: is the opposite of beatToClock:from:. It maps a "clock time" value to a beat number. In other words, this method implements the inverse time map.
There are a number of restrictions these methods must follow:
It is essential that these two methods are complementary. In particular, the following must be true:
t == [delegate beatToClock: [delegate clockToBeat: t from:aCond] from: aCond] |
Neither method should cause time to go backwards. That is, if t1 is less than t2, then [delegate beatToClock:t1 from:aCond] must be less than [delegate beatToClock:t2 from:aCond]. The same applies to clockToBeat:. In other words, both functions must be monotonically increasing.
These methods must not change over the course of a performance. For example, if the message [delegate beatToClock: 3. 1 from:aCond] returns 2.8, it must always return 2.8, whether invoked at the start, middle or end of the performance.
Note that while the Time Map Protocol makes tempo maps possible,
it is up to the application to apply such a map. For example, when a
Standard MIDI file is read into a
MKScore
object, the first part gets the tempo
map. The MusicKit does not apply this map
by default―this responsibility belongs to the
application.
See for example setMidifilesEvaluateTempo:
of |
Every MKConductor
instance has a notion
of the current time, measured in beats. This notion is updated by the
MKConductor
class only when a message from one
of the request queues is sent; all
MKConductor
s are updated when
any MKConductor
sends such
a message. If your application sends a message (or calls a C
function) that depends on a MKConductor
's
notion of time being current, you must first send lockPerformance to the
MKConductor
class. Every invocation of
lockPerformance should be balanced by
an invocation of unlockPerformance.
For example, if you send receiveNote:
to an MKInstrument
's
MKNoteReceiver
, you must bracket the message
with lockPerformance and unlockPerformance. (However, invocations of
receiveNote: that are requested through a
MKConductor
's messge request queue shouldn't be
bracketed by these methods.)
MKConductor
s and MKNote
sEvery MKNote
is associated with a
MKConductor
object. A
MKNote
's MKConductor
is
determined as follows: If the MKNote
is being
sent by a MKPerformer
during a performance, its
MKConductor
is that of the
MKPerformer
. If not, the
MKConductor
is the one set with
MKNote
's setConductor method. If no
MKConductor
was set, the
defaultConductor is used.
The association between a MKNote
and a
MKConductor
is of particular importance if the
MKNote
is a noteDur that's sent to a
MKSynthInstrument
or
MKMidi
object. Both of these
MKInstrument
s split a noteDur into a
noteOn/noteOff pair. The noteOn is realized immediately and the
noteOff is scheduled for realization at a later time, as indicated by
the original MKNote
's duration value. To do
this, a request for the noteOff to be sent in a receiveNote: message is enqueued with the
MKNote
's MKConductor
.
The exact time at which the MKNote
arrives
depends, therefore, on this MKConductor
's
tempo.
A MKNote
's
MKConductor
is also important if you send the
MKNote
to an
MKInstrument
through
MKNoteReceiver
's receiveNote:atTime: or receiveNote:withDelay: methods. These methods
cause the MKNoteReceiver
to enqueue a receiveNote: request with the
MKNote
's MKConductor
at
the specified time: The former method takes the atTime: argument as an absolute measure from
the beginning of the performance, while the latter measures the
withDelay: argument as some number of
beats from the time that it's invoked.
MKConductor
s and MKEnvelope
sThe relationship between an MKEnvelope
and a MKConductor
is as important as it is
unique: The dispatching of an MKEnvelope
's
breakpoints during DSP synthesis is always done through message
requests with the clockConductor. You don't have to do anything to
obtain this behavior, it happens automatically through MKUpdateAsymp()
, the function that you use in
the design of a MKSynthPatch
subclass to apply
an MKEnvelope
to a synthesis patch.
This association is particularly important not for the
particular MKConductor
with which the
breakpoints are scheduled, but that they are scheduled at all. Since
the clockConductor handles breakpoint dispatching, this means that its
queue may be filled with breakpoint messages without you knowing it.
As a result, if you set the performance to finish when the queues are
empty, the performance won't finish until all the breakpoint messages
are sent from the clockConductor's queue. This is generally desirable
behavior. Where things can become confusing is if you pause an entire
performance (through MKConductor
's pausePerformance class method) while
MKEnvelope
s are being handled. Not only will all
MKNote
handling stop, all MKEnvelope
s will
freeze as well. This usually isn't pleasant.
One way to avoid the problem is to pause all your
MKConductor
objects, through the pause method, rather than pause the entire
performance.
A MKConductor
object can be made to
synchronize to MIDI time code. For further
information, see the MKMidi
and
MKConductor
class descriptions, as well as
Appendix B.