Dieser Beitrag ist ein Programmier-Tutorial darueber, wie man MIDI-Nachrichten auf dem iPhone, iPad oder Mac mit eigenem Code empfaengt. Er beinhaltet eine Code-Erklaerung und ein herunterladbares Xcode-Projekt mit einem vollstaendig funktionierenden Beispiel.

Hintergrund

Am 26. August 2013 kaufte ich einen Roland A-88 MIDI-Controller. Ich dachte ehrlich, ich haette Zeit, mir selbst Klavierspielen beizubringen.

Da ich notorisch neugierig bin, fragte ich mich: “Was ist MIDI und was kommt aus diesem Controller?” Ja, ich haette eine App herunterladen koennen, aber…

Ein Jahr zuvor war ich von PC auf Mac umgestiegen und wollte unbedingt die macOS- und iOS-Entwicklung ausprobieren.

Ein neues Projekt war geboren: MIDI Aid.

https://twissmueller.medium.com/midi-aid-ab4454cfee58

So viel zum Klavierlernen.

Zu dieser Zeit wurde die App in Objective-C geschrieben und verwendete UIKit. Es war schwer, Tutorials zu CoreMIDI zu finden, aber ich schaffte es. Die erste Veroeffentlichung von MIDI Aid war am 8. Februar 2014.

Die Welt aenderte sich mit Swift und SwiftUI - zumindest meine Welt und sicherlich die vieler Apple-Entwickler. CoreMIDI bekam auch nuetzliche Updates, die ich nutzen wollte.

Ich schrieb MIDI Aid komplett in Swift und SwiftUI neu und veroeffentlichte es am 26. November 2020.

Es war (und ist immer noch) schwer, adaequate Tutorials zu CoreMIDI zu finden. Mit all den Aenderungen wurden viele aeltere Ressourcen veraltet. Es gab keine vollstaendigen Code-Beispiele, die zeigten, wie man einfache MIDI-Nachrichten von einem Controller auf iOS, iPadOS oder macOS empfaengt.

Dieser Beitrag fuellt diese Luecke, indem er eine Code-Erklaerung und ein voll funktionsfaehiges Xcode-Projekt bereitstellt.

Kompiliere es fuer macOS oder iOS, schliesse ein USB-MIDI-Geraet an und druecke die Tasten - oder was auch immer MIDI-Nachrichten auf deinem Geraet ausloest.

Wenn die App eine MIDI-Nachricht empfaengt, wird sie protokolliert. Im Moment unterstuetzt sie “note on”- und “note off”-Events, aber sie kann leicht fuer andere Typen erweitert werden.

https://youtu.be/I4xTyAwSt2E

Der Xcode-Workspace mit der voll funktionsfaehigen Anwendung kann hier heruntergeladen werden.

Bitte beachte: Ich habe den Code nicht geschrieben, um SysEx-Nachrichten zu verarbeiten, da ich keine Moeglichkeit habe, sie mit meinen Controllern zu testen.

Voraussetzungen

Ein paar Dinge werden benoetigt, um diesem Tutorial zu folgen.

Angenommen, du hast eine Art MIDI-Controller und eine Moeglichkeit, ihn mit deinem Mac, iPhone oder iPad zu verbinden.

Es gibt drei gaengige Verbindungstypen:

  • Klassisches MIDI-Kabel
  • USB-Kabel
  • Bluetooth

USB ist am einfachsten, da es direkt an dein Apple-Geraet angeschlossen werden kann. Du benoetigst moeglicherweise einen USB-A zu Lightning- oder USB-C-Adapter.

Wenn dein Geraet kein USB unterstuetzt, verwende die klassischen MIDI-Anschluesse mit einem Adapterkabel wie dem Roland UM-ONE mk2.

Bluetooth ist auch eine Option, erfordert aber zusaetzliche Programmierung fuer iOS, die ich hier nicht einbezogen habe. Geraete wie der Roland WM-1 uebertragen MIDI ueber Bluetooth.

Du benoetigst auch Xcode. Ich habe den Code mit Xcode 12.5.1 geschrieben und getestet.

Wenn du dich fragst, wie du auf iOS debuggen kannst, waehrend ein MIDI-Geraet an dein iOS-Geraet angeschlossen ist: Es gibt eine Xcode-Einstellung, um die App ueber WLAN zu starten.

