关于ios:iOS-中-MIDI-的处理结合-AudioKit-源代码

42次阅读

共计 6210 个字符,预计需要花费 16 分钟才能阅读完成。

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

正文完
 0