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 MKPerformers 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 MKPerformers 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. |
The MusicKit MKPerformers 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 MKPerformer is activated while a performance is in progress, the MKPerformer activate method adds its nextPerform to the current MKConductor time to determine the time of the first invocation of the MKPerformer's perform method. Thus, the protocol described above would seem to introduce an undesired offset equal to the MKConductor's current time. Therfore, the activate method makes a special case of MKPerformers that are managed by a MIDI Time Code Conductor― it does not add in the MKConductor's time when activating such MKPerformers. The distinction between the two interpretations of nextPerform is an historical artifact. |
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 MKPerformers using that MKConductor.
The MKSynthInstrument method allNotesOffsends a noteOff: message to all running MKSynthPatches.
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 MKSynthInstruments 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];