为什么这个简单的CoreMIDI程序没有产生MIDI输出?

20

这里有一个非常简单的CoreMIDI OS X应用程序,它发送MIDI数据。问题是它无法正常工作。它编译良好并运行。它没有报告任何错误,也没有崩溃。创建的Source在MIDI监视器中可见。然而,没有MIDI数据输出

请问有人知道我在这里做错了什么吗?

#include <CoreMIDI/CoreMIDI.h>

int main(int argc, char *args[])
{
    MIDIClientRef   theMidiClient;
    MIDIEndpointRef midiOut;
    MIDIPortRef     outPort;
    char pktBuffer[1024];
    MIDIPacketList* pktList = (MIDIPacketList*) pktBuffer;
    MIDIPacket     *pkt;
    Byte            midiDataToSend[] = {0x91, 0x3c, 0x40};
    int             i;

    MIDIClientCreate(CFSTR("Magical MIDI"), NULL, NULL,
                     &theMidiClient);
    MIDISourceCreate(theMidiClient, CFSTR("Magical MIDI Source"),
                     &midiOut);
    MIDIOutputPortCreate(theMidiClient, CFSTR("Magical MIDI Out Port"),
                         &outPort);

    pkt = MIDIPacketListInit(pktList);
    pkt = MIDIPacketListAdd(pktList, 1024, pkt, 0, 3, midiDataToSend);

    for (i = 0; i < 100; i++) {
        if (pkt == NULL || MIDISend(outPort, midiOut, pktList)) {
            printf("failed to send the midi.\n");
        } else {
            printf("sent!\n");
        }
        sleep(1);
    }

return 0;
}
4个回答

27
您正在调用MIDISourceCreate来创建一个虚拟MIDI源。
这意味着您的源将出现在其他应用程序的MIDI设置UI中,并且这些应用程序可以选择是否要监听您的源。除非其他应用程序将其通道到那里,否则您的MIDI不会发送到任何物理MIDI端口。这也意味着您的应用程序无法选择发送的MIDI去向。我假设这就是您想要的。 MIDISourceCreate的文档说:

创建虚拟源后,请使用MIDIReceived将来自虚拟源的MIDI消息传输到连接到虚拟源的任何客户端。

所以,做两件事:
  • 删除创建输出端口的代码。您不需要它。
  • MIDISend(outPort,midiOut,pktList)更改为:MIDIReceived(midiOut,pktlist)
那应该解决您的问题。
所以输出端口有什么用?如果你想将MIDI数据定向到特定的目标,比如物理MIDI端口,你不会创建虚拟MIDI源。相反:
  1. 调用MIDIOutputPortCreate()创建一个输出端口
  2. 使用MIDIGetNumberOfDestinations()MIDIGetDestination()获取目标列表,并找到你感兴趣的那个。
  3. 要将MIDI发送到一个目标,调用MIDISend(outputPort, destination, packetList)

7
谢谢您的夸奖。在接收到MIDI之后,我永远不会想到使用MIDIReceived来发送MIDI。 - sixohsix
5
如果你把MIDI源想象成一个MIDI设备驱动程序中的一部分,它可能更容易理解:在这种情况下,驱动程序接收到一些MIDI(通过线路),并将其通知给CoreMIDI。虚拟MIDI源是后来添加的。 - Kurt Revis
1
@sixohsix 这里更重要的可能是“更仔细地阅读文档”。我也错过了它,而且可能会再次错过,所以我感同身受。 - Dan Rosenstark
不要忘记,如果使用时间戳发送Midi事件,则需要将它们发送到可能的所有传输数据上 - 否则接收器/目标可能会忽略时间戳为0的事件(因为您的接收器会认为它们发生在过去)。 - Ol Sen
2
#抱怨 MIDIReceived 已接收。让它沉淀下来。即使在虚拟源之前转发MIDI时,这也是糟糕的命名。幸运的是,我们现在有Swift来解决所有这些文档/命名问题。/s - Rad'Val

