To synchronize a MusicKit performance to incoming MIDI Time
Code, send a conductor the message setMTCSynch:, with a
MKMidi
object as the argument. The
MKMidi
object passed to this method corresponds
to the MIDI device to which the user will be sending MIDI time code.
You must send setMTCSynch: before the
performance has started, i.e. before +startPerformance is sent. You must also assign
your conductor as the conductor for any
MKPerformer
s that you would like to synchronize
to time code.
Once you start the
performance, the MIDI time code MKConductor
waits until time code
starts running. It then tells its active performers the current time,
reactivates them, and begins conducting them.
Here is an example of a simple performance with one
MKPartPerformer
that is synchronized to MIDI
time code:
MKMidi *aMidi = [MKMidi midiOnDevice: @"midi0"]; MKPartPerformer *aPartPerf = [[MKPartPerformer alloc] init]; MKConductor *aCond = [MKConductor defaultConductor]; [aMidi open]; [aCond setMTCSynch:aMidi]; [aPartPerf setConductor:aCond]; [aPartPerf activate]; [aMidi run]; [MKConductor startPerformance]; |
In this example, the default
MKConductor
is used. Any MKConductor
, with the exception of the
clockConductor, may be made to synchronize with MIDI time code.
Currently, only one MKConductor
at a time may be in that
mode. If you send a second MKConductor
the setMTCSynch: message, it steals the role of MTC
Conductor from the MKConductor
that previously received the setMTCSynch: message. Thus, there can only be
one MIDI time code source.
MIDI Time
Code provides an absolute time base. The MusicKit allows you to
shift that time base using the MKConductor
method setTimeOffset:. The offset you specify is
subtracted from the MIDI time code. All MusicKit time methods take
this offset into account. For example, if you would like a MIDI time
code time of 30 minutes to correspond to a MusicKit time of 1 minute,
you send:
[myConductor setTimeOffset:29 * 60]; |
If, on the other hand, you want a MIDI time code time of 0 to correspond to a MusicKit time of 1 minute, you send
[myConductor setTimeOffset:-60]; |
The MKConductor
sends its delegate messages reflecting the status of MIDI Time Code.
When time code starts the first time, the delegate receives the
message conductorWillSeek:,
indicating that the MKPerformer
s were reactivated, followed by
conductorDidSeek: and conductorDidResume:. If time code stops, the
MKConductor
sends its delegate the message conductorDidPause:. If time code starts again
in the same place, the delegate receives conductorDidResume:. If time code starts in a
different place, the delegate receives conductorWillSeek: and conductorDidSeek: as well. If time code jumps,
without stopping, the delegate receives conductorWillSeek: and conductorDidSeek:, but not conductorDidResume:, since it's already
running. Finally, if time code starts going backwards, the delegate
receives the message conductorDidReverse:.
For example, you might want to silence any sounding notes when
time code stops are seeks. The
MKSynthInstrument
allNotesOff and the
MKMidi
method allNotesOffIfNeeded are useful for this
purpose. Another typical use of the delegate methods is to inform the
user of the status of MIDI time code.
As usual, if the delegate does not respond to one of these
messages, the MKConductor
does not send that
message.
There is a known bug in the NeXTStep/OpenStep MIDI driver that sometimes causes it to spuriously report backward time code. The only work-around is to stop the SMPTE tape, wait a second, and then start it again. That should unwedge the driver. |
MKPerformer
Subclass to Incoming MIDI Time CodeThe MusicKit
MKPerformer
s all synchronize correctly to MIDI time code. If you make
your own MKPerformer
subclass synchronize, you need to support a
simple informal protocol called Time Code
Performer Protocol. There are three parts to this
protocol:
A Time Code MKPerformer
must implement a
method setFirstTimeTag:, which takes
a double argument, represnting the starting value
of MIDI time code in seconds. A common implementation of this method
stores the value it is passed in an instance variable. The
MKPerformer
class provides a default implementation, which does
nothing.
A Time Code MKPerformer
's activateSelf method must position itself at the
Note it wants to send at firstTimeTag. If there
is no Note for that time, it should position itself at the first Note
following that time. It then
sets its nextPerform instance variableto that Note's time (which will be greater than or equal to
firstTimeTag.) In other words, it sets
nextPerform to the first time it wants to run.
Finally, it returns self. If there
are no Notes to send after the specified time, it returns nil.
The first invocation of
a Time Code MKPerformer
's performmethod should send the selected Note, then choose the next
Note and set nextPerform to the time until that
Note, as usual. You can identify the first invocation because the
instance variable performCount will be set to 1.
In the first invocation of perform,
you may also want to send any noteUpdates that preceed
firstTimeTag. This makes sure that all
MKSynthInstrument
and MIDI controllers are up to date. (This is
sometimes called "chasing controller values" in MIDI parlance.)
Here is an example of a simple, but complete, Time Code
Perfomer. This example is a simplified version of the MusicKit
MKPartPerformer
:
#import <MusicKit/MusicKit.h> #import "MyPartPerformer.h" @implementation MyPartPerformer:MKPerformer { id part; /* Part over which we're sequencing. */ double firstTimeTag; /* Required by Time Code Protocol. */ int currentIndex; /* Index of nextNote */ } - initForPart:aPart { if (!aPart) return nil; [super init]; part = aPart; [self addNoteSender: [[MKNoteSender alloc] init]]; return self; } - setFirstTimeTag:(double)aTimeTag { firstTimeTag = aTimeTag; return self; } - activateSelf { int cnt MKNote *aNote; id noteList; double tTag = 0; BOOL success = NO; noteList = [part notesNoCopy]; cnt = [noteList count]; for (currentIndex=0; currentIndex < cnt; currentIndex++) { aNote = [noteList objectAt:currentIndex]; tTag = [aNote timeTag]; if (tTag >= firstTimeTag) { success = YES; break; } } if (!success) return nil; nextPerform = tTag; return self; } - perform { double t = [nextNote timeTag]; MKNote *aNote; NSArray *noteList = [part notesNoCopy]; if (performCount == 1 && (firstTimeTag > 0)) { /* Send all noteUpdates up to now */ int i,cnt; for (i = 0, cnt = [noteList count]; i<cnt; i++) { aNote = (MKNote *) [noteList objectAtIndex: i]; if ([aNote noteType] == MK_noteUpdate) [[self noteSender] sendNote: aNote]; } aNote = [noteList objectAtIndex: currentIndex++]; [[self noteSender] sendNote: aNote]; if (currentIndex == [aList count]) return [self deactivate]; else nextPerform = [[noteList objectAtIndex: currentIndex] timeTag] - t; return self; } |
Of course, any performer can be used with a MIDI time code conductor. However, unless you follow the Time Code Performer protocol described above, it will not seek as you might expect.
When a |
MIDI Time Code is not intended for conveying tempo. It is
simply a steady stream reporting the passage of time. If you want to
incorporate tempo into your performance, you have two options. You
can use the MKConductor
's Tempo
Protocol or you can implement the Time Map
Protocol (informal). You cannot use both for the same
MKConductor
. In particular, if you implement
the Time Map Protocol, the
MKConductor
setTempo: and setBeatSize: methods have no effect.
To use the MKConductor
's tempo in a MIDI
time code performance, simply set the MIDI time code
MKConductor
's tempo with the setTempo: method. Changing the tempo during
the course of a MIDI time code performance will work, but when the
time code seeks, the MKConductor
uses the
current tempo as that of the entire piece. It sets its new time based
on that value. For example, if the tempo is 120 beats per minute and
MIDI time code starts at time 30.0 seconds, the
MKConductor
sets its time to 60 beats.
For more complex situations, when you want a planned and predicatable tempo trajectory synchronized to the MIDI time code, you should implement the Time Map Protocol.
This
protocol relies on the MKConductor
's delegate to implement two methods
that specify the mapping between "beat time" and "clock time." For
more information, see Chapter 6.
To round out the support for MIDI time code, the MusicKit provides the following support methods:
The MKMidi
method
time returns the current time,
according to the MIDI driver, adjusted as follows:
If deltaT mode is
MK_DELTAT_SCHEDULER_ADVANCE, deltaT is added to
this time. If the receiver is providing time code for a
MKConductor
, that
MKConductor
's time offset is reflected in the
time returned by this method. A similar MKMidi
method, getMTCFormat:hours:min:sec:frames:, returns by
reference the current MIDI time code time, according to the MIDI
driver. This method only works if a MKMidi
object is open and acting as a source of MIDI time code. The time is
adjusted as with the time method.
Unlike most of the MusicKit time methods
and functions, both time and
getMTCFormat:hours:min:sec:frames:
get the current time, whether or not [MKConductor adjustTime] or
[MKConductor lockPerformance] was done.
The MKConductor
method
clockTime is a convenience method.
It returns a double representing the current clock time for the
object. If the object is a MIDI time code MKConductor
, this is the
current MIDI time code time, the same value returned by MKMidi
's
time method. If the object is not a
MIDI time code MKConductor
, it is the same as
the value returned by:
[[MKConductor clockConductor] time] |
The MKConductor
method MTCSynch returns the MKMidi
object previously
set with setMTCSynch:, if any, else
nil. Similarly,the MKMidi
method synchConductorreturns the MKConductor
object
managing MIDI time code, if any, else nil. Keep in mind that only one
MKConductor
at a time may have an MTCSynch
object.
The MKConductor
method activePerformers returns a NSArray
object of
active MKPerformer
s using that MKConductor
.
The
MKSynthInstrument
method allNotesOffsends a noteOff: message
to all running MKSynthPatch
es.
The MKMidi
allNotesOff method sends a noteOff MIDI message for any notes that
previously were turned on. In contrast, the method allNotesOffBlast sends a noteOff on every MIDI channel and key
number.
To help support MIDI time code, the
MKPartPerformer
method perform now
sends all noteUpdates up to the current time the first time it is
invoked. This makes sure that all MKSynthInstrument
s and MIDI
controllers have the proper values.
Calling the function MKSetTrace(MK_TRACEMIDI) causes messages to
be written to stderr whenever the time code
MKConductor
resumes, pauses, or seeks. Also,
it prints a message when time code "slips" in comparison to the system
clock. A negative slip means that time code is running faster than
the system clock. A positive slip means that time code is running
slower than the system clock. Note that slip messages are not printed
when time code seeks.
You cannot pause a performance in which a MIDI time code
MKConductor
is participating. An attempt to do
wo will be ignored. Similarly, you cannot pause a MIDI time code
MKConductor
. Unclocked performances involving
MIDI time code conductors are not supported. Hence, setMTCSynch: sends [MKConductor
setClocked:YES];
The MKConductor
method predictTime: should no longer be overridden as
a way to control tempo. Use the tempo protocols described
above.