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