send steady midi clock on mac with CoreMIDI
Asked Answered
I

0

8

I am working on a small program to 'translate' a jittering incoming MIDI clock to a steady beat. The jittering MIDI clock generates an awful tremolo sound.

The idea is to 'listen' to the incoming midi clock and, after determining the tempo, to send a steady MIDI clock to the virtual IAC device so I can sync my DAW (NI Machine) to the same IAC device. Incomming MIDI is from a Korg Electribe so I am stuck to MIDI cable. I am using Komplete Audio 6 to receive the MIDI Clock.

The first part (listen and determine the tempo) is already covered but now I have to generate a steady clock for that tempo.

I tried to use a high priority thread to send the midi clock. The test routine below gives me a tempo jittering between 119.8 and 120.2.

Did I do something wrong in this routine or should I use anoter strategy? Any help is very appreciated.

regards, Rob

dispatch_source_t CreateDispatchTimer(uint64_t interval,
                                  uint64_t leeway,
                                  dispatch_queue_t queue,
                                  dispatch_block_t block)
{
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                                     0, 0, queue);
    if (timer)
    {
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, interval, leeway);
        dispatch_source_set_event_handler(timer, block);
        dispatch_resume(timer);
    }
    return timer;
}

- (void) testTimer{

    IAC = MIDIGetDestination(0); // 0 is the MAC IAC device on my system
    MIDIPacket       pkt;
    MIDIPacketList   l;
    pkt.timeStamp = 0;
    pkt.length = 1;
    pkt.data[0] = 0xF8;

    l.numPackets = 1;
    l.packet[0] = pkt;

    dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    aTimer = CreateDispatchTimer(20833 * NSEC_PER_SEC/1000000,    // 20.8333 ms will give me tempo 120
                                 0,
                                 q,
                                 ^{
                                     MIDISend(outPort, IAC, &l );  // outport was already created outside this code
                                  });

UPDATE

Figured out a strategy that works. The code below gives a perfect result on my system. I already used it on a gig with the band and it worked fine.

The solution for me was:

  1. Sending a packetlist with 24 clocks instead of sending a single clock
  2. Only set the timestamp with the current machtime in the very first clock and then just continue to increment the timestamp with the calculated number of ticks. (when the current machtime is set in each first packet of the packetlist, the result was not steady!)
  3. Round the calculated ticks to microseconds! This surprised me because you would think.. the more precision the beter result.. but when I use the nanosecond precision the tempo was steady in the screen of my DAW (NI Maschine) but there was still a 'jittering' sound. Don't know if this is related to CoreMidi, the virtual IAC device or NI Machine.

There are stil some issues when the tempo changes.. The tempo changes are not send out smooth ... but the basic question (how to send a steady clock with CoreMidi) is solved.

dispatch_source_t CreateDispatchTimer(uint64_t interval,
                                      uint64_t leeway,
                                      dispatch_queue_t queue,
                                      dispatch_block_t block)
{
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                                     0, 0, queue);
    if (timer)
    {
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, interval, leeway);
        dispatch_source_set_event_handler(timer, block);
        dispatch_resume(timer);
    }
    return timer;
}
- (void) timerTempo:(double) tempo{
    if (ignoreTempoChange) return; // ignoreTempoChange is set when a MIDI start is received

    _inTempo = tempo;
    if (aTimer)
    {
        nTicks = ticks_per_second / (tempo * 24 / 60);  //number of ticks for one beat.
        nTicks = nTicks/1000;
        nTicks = nTicks*1000;
        dispatch_source_set_timer(aTimer, DISPATCH_TIME_NOW, nTicks * 24, 0);
    }
}

- (void) startTimer:(double) tempo{

    _inTempo = tempo;
    mach_timebase_info_data_t mach_timebase_info_data_t;
    mach_timebase_info( &mach_timebase_info_data_t );  //denum and numer are always 1 on my system???
    ticks_per_second = mach_timebase_info_data_t.denom * NSEC_PER_SEC / mach_timebase_info_data_t.numer;

    nTicks = ticks_per_second / (tempo * 24 / 60);  //number of ticks for one beat.
    nTicks = nTicks/1000;
    nTicks = nTicks*1000;  // rounding the nTicks to microseconds was THE trick to get a rock solid clock in NI Maschine
    clocktTimeStamp = mach_absolute_time();
    dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    aTimer = CreateDispatchTimer(nTicks * 24,
                                 0,
                                 q,
                                 ^{
                                     const int packetListSize = sizeof(uint32)+ (25 *sizeof(MIDIPacket));
                                     MIDIPacketList *packetList= malloc(packetListSize);
                                     MIDIPacket *packet = MIDIPacketListInit( packetList );
                                     Byte clock = 0xF8;

                                     for( int i = 0; i < 24; i++ )
                                     {
                                         packet = MIDIPacketListAdd( packetList, packetListSize, packet, clocktTimeStamp, 1, &clock );
                                         clocktTimeStamp+= nTicks;
                                     }
                                     MIDISend(outPort, IAC, packetList );

                                     free(packetList);
                                  });
    timerStarted = true;

}

UPDATE
Made some progression on the response to tempo changes.

  1. stop sending packet lists when the fixed value of MIDITimeStamp is to far ahead of the mach_absolute_time()
  2. Sending smaller packet lists with only 8 clock's instead of 24

