iOS播放/渲染/解析MIDI

这篇具有很好参考价值的文章主要介绍了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 了。
iOS播放/渲染/解析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 {
    try
        self.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] 指向不同的音频产生节点

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)")
}
// 指定每个track的dest AudioUnit
for track in sequencer.tracks {
    track.destinationAudioUnit = self.sampler
}

sequencer.prepareToPlay()
do {
    try sequencer.start()
} catch {
    print("\(error)")
}

加载多个音色库

有这样一种case,随着业务发展,音色库需要有更新,增加新的乐器,如果每次更新都要全量更新音色库,流量消耗大。所以需要对音色库做增量更新。这样就会在客户端出现多个音色库,看前面的代码,每次播放只能加载一个音色库。那么有没有什么办法可以加载多个音色库播放一个MIDI文件呢?
答案是可以的。
我们看前面的数据流图,实际上,AudioEngine的mainMixerNode是有多路输入的,那么它应该可以连接多个输入Instrument。代码类似下面:

engine.connect(midiSynth0, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth1, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth2, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth3, to: engine.mainMixerNode, format: nil)
engine.connect(midiSynth4, to: engine.mainMixerNode, format: nil)
engine.connect(engine.mainMixerNode, to: engine.outputNode, format: nil)    

  

这里,假定midiSynth0~midiSynth4是五个不同的AVAudioUnitSampler实例,他们分别加载不同的音色库,假定为soundBank 0~4用他们来播放一个MIDI文件。我们期望,可以正常播放出MIDI中描述的所有轨道的音色。但是实际测试发现,只有midiSynth0挂载的音色库里的音色被渲染了出来。奇怪,这是为什么呢?
实际上,答案就隐藏在之前的数据流图上。这里,我们虽然创建了多个AVAudioUnitSampler作为input node,但是,因为我们没有指定MIDI每一个track的destinationAudioUnit,MIDI默认所有的track都通过midiSynth0,而midiSynth0只挂载了soundBank0的音色,所以,只有soundBank0被渲染了出来。
那么,这个问题怎么解决呢?其实,之前的代码里,我们已经给出了答案,即这一句:

for track in sequencer.tracks {
    track.destinationAudioUnit = self.sampler
}

指定每一个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 = 4096
             if #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: AVAudioFile
             do {
                 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.manualRenderingSampleTime
                     let 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:
                         break
                     case .cannotDoInCurrentContext:
                         break
                     case .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播放文章来源地址https://www.toymoban.com/news/detail-423290.html

到了这里,关于iOS播放/渲染/解析MIDI的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android视频融合特效播放与渲染

    一个有趣且很有创意的视频特效项目。 https://github.com/duqian291902259/AlphaPlayerPlus 前言 直播产品,需要更多炫酷的礼物特效,比如飞机特效,跑车特效,生日蛋糕融特效等,融合了直播流画面的特效。所以在字节开源的alphaPlayer库和特效VAP库的基础上进行改造,实现融合特效渲染

    2023年04月13日
    浏览(32)
  • MASA MinimalAPI源码解析:为什么我们只写了一个app.MapGet,却生成了三个接口

    源码如下: AutoMapRoute自动创建map路由,MinimalAPI会根据service中的方法,创建对应的api接口。 比如上文的一个方法: MinimalAPI会帮我们生成一个Post 的Weather接口,接口地址: ParseMethod方法代码: methodName 是方法名。PostWeather方法帮我们解析方法名中的关键信息生成对应请求类型

    2024年02月02日
    浏览(41)
  • UE4 顶点网格动画播放后渲染模糊问题

    问题描述:ABC格式的顶点网格动画播放结束后,改模型看起来显得很模糊有抖动的样子 解决办法:关闭逐骨骼动态模糊

    2024年02月07日
    浏览(42)
  • 【iOS】—— 离屏渲染

    UIView继承自UIResponder ,可以处理系统传递过来的事件,如:UIApplication、UIViewController、UIView,以及所有从UIView派生出来的UIKit类。每个UIView内部都有一个CALayer提供内容的绘制和显示,并且作为内部RootLayer的代理视图。 CALayer继承自NSObject类 ,负责显示UIView提供的内容contents。

    2024年02月14日
    浏览(37)
  • 数字1渲染和其他数字大小不一致

    一、场景 手机号都是11位,但是在小程序里面会体现出长度不一样,如下图: 经过仔细观察会发现,其他数字都是对齐的,只有数字1渲染和其他数字大小不一致 二、原因 系统默认使用的是非等宽的字体 三、方法 选用等宽字体即可。例如给对应的标签添加样式:

    2023年04月08日
    浏览(40)
  • Android与IOS渲染流程对比

    目录 Android CPU计算图元信息 GPU干预 几何阶段等后处理 Android APP通过WindowManager统一提供所有Surface的缓冲区【不管是SurfaceView还是普通的布局流程都会将数据提交到Surface的BufferQuene中】 Java中的Surface是null,最终都是由Native层的Surface处理。 Native中的Surface持有的一个接口用于和

    2024年02月05日
    浏览(56)
  • iOS知识点 ---- 离屏渲染

    iOS 中的离屏渲染(Off-Screen Rendering)是指在绘制某些复杂图形或特殊效果时,系统无法直接在当前屏幕缓冲区进行绘制,而是需要先在额外的离屏缓冲区(Off-Screen Buffer)中完成渲染工作,然后再将结果混合到屏幕缓冲区的过程。离屏渲染往往发生在需要进行特定图形操作(

    2024年04月16日
    浏览(52)
  • wpf下RTSP|RTMP播放器两种渲染模式实现

    在这篇blog之前,我提到了wpf下播放RTMP和RTSP渲染的两种方式,一种是通过控件模式,另外一种是直接原生RTSP、RTMP播放模块,回调rgb,然后在wpf下渲染,本文就两种方式做个说明。 以大牛直播SDK的Windows平台SmartPlayer为例,我们先说第一种通过控件模式,控件模式,非常简单:

    2024年04月15日
    浏览(31)
  • 过去一年渲染了3亿帧,助力了63.81亿票房、1150亿播放量丨瑞云渲染年度大事记

      2022年,注定是充满未知和挑战的一年。抗疫三年,终于在2022年底迎来放开,我们怀着忐忑的心情告别了核酸、行程码和封控,成为了自己健康的第一负责人。这段时间大家应该都忙着和病毒做斗争吧,瑞云各个岗位的小伙伴们也都在抗争中交棒前行,力争不落下每个项目

    2024年02月02日
    浏览(36)
  • iOS渲染卡死应该如何解决

    1)iOS渲染卡死应该如何解决 2)C#传给C++的Byte数组如何释放 3)EAssetBundle.Unload(true)触发长时间卡顿的原因 这是第358篇UWA技术知识分享的推送,精选了UWA社区的热门话题,涵盖了UWA问答、社区帖子等技术知识点,助力大家更全面地掌握和学习。 Q:想问问大家是否有遇到过iOS渲

    2024年02月05日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包