3

我只是为了自己的参考而留下这个。这是一个完整的例子,基于你的例子,但包括另一方(接收),我的糟糕的C代码和被接受的答案的更正(当然)。

#import "AppDelegate.h"

@implementation AppDelegate

@synthesize window = _window;

#define NSLogError(c,str) do{if (c) NSLog(@"Error (%@): %u:%@", str, (unsigned int)c,[NSError errorWithDomain:NSMachErrorDomain code:c userInfo:nil]); }while(false)

static void spit(Byte* values, int length, BOOL useHex) {
    NSMutableString *thing = [@"" mutableCopy];
    for (int i=0; i<length; i++) {
        if (useHex)
            [thing appendFormat:@"0x%X ", values[i]];
        else
            [thing appendFormat:@"%d ", values[i]];
    }
    NSLog(@"Length=%d %@", length, thing);
}

- (void) startSending {
    MIDIEndpointRef midiOut;
    char pktBuffer[1024];
    MIDIPacketList* pktList = (MIDIPacketList*) pktBuffer;
    MIDIPacket     *pkt;
    Byte            midiDataToSend[] = {0x91, 0x3c, 0x40};
    int             i;

    MIDISourceCreate(theMidiClient, CFSTR("Magical MIDI Source"),
                     &midiOut);
    pkt = MIDIPacketListInit(pktList);
    pkt = MIDIPacketListAdd(pktList, 1024, pkt, 0, 3, midiDataToSend);

    for (i = 0; i < 100; i++) {
        if (pkt == NULL || MIDIReceived(midiOut, pktList)) {
            printf("failed to send the midi.\n");
        } else {
            printf("sent!\n");
        }
        sleep(1);
    }
}

void ReadProc(const MIDIPacketList *packetList, void *readProcRefCon, void *srcConnRefCon)
{
    const MIDIPacket *packet = &packetList->packet[0];

    for (int i = 0; i < packetList->numPackets; i++)
    {

        NSData *data = [NSData dataWithBytes:packet->data length:packet->length];
        spit((Byte*)data.bytes, data.length, YES);

        packet = MIDIPacketNext(packet);
    }
}

- (void) setupReceiver {
    OSStatus s;
    MIDIEndpointRef virtualInTemp;
    NSString *inName = [NSString stringWithFormat:@"Magical MIDI Destination"];
    s = MIDIDestinationCreate(theMidiClient, (__bridge CFStringRef)inName, ReadProc,  (__bridge void *)self, &virtualInTemp);
    NSLogError(s, @"Create virtual MIDI in");
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    MIDIClientCreate(CFSTR("Magical MIDI"), NULL, NULL,
                     &theMidiClient);
    [self setupReceiver];
    [self startSending];

}

@end

1
一些人忽略的小细节:对于一些音乐应用程序来说,MIDIPacketListAdd函数的time参数是很重要的。
以下是获取该参数的示例:
#import <mach/mach_time.h>
MIDITimeStamp midiTime = mach_absolute_time();

来源:苹果文档

然后,应用于这里的其他示例:

pktBuffer[1024];
MIDIPacketList *pktList = (MIDIPacketList*)pktBuffer;
MIDIPacket *pktPtr = MIDIPacketListInit(pktList);
MIDITimeStamp midiTime = mach_absolute_time();
Byte midiDataToSend[] = {0x91, 0x3c, 0x40};
pktPtr = MIDIPacketListAdd(pktList, sizeof(pktList), pktPtr, midiTime, sizeof(midiDataToSend), midiDataToSend);

你可能会发现,如果你忘记正确设置时间参数,一款由德国知名软件公司开发的著名应用程序可能会拒绝绘制你的传入MIDI-CC数据。 - gog