On my system the tempo changes are send out smooth and with a minimum of latency but a small offset in the beat of the sending midi device and the DAW listening to the generated MIDIclock might occur after multiple changes of the tempo.

In a live performance this would mean the 'drummer' using the sending midi device would have to perform a stop and a start on his to device to get the sound in sync again. For my band this is not an issue. Sudden stops and starts are great as an effect!

Below the optimised code. I wrapped it in a class for easy use. Please respond if you can see improvements.

//
//  MidiClockGenerator.h
//  MoxxxClock
//
//  Created by Rob Keeris on 17/05/15.
//  Copyright (c) 2015 Connector. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <CoreMIDI/CoreMIDI.h>

@interface MidiClockGenerator : NSObject

@property MIDIPortRef outPort;
@property MIDIEndpointRef destination;
@property (nonatomic, setter=setBPM:) float BPM;
@property (readonly) bool started;
@property int listSize;

- (id) initWithBPM:(float)BPM outPort:(MIDIPortRef) outPort destination:(MIDIEndpointRef) destination;
- (void) start;
- (void) stop;

@end

//
//  MidiClockGenerator.m
//  MoxxxClock
//
//  Created by Rob Keeris on 17/05/15.
//  Copyright (c) 2015 Connector. All rights reserved.
//    
#import "MidiClockGenerator.h"
#import <CoreMIDI/CoreMIDI.h>

@implementation MidiClockGenerator

dispatch_source_t timer;
uint64_t nTicks,bTicks,ticks_per_second;
MIDITimeStamp clockTimeStamp;

bool timerStarted;

dispatch_source_t CreateDispatchTimer(uint64_t interval,
                                      uint64_t leeway,
                                      dispatch_queue_t queue,
                                      dispatch_block_t block)
{
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                                     0, 0, queue);
    if (timer)
    {
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, interval, leeway);
        dispatch_source_set_event_handler(timer, block);
        dispatch_resume(timer);
    }
    return timer;
}

- (void) initTemo{
    nTicks = ticks_per_second / (_BPM * 24 / 60);  // number of ticks between clock's.
    nTicks = nTicks/100;  // round the nTicks to avoid 'jitter' in the sound
    nTicks = nTicks*100;
    bTicks = nTicks * _listSize;
}

- (void) setBPM:(float)BPM{
    _BPM = BPM;
    // calculate new values for nTicks and bTicks
    [self initTemo];
    // Set the interval of the timer to the new calculated bTicks
    if (timer)
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, bTicks, 0);
}

- (void) startTimer{

    [self initTemo];
    clockTimeStamp = mach_absolute_time();

    // default queu is good enough on my iMac.
    dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    timer = CreateDispatchTimer(bTicks,
                                0,
                                q,
                                ^{

                                    // avoid to much blocks send in the future to avoid latency in tempo changes
                                    // just skip on block when the clockTimeStamp is ahead of the mach_absolute_time()
                                    MIDITimeStamp now = mach_absolute_time();
                                    if (clockTimeStamp > now && (clockTimeStamp - now)/(bTicks) > 0) return;

                                    // setup packetlist
                                    Byte clock = 0xF8;
                                    uint32 packetListSize = sizeof(uint32)+ (_listSize *sizeof(MIDIPacket));
                                    MIDIPacketList *packetList= malloc((uint32)packetListSize);
                                    MIDIPacket *packet = MIDIPacketListInit( packetList );

                                    // Set the time stamps
                                    for( int i = 0; i < _listSize; i++ )
                                    {
                                        packet = MIDIPacketListAdd( packetList, packetListSize, packet, clockTimeStamp, 1, &clock );
                                        clockTimeStamp+= nTicks;
                                    }

                                    MIDISend(_outPort, _destination, packetList );
                                    free(packetList);
                                });
    _started = true;
}


- (id) init{
    return [self initWithBPM:0 outPort:0 destination:0];
}

- (id) initWithBPM:(float)BPM outPort:(MIDIPortRef) outPort destination:(MIDIEndpointRef) destination{
    self = [super init];
    if (self) {

        _listSize = 4;  // nr of clock's send in each packetlist. Should be big enough to deal with instability of the timer
                        // higher values will slowdown responce to tempochanges
        _outPort = outPort;
        _destination = destination;
        _BPM = BPM;

        // find out how many machtime ticks are in one second
        mach_timebase_info_data_t mach_timebase_info_data_t;
        mach_timebase_info( &mach_timebase_info_data_t );  //denum and numer are always 1 on my system???
        ticks_per_second = mach_timebase_info_data_t.denom * NSEC_PER_SEC / mach_timebase_info_data_t.numer;

        [self start];
    }
    return self;
}


- (void) start{
    if (_BPM > 0 && _outPort && _destination){
        if (!timer) {
            [self startTimer];
        } else {
            if (!_started) {
                dispatch_resume(timer);
                _started = true;
            }
        }
    }
}

- (void) stop{
    if (_started && timer){
        dispatch_suspend(timer);
        _started = false;
    }
}

@end
Industrials answered 12/5, 2015 at 23:40 Comment(2)
Thank you for your research on this. There isn't a lot of in-depth info on this topic out thereFormulate
You should answer your own question – I'd certainly give it an upvote!Hellen

© 2022 - 2024 — McMap. All rights reserved.