MIDI, MIDI 不是音频数据
MIDI 乐器数字接口, Musical Instrument Digital Interface
MIDI 计算机能理解的乐谱,计算机和电子乐器都可以处理的乐器格式
MIDI 不是音频信号,不包含 pcm buffer
通过音序器 sequencer,结合音频数据 / 乐器 ,播放 MIDI Event 数据
( 通过音色库 SoundFont,播放乐器的声音 )
通过 AVAudioSequencer ,简单播放
连接 AVAudioEngine 的输入和输出,
输入 AVAudioUnitSampler → 混频器 engine.mainMixerNode
→ 输出 engine.outputNode
拿 AVAudioEngine ,创建 AVAudioSequencer ,就可以播放 MIDI 了
配置 AVAudioEngine 的输入输出
- 输入 AVAudioUnitSampler → 混频器
engine.mainMixerNode
// 连接输入、输出
var engine = AVAudioEngine()
var sampler = AVAudioUnitSampler()
engine.attach(sampler)
// 节点 node 的 bus 0 是输出,
// bus 1 是输入
let outputHWFormat = engine.outputNode.outputFormat(forBus: 0)
engine.connect(sampler, to: engine.mainMixerNode, format: outputHWFormat)
guard let bankURL = Bundle.main.url(forResource: soundFontMuseCoreName, withExtension: "sf2") else {
fatalError("\(self.soundFontMuseCoreName).sf2 file not found.")
}
// 载入资源
do {
try
self.sampler.loadSoundBankInstrument(at: bankURL,
program: 0,
bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB),
bankLSB: UInt8(kAUSampler_DefaultBankLSB))
try engine.start()
} catch { print(error) }
- 混频器
engine.mainMixerNode
→ 输出engine.outputNode
, 不需要处理,就用 AVAudioEngine 默认的
用 AVAudioSequencer ,播放 MIDI
AVAudioSequencer 可以用不同的音频轨道 track,对应不同的乐器声音
tracks[index]
指向不同的音频产生节点
var sequencer = AVAudioSequencer(audioEngine: engine)
guard let fileURL = Bundle.main.url(forResource: "sibeliusGMajor", withExtension: "mid") else {
fatalError("\"sibeliusGMajor.mid\" file not found.")
}
do {
try sequencer.load(from: fileURL, options: .smfChannelsToTracks)
print("loaded \(fileURL)")
} catch {
fatalError("something screwed up while loading midi file \n \(error)")
}
// 这里处理的,比较简单
for track in sequencer.tracks {
track.destinationAudioUnit = self.sampler
}
sequencer.prepareToPlay()
do {
try sequencer.start()
} catch {
print("\(error)")
}
AudioKit 源代码中,用的是 MusicSequence 和 MusicPlayer
MIDI 让单调的音频,赋有节奏感
调用:
// 音频播放引擎
let engine = AudioEngine()
// 音频输入,采用 AVAudioUnitSampler
let drums = MIDISampler(name: "鼓点")
// 音序器 sequencer, 播放 MIDI Event
let sequencer = AppleSequencer(filename: "4tracks")
// 连接输入输出
engine.output = drums
do {
try engine.start()
} catch { // ...
}
do {
let bassDrumURL = Bundle.main.resourceURL?.appendingPathComponent("Samples/bass_drum_C1.wav")
let bassDrumFile = try AVAudioFile(forReading: bassDrumURL!)
let clapURL = Bundle.main.resourceURL?.appendingPathComponent("Samples/clap_D#1.wav")
let clapFile = try AVAudioFile(forReading: clapURL!)
// ...
// 初始化其他音频文件
// 给音频输入,分配音频资源
try drums.loadAudioFiles([bassDrumFile,
clapFile,
// ...其他音频文件 ])
} catch { //...
}
以上,一次简单的音频文件播放,就成了
音频文件上加入 MIDI 效果,调整每个音轨 track 上的音效,
音轨 track 不是通道 channel, 一个 channel 可以有多个 track
MIDI 效果, 播放开始时间、播放持续时间、播放音量 ( velocity 速度 )和音高 note
- 音高 note 的范围是 0 ~ 127
- 播放音量 volume,使用 velocity 来描述,他的值也在 0 ~ 127 之间
有时候,不同的 velocity,在乐器上,产生不同的音色
sequencer.clearRange(start: Duration(beats: 0), duration: Duration(beats: 100))
// 音序器的输出,指向音频输入的 MIDI 入口
sequencer.setGlobalMIDIOutput(drums.midiIn)
// 循环播放,MIDI 的播放时间,可以按秒,也可以按 beat 拍子
sequencer.enableLooping(Duration(beats: 4))
// 这里设置播放速度
sequencer.setTempo(150)
sequencer.tracks[0].add(noteNumber: 24, velocity: 80, position: Duration(beats: 0), duration: Duration(beats: 1))
sequencer.tracks[0].add(noteNumber: 24, velocity: 80, position: Duration(beats: 2), duration: Duration(beats: 1))
sequencer.tracks[1].add(noteNumber: 26, velocity: 80, position: Duration(beats: 2), duration: Duration(beats: 1))
// ...
// 配置 sequencer.tracks[2], 和 sequencer.tracks[3]
// 播放
sequencer.play()
AudioKit 源代码实现:
AppleSequencer 是对 MusicSequence 、 MusicTrack ( MusicTrackManager ) 和 MusicPlayer 的封装,
MusicTrack 通过 MusicTrackManager 的封装,使用
初始化
新建 sequence, 加载 MIDI 文件, 提供播放资源
新建 MusicPlayer, 来播放 MIDI 文件
class AppleSequencer: NSObject {
/// Music sequence
open var sequence: MusicSequence?
/// Array of AudioKit Music Tracks
open var tracks = [MusicTrackManager]()
/// Music Player
var musicPlayer: MusicPlayer?
/// 初始化
override public init() {
// 初始化音序器
NewMusicSequence(&sequence)
// setup and attach to musicplayer
// 初始化音乐播放器
NewMusicPlayer(&musicPlayer)
if let existingMusicPlayer = musicPlayer {
// 把播放器,关联到音序器
MusicPlayerSetSequence(existingMusicPlayer, sequence)
}
}
}
初始化调用,怎么走
// 通过文件名,实例化
public convenience init(filename: String) {
// 这个就是,上一步
self.init()
loadMIDIFile(filename)
}
// 通过文件名,加载 MIDI
public func loadMIDIFile(_ filename: String) {
// 文件名,转包 url
let bundle = Bundle.main
guard let file = bundle.path(forResource: filename, ofType: "mid") else {
Log("No midi file found")
return
}
let fileURL = URL(fileURLWithPath: file)
loadMIDIFile(fromURL: fileURL)
}
// 通过包 url,加载 MIDI
public func loadMIDIFile(fromURL fileURL: URL) {
// ...
// 重置状态
if let existingSequence = sequence {
// 把 MIDI 文件,加载到音序器
let status: OSStatus = MusicSequenceFileLoad(existingSequence,
fileURL as CFURL,
.midiType,
MusicSequenceLoadFlags())
if status != OSStatus(noErr) {
// 错误日志
// ...
}
}
initTracks()
}
初始化音轨,
完成 open var tracks = [MusicTrackManager]()
的初始化
func initTracks() {
var count: UInt32 = 0
if let existingSequence = sequence {
MusicSequenceGetTrackCount(existingSequence, &count)
}
for i in 0 ..< count {
var musicTrack: MusicTrack?
if let existingSequence = sequence {
// 通过音序器 sequence, 创建音轨 MusicTrack
MusicSequenceGetIndTrack(existingSequence, UInt32(i), &musicTrack)
}
if let existingMusicTrack = musicTrack {
tracks.append(MusicTrackManager(musicTrack: existingMusicTrack, name: "InitializedTrack"))
}
}
// ...
// 循环播放控制
}
给音轨添加播放资源
调用部分
sequencer.setGlobalMIDIOutput(drums.midiIn)
class AppleSequencer
里面,
统一设置,最简单,
就是把音频输入,塞给每一个音轨
public func setGlobalMIDIOutput(_ midiEndpoint: MIDIEndpointRef) {
for track in tracks {
track.setMIDIOutput(midiEndpoint)
}
}
class MusicTrackManager
里面,
给音乐音轨,添加播放资源
public func setMIDIOutput(_ endpoint: MIDIEndpointRef) {
if let track = internalMusicTrack {
MusicTrackSetDestMIDIEndpoint(track, endpoint)
}
}
播放音序器处理后的音频,很简单
/// Play the sequence
public func play() {
if let existingMusicPlayer = musicPlayer {
MusicPlayerStart(existingMusicPlayer)
}
}
MIDISampler, 音频输入
MIDISampler 继承自 AppleSampler,
AppleSampler 封装了一个 AVAudioUnitSampler
, 主要做 3 件事,
- 音频资源文件加载
- 基础的播放功能,play / stop
- 基础的播放效果控制,音量、左右声道
初始化音频采样
open class MIDISampler: AppleSampler, NamedNode {
/// MIDI 输入,也就是他采样的输出
open var midiIn = MIDIEndpointRef()
/// 起个名字
open var name = "MIDI Sampler"
/// 初始化
public init(name midiOutputName: String? = nil) {
super.init()
name = midiOutputName ?? name
enableMIDI(name: name)
// ...
// 其他事情
}
/// 提供数据给输出 midiIn ( MIDIEndpointRef )
public func enableMIDI(_ midiClient: MIDIClientRef = MIDI.sharedInstance.client,
name: String = "MIDI Sampler") {
CheckError(MIDIDestinationCreateWithBlock(midiClient, name as CFString, &midiIn) { packetList, _ in
// 音频数据处理
for e in packetList.pointee {
e.forEach { (event) in
if event.length == 3 {
do {
try self.handle(event: event)
} catch let exception {
// 错误日志
}
}
}
}
})
}
}
MIDI 数据,就是 event
private func handle(event: MIDIEvent) throws {
try self.handleMIDI(data1: event.data[0],
data2: event.data[1],
data3: event.data[2])
}