CoreMIDIでMIDIデータを受信する

ほんとは送信したいんだけど、そのためには送信先を用意する必要があって、
まずは下準備ということでMIDIデータの受信から。

追記 2015/10/22
あまりにも用を足さないコードだったので、
まるっと書き直しました。
— ここまで追記 —

検索するといろいろ見つかったんだけど、
今回もこちらのサイトが詳しくて、このページを参考にさせて頂きました。

Core MIDI その1 MIDIObject
http://objective-audio.jp/2008/06/core-midi-midiobject.html

手順は、こんな感じ。

  1. 適当なプロジェクト(ex. MyMidiInst)を作る
  2. “Linked Frameworks and Libraries”にCoreMIDIを追加する

あとは、これを”ViewController.swift”にコピペする
(”Device Orientation”はPortraitで固定してね。)

import UIKit
import CoreMIDI

class ViewController: UIViewController {
    let clientName: CFStringRef = "MyMidiInst"
    
    var clientRef: MIDIClientRef = 0
    var extPointRef: MIDIEndpointRef = 0
    var inPortRef: MIDIPortRef = 0

    var textView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        var textArea = self.view.frame
        textArea.insetInPlace( dx: 0.0, dy: 20.0 )
        textArea.offsetInPlace( dx: 0.0, dy: 40.0 )
        
        textView = UITextView( frame: textArea )
        textView.scrollEnabled = true
        textView.font = UIFont.systemFontOfSize( 12.0 )
        textView.textColor = UIColor.whiteColor()
        textView.backgroundColor = UIColor.blackColor()
        textView.editable = false
        textView.selectable = false

        let clearButton = UIButton(frame: CGRect(x: 0,y: 20,width: 120,height: 40) )
        clearButton.backgroundColor = UIColor.blueColor()
        clearButton.setTitle( "Clear", forState: .Normal )
        clearButton.addTarget( self, action: Selector("clearButtonTapped:"), forControlEvents: .TouchUpInside )

        let scanButton = UIButton(frame: CGRect(x: 120,y: 20,width: 120,height: 40) )
        scanButton.backgroundColor = UIColor.blueColor()
        scanButton.setTitle( "Scan", forState: .Normal )
        scanButton.addTarget( self, action: Selector("scanButtonTapped:"), forControlEvents: .TouchUpInside )

        let connectButton1 = UIButton(frame: CGRect(x: 240,y: 20,width: 120,height: 40) )
        connectButton1.backgroundColor = UIColor.blueColor()
        connectButton1.setTitle( "Connect[0]", forState: .Normal )
        connectButton1.addTarget( self, action: Selector("connectButtonTapped:"), forControlEvents: .TouchUpInside )

        let connectButton2 = UIButton(frame: CGRect(x: 360,y: 20,width: 120,height: 40) )
        connectButton2.backgroundColor = UIColor.blueColor()
        connectButton2.setTitle( "Connect[1]", forState: .Normal )
        connectButton2.addTarget( self, action: Selector("connectButtonTapped:"), forControlEvents: .TouchUpInside )
        
        let disconnectButton = UIButton(frame: CGRect(x: 480,y: 20,width: 120,height: 40) )
        disconnectButton.backgroundColor = UIColor.blueColor()
        disconnectButton.setTitle( "Disconnect", forState: .Normal )
        disconnectButton.addTarget( self, action: Selector("disconnectButtonTapped:"), forControlEvents: .TouchUpInside )
        
        self.view.addSubview( textView )
        self.view.addSubview( clearButton )
        self.view.addSubview( scanButton )
        self.view.addSubview( connectButton1 )
        self.view.addSubview( connectButton2 )
        self.view.addSubview( disconnectButton )
        
        var status: OSStatus = noErr
        status = MIDIClientCreateWithBlock( "MyMidiInst", &clientRef, MyMIDINotifyBlock )
        if status != noErr {
            textView.text.appendContentsOf( "cannot create MIDI client!" )
            return
        }
        
        status = MIDIInputPortCreateWithBlock( clientRef, "MyMidiInst MIDI In", &inPortRef, MyMIDIReadBlock )
        if status != noErr {
            textView.text.appendContentsOf( "cannot create MIDI In!" )
            return
        }

        textView.text.appendContentsOf( "MIDI In opened!\n" )
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    deinit {
        if inPortRef != 0 {
            MIDIPortDispose( inPortRef )
            inPortRef = 0
        }

        if clientRef != 0 {
            MIDIClientDispose( clientRef )
            clientRef = 0
        }
    }
    