0
考虑到你自己的 MIDI 客户端创建应用程序可能会崩溃,或者主机发送 MIDI 也可能会崩溃。你可以通过检查客户端/目标是否已经存在,然后通过处理单例分配来更轻松地处理这个问题。当你的 MIDI 客户端存在但不工作时,这是因为你需要告诉 CoreMidi 你的定制客户端能够处理什么和它将具有什么延迟,特别是当主机发送客户端使用时间戳很多时(例如 Ableton 和其他软件)。
在你的 .h 文件中。
#import <CoreMIDI/CoreMIDI.h>
#import <CoreAudio/HostTime.h>

@interface YourVirtualMidiHandlerObject : NSObject 

@property (assign, nonatomic) MIDIClientRef midi_client;
@property (nonatomic) MIDIEndpointRef outSrc;
@property (nonatomic) MIDIEndpointRef inSrc;
- (id)initWithVirtualSourceName:(NSString *)clientName;

@end

在你的 .m 文件中

@interface YourVirtualMidiHandlerObject () {
   MIDITimeStamp midiTime;
   MIDIPacketList pktList;
}
@end

您可以按照以下方式准备虚拟客户端的初始化,同时在您的.m文件中进行操作。

@implementation YourVirtualMidiHandlerObject 

// this you can call in dealloc or manually 
// else where when you stop working with your virtual client
-(void)teardown {
    MIDIEndpointDispose(_inSrc);
    MIDIEndpointDispose(_outSrc);
    MIDIClientDispose(_midi_client);
}

- (id)initWithVirtualSourceName:(NSString *)clientName { 
    if (self = [super init]) {
        OSStatus status = MIDIClientCreate((__bridge CFStringRef)clientName, (MIDINotifyProc)MidiNotifyProc, (__bridge void *)(self), &_midi_client);

        BOOL isSourceLoaded = NO;
        BOOL isDestinationLoaded = NO;

        ItemCount sourceCount = MIDIGetNumberOfSources();
        for (ItemCount i = 0; i < sourceCount; ++i) {
            _outSrc = MIDIGetSource(i);
            if ( _outSrc != 0 ) {
                if ([[self getMidiDisplayName:_outSrc] isEqualToString:clientName] && !isSourceLoaded) {
                    isSourceLoaded = YES; 
                    break; //stop looping thru sources if it is existing
                }
            }
        }

        ItemCount destinationCount = MIDIGetNumberOfDestinations();
        for (ItemCount i = 0; i < destinationCount; ++i) {
            _inSrc = MIDIGetDestination(i);
            if (_inSrc != 0) {
                if ([[self getMidiDisplayName:_inSrc] isEqualToString:clientName] && !isDestinationLoaded) {
                    isDestinationLoaded = YES;
                    break; //stop looping thru destinations if it is existing
                }
            }
        }

        if(!isSourceLoaded) {
            //your costume source needs to tell CoreMidi what it is handling
            MIDISourceCreate(_midi_client, (__bridge CFStringRef)clientName, &_outSrc);
            MIDIObjectSetIntegerProperty(_outSrc, kMIDIPropertyMaxTransmitChannels, 16);
            MIDIObjectSetIntegerProperty(_outSrc, kMIDIPropertyTransmitsProgramChanges, 1);
            MIDIObjectSetIntegerProperty(_outSrc, kMIDIPropertyTransmitsNotes, 1);
            // MIDIObjectSetIntegerProperty(_outSrc, kMIDIPropertyTransmitsClock, 1);
            isSourceLoaded = YES;
        }

        if(!isDestinationLoaded) {
            //your costume destination needs to tell CoreMidi what it is handling
            MIDIDestinationCreate(_midi_client, (__bridge CFStringRef)clientName, midiRead, (__bridge void *)(self), &_inSrc);
            MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyAdvanceScheduleTimeMuSec, 1); // consider more 14ms in some cases
            MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyReceivesClock, 1);
            MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyReceivesNotes, 1);
            MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyReceivesProgramChanges, 1);
            MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyMaxReceiveChannels, 16);
            // MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyReceivesMTC, 1);
            // MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyReceivesBankSelectMSB, 1);
            // MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertyReceivesBankSelectLSB, 1);
            // MIDIObjectSetIntegerProperty(_inSrc, kMIDIPropertySupportsMMC, 1);
            isDestinationLoaded = YES;
        }

        if (!isDestinationLoaded || !isSourceLoaded) {
            if (status != noErr ) {
                NSLog(@"Failed creation of virtual Midi client \"%@\", so disposing the client!",clientName);
                MIDIClientDispose(_midi_client);
            }
        }
    }
    return self;
}

