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]) }