Oeffne Window Devices and Simulators, waehle dein iOS-Geraet und aktiviere “Connect via network”. Von da an ist kein USB-Kabel mehr erforderlich, um die App von Xcode aus auf deinem iOS-Geraet auszufuehren.

Ueber Netzwerk verbinden

Code-Erklaerung

Jetzt etwas Code. Ich stelle die verwendeten Klassen zusammen mit Links zu Apples Dokumentation und zitierten Beschreibungen bereit.

Alles beginnt mit einem

A client object derives from MIDIObjectRef. It doesn’t have an owning object.

var midiClient: MIDIClientRef = 0

Wir erstellen diesen Client durch Aufruf von

Creates a MIDI client with a callback block.

MIDIClientCreateWithBlock("Client" as CFString, &midiClient) { midiNotification in
...
}

Dieser Callback wird aufgerufen, wenn sich etwas im MIDI-Setup aendert. Wenn ein Geraet angeschlossen wird, muss es “intern” verbunden werden (wird unten erklaert).

switch (notification.messageID) {
...
 case.msgObjectAdded:
   NSLog("msgObjectAdded")
   // connect the source, see below

Als Naechstes deklarieren wir einen

A port object derives from MIDIObjectRef, and its owning object is a MIDIDeviceRef. It represents an input or output port, and it provides the means to communicate with any number of MIDI sources or destinations.

var inputPort: MIDIPortRef = 0

Der Port muss durch Aufruf von erstellt werden

Creates an input port through which the client may receive incoming MIDI messages from any MIDI source.

MIDIInputPortCreateWithProtocol(
   midiClient,
   "Input Port as CFString" as CFString,
   MIDIProtocolID._1_0,
   &inputPort) { [weak self] eventList, srcConnRefCon in
...
}

Wir arbeiten dann mit eventList, das ein UnsafePointer<MIDIEventList> ist. Dies sind die zu extrahierenden MIDI-Daten.

A variable-length list of MIDI event packets.

Von dort parsen wir die MIDI-Nachrichten:

let midiEventList: MIDIEventList = unsafePointerMidiEventList.pointee
var packet = midiEventList.packet
 
(0..< midiEventList.numPackets).forEach { _ in
...
}

In der Schleife extrahieren wir die Pakete:

let words = Mirror(reflecting: packet.words).children
words.forEach { word in
 let uint32 = word.value as! UInt32
 guard uint32 > 0 else { return }
 midiPacket = MidiPacket(
  first: UInt8((uint32 & 0xFF000000) >> 24),
  second: UInt8((uint32 & 0x00FF0000) >> 16),
  third: UInt8((uint32 & 0x0000FF00) >> 8),
  fourth: UInt8(uint32 & 0x000000FF))
}

Was mir nicht klar ist, ist die Anzahl der Pakete (numPackets). Bei meinen Beobachtungen erzeugt das Druecken einer Taste ein Paket, das Loslassen ein weiteres, und so weiter.

Vielleicht erzeugen SysEx-Nachrichten mehrere Pakete, da sie variable Laenge haben koennen.

Schliesslich verbinden wir die Quellen.

Hole zuerst die Anzahl der Quellen mit

Returns the number of sources in the system.

let sourceCount = MIDIGetNumberOfSources()

Iteriere ueber jeden Quellindex, um die Quelle abzurufen mit

Returns a source in the system.

var source = MIDIGetSource(sourceIndex)

Dann verbinde jede Quelle durch Aufruf von

Makes a connection from a source to a client input port.

MIDIPortConnectSource(inputPort, source, &source)

Ich habe den Quellverbindungscode in eine separate Funktion gelegt und rufe sie zweimal auf: einmal beim Start nach dem Erstellen der MIDIClientRef und MIDIPortRef, und erneut, wenn ein neues Geraet angeschlossen wird.

Fazit

Hoffentlich hilft dieses Tutorial dir, deinen eigenen CoreMIDI-basierten Code zu implementieren.

Wir haben behandelt, wie man einen MIDI-Controller mit einem Mac, iPhone oder iPad verbindet; wie man die relevanten Strukturen im Code einrichtet; und wie man die empfangenen MIDI-Nachrichten anzeigt. Es gibt viele weitere Anwendungsfaelle - du kannst auch MIDI-Nachrichten aus dem Code generieren, um andere Geraete zu steuern. Vielleicht schreibe ich in Zukunft ein weiteres Tutorial, aber zuerst sollte ich wahrscheinlich Klavierspielen lernen.

Ressourcen