    func clearButtonTapped(sender: AnyObject?) {
        textView.text = ""
    }

    func scanButtonTapped(sender: AnyObject?) {
        let n = MIDIGetNumberOfSources()
        for var i=0; i<n; i++ {
            let endPointRef = MIDIGetSource( i )
            
            var str: Unmanaged<CFString>?
            let status = MIDIObjectGetStringProperty( endPointRef, kMIDIPropertyDisplayName, &str )
            if status == noErr {
                textView.text.appendContentsOf( "[\(i)]: " )
                textView.text.appendContentsOf( str!.takeUnretainedValue() as String )
                textView.text.appendContentsOf( "\n" )
                str!.release()
            }
        }
    }

    func connectButtonTapped(sender: AnyObject?) {
        extPointRef = 0

        let btn = sender as! UIButton
        let title = btn.currentTitle!
        let ch = title[title.endIndex.predecessor().predecessor()]
        if ch == "0" {
            if 1 <= MIDIGetNumberOfSources() {
                extPointRef = MIDIGetSource( 0 )
            }
            else {
                self.textView.text.appendContentsOf( "MIDISources[0] not found!\n" )
                return
            }
        }
        else {
            if 2 <= MIDIGetNumberOfSources() {
                extPointRef = MIDIGetSource( 1 )
            }
            else {
                self.textView.text.appendContentsOf( "MIDISources[1] not found!\n" )
                return
            }
        }
        
        let status = MIDIPortConnectSource( inPortRef, extPointRef, nil )
        if status == noErr {
            textView.text.appendContentsOf( "Connect!\n" )
        }
        else {
            textView.text.appendContentsOf( "Connent with MIDI source failed!\n" )
        }
    }

    func disconnectButtonTapped(sender: AnyObject?) {
        if extPointRef != 0 {
            MIDIPortDisconnectSource( inPortRef, extPointRef )
            extPointRef = 0
            textView.text.appendContentsOf( "Disconnect!\n" )
        }
        else {
            textView.text.appendContentsOf( "Already disconnented!\n" )
        }
    }
    
    func MyMIDIReadBlock(packetList: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutablePointer<Void>) {
        let packets: MIDIPacketList = packetList.memory
        var packet: MIDIPacket = packets.packet
        for _ in 0 ..< packets.numPackets {
            if packet.data.0 == 0xF8 || packet.data.0 == 0xFE {
                continue
            }

            let str: String
            switch packet.length {
                case 1: str = String(format: "%02X", packet.data.0)
                case 2: str = String(format: "%02X %02X", packet.data.0, packet.data.1)
                case 3: str = String(format: "%02X %02X %02X", packet.data.0, packet.data.1, packet.data.2)
                default: str = "length: \(packet.length)"
            }

            dispatch_async( dispatch_get_main_queue(), {
                self.textView.text.appendContentsOf( str )
                self.textView.text.appendContentsOf( "\n" )
            } )
            packet = MIDIPacketNext( &packet ).memory
        }
    }

    func MyMIDINotifyBlock(midiNotification: UnsafePointer<MIDINotification>) {
        self.textView.text.appendContentsOf( "MyMIDINotifyBlock called!\n" )
        // todo: ここは何をしたらいんだろうね?
    }
}

Clearボタンは置いといて、
Scanボタンをタッチすると接続先が表示されます。
常に1つ存在するみたいですが、良くわかりません。(*1)
他のアプリだと特に選択させたりしないので、
最後にiPadと接続したものが選択されてそう。

という訳で、
手元にあるreface CSをCamera Connection Kitで接続して、
Scanボタンをタッチすると2つ目に表示されました。
これと接続するには、Connect[1]をタッチします。
すると、大量にデータが送られてくるので、
一定間隔で送られてくる0xF80xFEは無視して、
他の受信データはヘキサで表示してみました。

表示されている文字を消す場合はClearボタンをタッチして、
接続を解除する場合はDisconnectボタンをタッチします。

MyMIDIReadBlockの辺りが自信ないというか、
Swiftで書かれたサンプルが見つからなくて、
やっとコンパイルできたレベルなので、ちょっとアレです。
あと、受信データはMIDIPacketで確実に区切られていれば良いのですが、
そうでなければ、この実装のままだと取りこぼしてそう・・・。

おしまい。

(*1) 自分の環境だと、「ネットワーク Session 1」が常に存在する

Leave a Comment