Отправить устойчивые MIDI часы на Mac с CoreMIDI

Я работаю над небольшой программой, чтобы "перевести" дрожащие входящие MIDI-часы в устойчивый ритм. Дрожащие часы MIDI генерируют ужасный звук тремоло.

Идея состоит в том, чтобы "прослушать" входящие миди-часы и, после определения темпа, отправить устойчивые MIDI-часы на виртуальное устройство IAC, чтобы я мог синхронизировать свою DAW (NI Machine) с тем же устройством IAC. MIDI поступает от Korg Electribe, поэтому я застрял на MIDI-кабеле. Я использую Komplete Audio 6 для получения MIDI Clock.

Первая часть (прослушивание и определение темпа) уже рассмотрена, но теперь мне нужно генерировать устойчивые часы для этого темпа.

Я попытался использовать поток с высоким приоритетом для отправки миди-часов. Приведенная ниже процедура тестирования дает мне темп дрожания между 119,8 и 120,2.

Я сделал что-то не так в этой процедуре или я должен использовать другую стратегию? Любая помощь очень ценится.

С уважением, Роб

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
                                  });

ОБНОВИТЬ

Выяснили стратегию, которая работает. Код ниже дает идеальный результат в моей системе. Я уже использовал его на концерте с группой, и он работал нормально.

Решение для меня было:

  1. Отправка списка пакетов с 24 часами вместо отправки одного часа
  2. Установите только отметку времени с текущим временем обработки в самых первых часах, а затем просто продолжайте увеличивать отметку времени с вычисленным количеством тактов. (когда в каждом первом пакете списка пакетов установлено текущее время обработки, результат не был устойчивым!)
  3. Округлите рассчитанные тики в микросекунды! Это удивило меня, потому что вы подумали, что чем точнее результат, тем лучше результат, но когда я использую наносекундную точность, темп на экране моей DAW (NI Maschine) был стабильным, но все еще был "дрожащий" звук. Не знаю, связано ли это с CoreMidi, виртуальным устройством IAC или NI Machine.

Есть некоторые проблемы, когда темп меняется.. Изменения темпа не отсылаются гладко... но основной вопрос (как отправить устойчивые часы с CoreMidi) решен.

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;

}

ОБНОВИТЬ
Сделал некоторую прогрессию в ответ на изменения темпа.

  1. прекратить отправку списков пакетов, когда фиксированное значение MIDITimeStamp намного опережает mach_absolute_time()
  2. Отправка небольших списков пакетов только с 8 часами вместо 24

В моей системе изменения темпа передаются плавно и с минимальной задержкой, но небольшое смещение в такте отправляющего миди-устройства и DAW, прослушивающего сгенерированный MIDI-блок, может произойти после нескольких изменений темпа.

В живом исполнении это означало бы, что "барабанщик", использующий отправляющее миди-устройство, должен будет выполнить остановку и запуск на своем устройстве, чтобы снова получить синхронизированный звук. Для моей группы это не проблема. Внезапные остановки и пуски великолепны как эффект!

Ниже оптимизированный код. Я обернул это в классе для легкого использования. Пожалуйста, ответьте, если вы видите улучшения.

//
//  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

0 ответов

Другие вопросы по тегам