WebMIDIKit: 最简单的 Swift MIDI 库

###想在一下午学会音频合成、声音设计以及如何制作酷炫的声音吗?快来看看 Syntorial!

关于

什么是 MIDI

MIDI 是一个管理音乐软件和音乐设备互连的标准。它允许您通过在应用程序和设备之间发送数据来制作音乐。

什么是 WebMIDI

WebMIDI 是一个浏览器 API 标准,它将 MIDI 技术带到 Web 上。 WebMIDI 非常精简,它只描述 MIDI 端口的选择,从输入端口接收数据以及将数据发送到输出端口。WebMIDI 目前在 Chrome 和 Opera 中实现。 请注意,WebMIDI 相对底层,因为消息仍然表示为 UInt8s(字节/八位字节)序列。

什么是 WebMIDIKit

WebMIDIKit 是 macOS/iOS 上 WebMIDI API 的一个实现。 在这些操作系统上,用于处理 MIDI 的原生框架是 CoreMIDI。 CoreMIDI 很老,并且 API 完全是用 C 语言编写的 (💩)。 使用它涉及大量的 void 指针类型转换 (💩^9.329) 和其他难以启齿的东西。 此外,一些 API 并没有完全从 Swift 过渡过来,并且实际上在 Swift 中无法使用(MIDIPacketList APIs,我说的就是你)。

CoreMIDI 也很冗长且容易出错。 选择一个输入端口并从中接收数据需要 约 80 行 复杂的 Swift 代码WebMIDIKit 让你用 1 行代码完成同样的事情。

WebMIDIKit 是 AudioKit 项目的一部分,最终将取代 AudioKit 的 MIDI 实现

另请注意,WebMIDIKit 添加了一些 API,这些 API 不是 WebMIDI 标准的一部分。 这些在代码库中标记为非标准。

用法

安装

使用 Swift Package Manager。 将以下 .Package 条目添加到您的依赖项中。

.Package(url: "https://github.com/adamnemecek/webmidikit", from: "1.0.0")

查看 示例项目

选择一个输入端口并从中接收 MIDI 消息

import WebMIDIKit

/// represents the MIDI session
let midi: MIDIAccess = MIDIAccess()

/// prints all MIDI inputs available to the console and asks the user which port they want to select
let inputPort: MIDIInput? = midi.inputs.prompt()

/// Receiving MIDI events 
/// set the input port's onMIDIMessage callback which gets called when the port receives MIDI packets
inputPort?.onMIDIMessage = { (list: UnsafePointer<MIDIPacketList>) in
    for packet in list {
        print("received \(packet)")
    }
}

选择一个输出端口并将 MIDI 数据包发送到该端口

/// select an output port
let outputPort: MIDIOutput? = midi.outputs.prompt()

/// send messages to it
outputPort.map {

	/// send a note on message
	/// the bytes are in the normal MIDI message format (https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
	/// i.e. you have to send two events, a note on event and a note off event to play a single note
	/// the format is as follows:
	/// byte0 = message type (0x90 = note on, 0x80 = note off)
	/// byte1 = the note played (0x60 = C8, see http://www.midimountain.com/midi/midi_note_numbers.html)
	/// byte2 = velocity (how loud the note should be 127 (=0x7f) is max, 0 is min)

	let noteOn: [UInt8] = [0x90, 0x60, 0x7f]
	let noteOff: [UInt8] = [0x80, 0x60, 0]

	/// send the note on event
	$0.send(noteOn)

	/// send a note off message 1000 ms (1 second) later
	$0.send(noteOff, offset: 1000)

	/// in WebMIDIKit, you can also chain these
	$0.send(noteOn)
	  .send(noteOff, offset: 1000)
}

如果想要选择的输出端口具有相应的输入端口,您还可以这样做

let outputPort: MIDIOutput? = midi.output(for: inputPort)

同样,您可以找到输出端口的输入端口。

let inputPort2: MIDIInput? = midi.input(for: outputPort)

循环遍历端口

端口映射是类似字典的 MIDIInputsMIDIOutputs 集合,它们使用端口的 ID 进行索引。 因此,您无法像对数组那样对它们进行索引(原因是端点可以添加和删除,因此您无法通过它们的索引引用它们)。

for (id, port) in midi.inputs {
	print(id, port)
}

创建虚拟端口

要创建虚拟输入和输出端口,请分别使用 createVirtualVirtualMIDIInputcreateVirtualVirtualMIDIOutput 函数。

let virtualInput = midi.createVirtualMIDIInput(name: "Virtual input")

let virtualOutput = midi.createVirtualMIDIOutput(name: "Virtual output") { (list: UnsafePointer<MIDIPacketList>) in

}

安装

使用 Swift Package Manager。 将以下 .Package 条目添加到您的依赖项中。

.Package(url: "https://github.com/adamnemecek/webmidikit", from: "1.0.0")

如果您遇到任何构建问题,请查看示例项目 示例项目

文档

MIDIAccess

表示 MIDI 会话。 请参阅 规范

class MIDIAccess {
	/// collections of MIDIInputs and MIDIOutputs currently connected to the computer
	var inputs: MIDIInputMap { get }
	var outputs: MIDIOutputMap { get }

	/// will be called if a port changes either connection state or port state
	var onStateChange: ((MIDIPort) -> ())? = nil { get set }

	init()
	
	/// given an output, tries to find the corresponding input port
	func input(for port: MIDIOutput) -> MIDIInput?
	
	/// given an input, tries to find the corresponding output port
	/// if you send data to the output port returned, the corresponding input port
	/// will receive it (assuming the `onMIDIMessage` is set)
	func output(for port: MIDIInput) -> MIDIOutput?
}

MIDIPort

请参阅 规范。 表示 MIDIInputMIDIOutput 的基类。

请注意,您不要自己构造 MIDIPort 及其子类,您只能从 MIDIAccess 对象获取它们。 另请注意,您只处理子类或 MIDIPort (MIDIInputMIDIOutput),永远不要处理 MIDIPort 本身。

class MIDIPort {

	var id: Int { get }
	var manufacturer: String { get }

	var name: String { get }

	/// .input (for MIDIInput) or .output (for MIDIOutput)
	var type: MIDIPortType { get }

	var version: Int { get }

	/// .connected | .disconnected,
	/// indicates if the port's endpoint is connected or not
	var state: MIDIPortDeviceState { get }

	/// .open | .closed
	var connection: MIDIPortConnectionState { get }

	/// open the port, is called implicitly when MIDIInput's onMIDIMessage is set or MIDIOutputs' send is called
	func open()

	/// closes the port
	func close()
}

MIDIInput

允许接收发送到端口的数据。

请参阅 规范

class MIDIInput: MIDIPort {
	/// set this and it will get called when the port receives messages.
	var onMIDIMessage: ((UnsafePointer<MIDIPacketList>) -> ())? = nil { get set }
}

MIDIOutput

请参阅 规范

class MIDIOutput: MIDIPort {

	/// send data to port, note that unlike the WebMIDI API, 
	/// the last parameter specifies offset from now, when the event should be scheduled (as opposed to absolute timestamp)
	/// the unit remains milliseconds though.
	/// note that right now, WebMIDIKit doesn't support sending multiple packets in the same call, to send multiple packets, you need on call per packet
	func send<S: Sequence>(_ data: S, offset: Timestamp = 0) -> MIDIOutput where S.Iterator.Element == UInt8
	
	// clear all scheduled but yet undelivered midi events
	func clear()
}