Synchronizing to Incoming MIDI Time Code

Overview

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.

Adding a Time Offset to Incoming MIDI Time Code

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];

Finding Out What's Going On With MTC

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.

Important

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.

Synchronizing Your Own MKPerformer Subclass to Incoming MIDI Time Code

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:

  1. 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.

  2. 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.

  3. 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.

Tip

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.

Specifying Tempo

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.

Using the Conductor's Tempo Protocol

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.

Using the Conductor's 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.

Other Useful Methods

To round out the support for MIDI time code, the MusicKit provides the following support methods:

Debugging MIDI Time Code

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.

Restrictions

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];

Obsolete methods

The MKConductor method predictTime: should no longer be overridden as a way to control tempo. Use the tempo protocols described above.