Рассчитайте точный BPM из MIDI часов в ObjC с CoreMIDI
У меня возникли проблемы с вычислением точного BPM из принимаемых MIDI Clock (используя Ableton Live в моих тестах для отправки MIDI Clock).
Я использую CoreMIDI и PGMidi от Пита Гудлиффа.
В PGMidi lib есть метод, вызываемый при получении MIDI-сообщений. Из документа это происходит из высокоприоритетного фонового потока.
Вот моя текущая реализация для расчета BPM
double BPM;
double currentClockInterval;
uint64_t startClockTime;
- (void) midiSource:(PGMidiSource*)input midiReceived:(const MIDIPacketList *)packetList
{
[self onTick:nil];
MIDIPacket *packet = MIDIPacketListInit((MIDIPacketList*)packetList);
int statusByte = packet->data[0];
int status = statusByte >= 0xf0 ? statusByte : statusByte >> 4 << 4;
switch (status) {
case 0xb0: //cc
//NSLog(@"CC working!");
break;
case 0x90: // Note on, etc...
//NSLog(@"Note on/off working!");
break;
case 0xf8: // Clock tick
if (startClockTime != 0)
{
uint64_t currentClockTime = mach_absolute_time();
currentClockInterval = convertTimeInMilliseconds(currentClockTime - startClockTime);
BPM = (1000 / currentClockInterval / 24) * 60;
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"BPM: %f",BPM);
});
}
startClockTime = mach_absolute_time();
break;
}
}
uint64_t convertTimeInMilliseconds(uint64_t time)
{
const int64_t kOneMillion = 1000 * 1000;
static mach_timebase_info_data_t s_timebase_info;
if (s_timebase_info.denom == 0) {
(void) mach_timebase_info(&s_timebase_info);
}
// mach_absolute_time() returns billionth of seconds,
// so divide by one million to get milliseconds
return (uint64_t)((time * s_timebase_info.numer) / (kOneMillion * s_timebase_info.denom));
}
Но по некоторым причинам рассчитанный BPM не является точным. Когда я посылаю из Ableton Live BPM ниже 70, это нормально, но чем больше я посылаю более высокий BPM, тем менее точным является пример:
- Установка 69 ударов в минуту в Live дает мне 69,44444
- 100 -> 104.16666666
- 150 -> 156,250
- 255 -> 277,7777777
Может кто-нибудь помочь мне с этим? Я считаю, что я, вероятно, не использую хорошую стратегию для расчета BPM. То, что я сначала вычисляю, сколько времени прошло между каждыми миди-часами, используя mach_absolute_time().
Спасибо за вашу помощь!
ОБНОВИТЬ
После ответа Курта, вот гораздо более точная процедура, которая работает на iOS (поскольку я не использую CoreAudio/HostTime.h, который доступен только в OSX)
double currentClockTime;
double previousClockTime;
- (void) midiSource:(PGMidiSource*)input midiReceived:(const MIDIPacketList *)packetList
{
MIDIPacket *packet = (MIDIPacket*)&packetList->packet[0];
for (int i = 0; i < packetList->numPackets; ++i)
{
int statusByte = packet->data[0];
int status = statusByte >= 0xf0 ? statusByte : statusByte & 0xF0;
if(status == 0xf8)
{
previousClockTime = currentClockTime;
currentClockTime = packet->timeStamp;
if(previousClockTime > 0 && currentClockTime > 0)
{
double intervalInNanoseconds = convertTimeInNanoseconds(currentClockTime-previousClockTime);
BPM = (1000000 / intervalInNanoseconds / 24) * 60;
}
}
packet = MIDIPacketNext(packet);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"BPM: %f",BPM);
});
}
uint64_t convertTimeInNanoseconds(uint64_t time)
{
const int64_t kOneThousand = 1000;
static mach_timebase_info_data_t s_timebase_info;
if (s_timebase_info.denom == 0)
{
(void) mach_timebase_info(&s_timebase_info);
}
// mach_absolute_time() returns billionth of seconds,
// so divide by one thousand to get nanoseconds
return (uint64_t)((time * s_timebase_info.numer) / (kOneThousand * s_timebase_info.denom));
}
Как вы можете видеть, я теперь полагаюсь на метку времени MidiPacket вместо mach_absolute_time (), которая может быть отключена на непостоянную величину. Кроме того, вместо того, чтобы использовать миллисекунды для расчета BPM, я теперь использую наносекунды для большей точности.
С помощью этой процедуры я теперь получаю нечто гораздо более точное, НО оно все еще отключено на долю BPM ниже 150 и может быть отключено до 10 BPM при очень высоком BPM (например,> 400 BPM):
- Установка хоста на 100 ударов в минуту дает мне 100.401606
- 150 ударов в минуту -> 149,700599 ~ 150,602410
- 255 ударов в минуту -> 255.102041 ~ 257.731959
- 411 ударов в минуту -> 409,836066 ~ 416,666667
Есть ли что-то еще, чтобы рассмотреть что-то еще более точное?
Спасибо за помощь, Курт! очень полезно!
ОБНОВЛЕНИЕ 2
Я разветвил PGMidi и добавил некоторые функции, такие как вычисление BPM и квантование. Репо здесь https://github.com/yderidde/PGMidi
Я уверен, что это может быть оптимизировано, чтобы быть более точным. Кроме того, процедура квантования не идеальна... Так что, если кто-то увидит какую-то ошибку в моем коде или у вас есть предложения, чтобы сделать все это более стабильным / точным, пожалуйста, дайте мне знать!!
1 ответ
Здесь есть несколько ошибок, некоторые более важные, чем другие.
Самое важное: вы работаете с целыми числами в миллисекундах, что недостаточно для получения точных ударов в минуту. Давайте использовать 120 ударов в минуту в качестве примера. При 120 ударах в минуту и 24 ударах в минуту, каждые часы поступают за 20,833 мс. Поскольку вы вычисляете целые миллисекунды, это будет 20 или 21 мс. Когда вы делаете математику (с двойным!), Чтобы вернуться к BPM, это дает вам 125 ударов в минуту или 119,0476 ударов в минуту. Не то, что вы ожидаете.
Если бы вы делали математику с интегральными микросекундами или наносекундами, вы бы получили более точные значения. Я предлагаю использовать AudioConvertHostTimeToNanos()
, определенный в <CoreAudio/HostTime.h>
, чтобы преобразовать из MIDITimeStamp
в целое число наносекунд, а затем преобразовать в double
и идти оттуда. Вы не должны использовать mach_timebase_info
сам.
Также:
MIDIPacket
сtimeStamp
значение, которое отмечает, когда они были получены. CoreAudio приложит много усилий, чтобы дать вам эту временную метку, так что используйте ее!Не полагайтесь на звонок
mach_absolute_time()
, что будет позже через непоследовательное количество времени, в зависимости от многих факторов вне вашего контроля.Не звони
MIDIPacketListInit
,Перебирать каждый
MIDIPacket
вMIDIPacketList
, используйте этот код, прямо изMIDIServices.h
:MIDIPacket *packet = &packetList->packet[0]; for (int i = 0; i < packetList->numPackets; ++i) { /* your code to use the packet goes here */ packet = MIDIPacketNext(packet); }
statusByte >> 4 << 4
Больно смотреть на. Ты имеешь в видуstatusByte & 0xF0
,