iOS播放/渲染/解析MIDI
什么是MIDI
MIDI:樂器數字接口, Musical Instrument Digital Interface。
MIDI 是計算機能理解的樂譜,計算機和電子樂器都可以處理的樂器格式。
MIDI 不是音頻信號,不包含 pcm buffer。
通過音序器 sequencer,結合音頻數據 / 樂器 ,播放 MIDI Event 數據
( 通過音色庫 SoundFont,播放樂器的聲音。iOS上一般稱sound bank )。
通過 AVAudioEngine/AVAudioSequencer 播放
連接 AVAudioEngine 的輸入和輸出,
輸入 AVAudioUnitMIDIInstrument → 混頻器 engine.mainMixerNode → 輸出 engine.outputNode
用AVAudioEngine ,創建 AVAudioSequencer ,就可以播放 MIDI 了。
配置 AVAudioEngine 的輸入輸出
var engine = AVAudioEngine() var sampler = AVAudioUnitSampler() // AVAudioUnitMIDIInstrument的子類 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 {tryself.sampler.loadSoundBankInstrument(at: bankURL,program: 0,bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB),bankLSB: UInt8(kAUSampler_DefaultBankLSB))try engine.start() } catch { print(error) }engine.mainMixerNode是AVAudioEngine自帶的node。負責混音,它有多路輸入,一路輸出。
用 AVAudioSequencer ,播放 MIDI
AVAudioSequencer 可以用不同的音頻軌道 track,對應不同的樂器聲音
tracks[index] 指向不同的音頻產生節點
加載多個音色庫
有這樣一種case,隨著業務發展,音色庫需要有更新,增加新的樂器,如果每次更新都要全量更新音色庫,流量消耗大。所以需要對音色庫做增量更新。這樣就會在客戶端出現多個音色庫,看前面的代碼,每次播放只能加載一個音色庫。那么有沒有什么辦法可以加載多個音色庫播放一個MIDI文件呢?
答案是可以的。
我們看前面的數據流圖,實際上,AudioEngine的mainMixerNode是有多路輸入的,那么它應該可以連接多個輸入Instrument。代碼類似下面:
這里,假定midiSynth0~midiSynth4是五個不同的AVAudioUnitSampler實例,他們分別加載不同的音色庫,假定為soundBank 0~4用他們來播放一個MIDI文件。我們期望,可以正常播放出MIDI中描述的所有軌道的音色。但是實際測試發現,只有midiSynth0掛載的音色庫里的音色被渲染了出來。奇怪,這是為什么呢?
實際上,答案就隱藏在之前的數據流圖上。這里,我們雖然創建了多個AVAudioUnitSampler作為input node,但是,因為我們沒有指定MIDI每一個track的destinationAudioUnit,MIDI默認所有的track都通過midiSynth0,而midiSynth0只掛載了soundBank0的音色,所以,只有soundBank0被渲染了出來。
那么,這個問題怎么解決呢?其實,之前的代碼里,我們已經給出了答案,即這一句:
指定每一個track的destinationAudioUnit為對應的掛載了改track上instrument音色的音色庫的AVAudioUnitSampler,讓對應的track上的event通過對應AudioUnitSampler,則可以渲染出完整的音色。
MIDI文件渲染成音頻文件
我們可以把MIDI文件渲染為wav/caf等音頻文件,這需要開啟AVAudioEngine的離線渲染模式。代碼如下:
/*** 渲染midi到音頻文件(wav格式)。耗時操作。* @param midiPath 要渲染的midi文件路徑* @param audioPath 輸出的音頻文件路徑*/public func render(midiPath: String, audioPath: String) {let renderEngine = AVAudioEngine()let renderMidiSynth = AVAudioUnitMIDISynth()loadSoundFont(midiSynth: renderMidiSynth, path:soundFontPath)renderEngine.attach(renderMidiSynth)renderEngine.connect(renderMidiSynth, to: renderEngine.mainMixerNode, format: nil)let renderSequencer = AVAudioSequencer(audioEngine: renderEngine)if renderSequencer.isPlaying {renderSequencer.stop()}renderSequencer.currentPositionInBeats = TimeInterval(0)do {let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)!do {let maxFrames: AVAudioFrameCount = 4096if #available(iOS 11.0, *) {try renderEngine.enableManualRenderingMode(.offline, format: format,maximumFrameCount: maxFrames)} else {fatalError("Enabling manual rendering mode failed")}} catch {fatalError("Enabling manual rendering mode failed: \(error).")}if (!renderEngine.isRunning) {do {try renderEngine.start()} catch {print("start render engine failed")}}setupSequencerFile(sequencer: renderSequencer, midiPath:midiPath)print("attempting to play")do {try renderSequencer.start()print("playing")} catch {print("cannot start \(error)")}if #available(iOS 11.0, *) {// The output buffer to which the engine renders the processed data.let buffer = AVAudioPCMBuffer(pcmFormat: renderEngine.manualRenderingFormat,frameCapacity: renderEngine.manualRenderingMaximumFrameCount)!let avChannelLayoutKey: [UInt8] = [0x02, 0x00, 0x65, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]let avChannelLayoutData = NSData.init(bytes: avChannelLayoutKey, length: 12)let settings: [String: Any] = ["AVLinearPCMIsFloatKey": 0, "AVFormatIDKey":1819304813, "AVSampleRateKey": 44100, "AVLinearPCMBitDepthKey": 16,"AVLinearPCMIsNonInterleaved": 0, "AVLinearPCMIsBigEndianKey": 1, "AVChannelLayoutKey": avChannelLayoutData, "AVNumberOfChannelsKey": 2]let outputFile: AVAudioFiledo {let outputURL = URL(fileURLWithPath: audioPath)outputFile = try AVAudioFile(forWriting: outputURL, settings: settings)} catch {fatalError("Unable to open output audio file: \(error).")}var totalFrames: Int = 0;for track in renderSequencer.tracks {let trackFrames = track.lengthInSeconds * 1000 * 44100 / (1024 / 1.0)if (Int(trackFrames) > totalFrames) {totalFrames = Int(trackFrames)}}let totalFramesCount = AVAudioFramePosition(totalFrames)while renderEngine.manualRenderingSampleTime < totalFrames {do {let frameCount = totalFramesCount - renderEngine.manualRenderingSampleTimelet framesToRender = min(AVAudioFrameCount(frameCount), buffer.frameCapacity)let status = try renderEngine.renderOffline(framesToRender, to: buffer)switch status {case .success:try outputFile.write(from: buffer)case .insufficientDataFromInputNode:breakcase .cannotDoInCurrentContext:breakcase .error:fatalError("The manual rendering failed.")}} catch {fatalError("The manual rendering failed: \(error).")}}print("isPlaying 2: " + String(renderSequencer.isPlaying) + ", " + String(renderSequencer.currentPositionInBeats) + ", " + String(renderSequencer.currentPositionInSeconds))} else {// Fallback on earlier versions}}renderEngine.stop()}渲染的流程其實和播放完全一致,只是需要為渲染單獨創建AVAudioEngine和AVAudioSequencer的實例。通過AVAudioEngine的enableManualRenderingMode開啟離線渲染模式。代碼中的settings是一個字典,存儲輸出文件相關的參數。其中各個參數的具體含義可以在蘋果開發者網站查到了。比如AVSampleRateKey表示采樣率,AVLinearPCMBitDepthKey表示采樣深度。
循環遍歷MIDI的所有track,計算出總幀數。秒數與幀的換算公式track.lengthInSeconds * 1000 * 44100 / (1024 / 1.0)。然后,循環輸出所有幀到音頻文件。
此外,還可以解析MIDI文件,獲取每個track,以及每個track對應的,編輯MIDI文件,重新生成一個新的MIDI文件。
可以從這里獲取源碼:iOS MIDI播放
總結
以上是生活随笔為你收集整理的iOS播放/渲染/解析MIDI的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 批量将文件名称转为大写
- 下一篇: TORC头盔怎么样