The 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).
The 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 MKConductors' 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 MKConductors' 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 MKNotes have been played. However, for many applications, such as those that create and perform MKNotes 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 MKScore. |
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 MKConductors 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.)
Every 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 MKInstruments 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.
The 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 MKEnvelopes are being handled. Not only will all MKNote handling stop, all MKEnvelopes 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.