iOS 中 MIDI 的处理,结合 AudioKit 源代码

2,204 阅读4分钟

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

剩下的,见 github repo