// Returns the display name of a given MIDIObjectRef as an NSString
-(NSString *)getMidiDisplayName:(MIDIObjectRef)obj {
    CFStringRef name = nil;
    if (noErr != MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &name)) return nil;
    return (__bridge NSString *)name;
}

对于那些在创作过程中尝试读取节奏(MIDI传输)并设置虚拟目标属性的人们……

不要忘记时间戳随数据包一起发送,但一个数据包可以包含多个相同类型的命令,甚至包含多个时钟命令。当构建时钟计数器以查找BPM节奏时,您必须考虑计算至少12个命令才能进行计算。如果您只使用其中3个,则实际上测量的是自己的缓冲区读取处理延迟而不是真正的时间戳。

如果MIDI发送方未能正确设置时间戳,则您的读取过程(回调)将处理时间戳……

void midiRead(const MIDIPacketList * pktlist, void * readProcRefCon, void * srcConnRefCon) {

    const MIDIPacket *pkt = pktlist->packet;

    for ( int index = 0; index < pktlist->numPackets; index++, pkt = MIDIPacketNext(pkt) ) {
        MIDITimeStamp timestamp = pkt->timeStamp;
        if ( !timestamp ) timestamp = mach_absolute_time();
        if ( pkt->length == 0 ) continue;

        const Byte *p = &pkt->data[0];
        Byte functionalDataGroup = *p & 0xF0;

        // Analyse the buffered bytes in functional groups is faster
        // like 0xF will tell it is clock/transport midi stuff
        // go in detail after this will shorten the processing
        // and it is easier to read in code
        switch (functionalDataGroup) {
            case 0xF : {
                   // in here read the exact Clock command
                   // 0xF8 = clock
               }
               break;
            case ... : {
                   // do other nice grouped stuff here, like reading notes
               }
               break;
            default  : break;
        }
    }
}

不要忘记客户端需要一个回调函数来处理内部通知。

void MidiNotifyProc(const MIDINotification* message, void* refCon) {

    // when creation of virtual client fails we despose the whole client
    // meaning unless you need it you can ignore added/removed notifications
    if (message->messageID != kMIDIMsgObjectAdded &&
        message->messageID != kMIDIMsgObjectRemoved) return;

    // reactions to other midi notications you gonna trigger here..
}

然后你就可以发送MIDI了...

-(void)sendMIDICC:(uint8_t)cc Value:(uint8_t)v ChZeroToFifteen:(uint8_t)ch {


    MIDIPacket *packet = MIDIPacketListInit(&pktList);
    midiTime = packet->timeStamp;

    unsigned char ctrl[3] = { 0xB0 + ch, cc, v };
    while (1) {
        packet = MIDIPacketListAdd(&pktList, sizeof(pktList), packet, midiTime, sizeof(ctrl), ctrl);
        if (packet != NULL) break;
        // create an extra packet to fill when it failed before
        packet = MIDIPacketListInit(&pktList);
    }

    // OSStatus check = // you dont need it if you don't check failing
    MIDIReceived(_outSrc, &pktList);
}

发布此帖是因为所有其他答案都无法解决所提出的问题。 在填充任何缓冲区之前,必须正确设置CoreMidi。如果CoreMidi不知道您的虚拟Midi客户端具有什么延迟和属性,它将显示但不起作用。 - Ol Sen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接