Audio Kit 播放的相关源代码,看看

443 阅读3分钟

Audio Kit 真正厉害的是,MIDI 电子乐相关,

本文简单看看 Audio Kit 播放相关的源代码

调用部分


    let engine = AudioEngine()
    let player = AudioPlayer()
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        player.isLooping = true
        engine.output = player
        do {
            try engine.start()
        } catch {
            print("AudioKit did not start! \(error)")
        }
    }

    

    //  停止播放
    func toStop(){
        player.stop()
    }
    
    
    //  播放
    func toPlay(){
        player.stop()
        let url = Bundle.main.url(forResource: "x", withExtension: "mp3")
        
        var file: AVAudioFile?
        
        do {
            file = try AVAudioFile(forReading: url!)
        } catch {
            print(error)
        }

        guard let f = file else {
            return
        }
        
        let buffer = try! AVAudioPCMBuffer(file: f)!
        player.buffer = buffer
        player.schedule(at: nil)
        player.play()
    }

播放使用 3 步:

  • 创建 engine 和 player, 指定输出 engine.output = player

再开启 engine

  • 准备播放文件

拿文件 url, 创建 AVAudioFile,

再拿 AVAudioFile,去创建 AVAudioPCMBuffer,

  • 给音频播放节点调度 AVAudioPCMBuffer,去播放

源代码部分

准备 engine 的工作,连接节点,

engine.output = player 背后的是,


/// Output node
   public var output: Node? {
       didSet {
           // AVAudioEngine doesn't allow the outputNode to be changed while the engine is running
           let wasRunning = avEngine.isRunning
           if wasRunning { stop() }

           // remove the exisiting node if it is present
           if let node = oldValue {
               mainMixerNode?.removeInput(node)
               node.detach()
               avEngine.outputNode.disconnect(input: node.avAudioNode)
           }

           // if non nil, set the main output now
           if let node = output {
               avEngine.attach(node.avAudioNode)

               // has the sample rate changed?
               if let currentSampleRate = mainMixerNode?.avAudioUnitOrNode.outputFormat(forBus: 0).sampleRate,
                   currentSampleRate != Settings.sampleRate {
                   print("Sample Rate has changed, creating new mainMixerNode at", Settings.sampleRate)
                   removeEngineMixer()
               }

               // 上面的是,完成初始化的状态,
               // 建立新状态,我们需要的
               // create the on demand mixer if needed
               createEngineMixer()
               mainMixerNode?.addInput(node)
               mainMixerNode?.makeAVConnections()
           }

           if wasRunning { try? start() }
       }
   }

里面的代码,主要是辞旧迎新

辞旧: 这个 engine, 可能不是第一次使用,把他以前的状态,给去除掉

迎新: 建立我们需要的状态

把 player 、 mixer 和 avEngine.outputNode,添加进 engine,

engine: player -> mixer -> avEngine.outputNode

   private func createEngineMixer() {
        guard mainMixerNode == nil else { return }

        let mixer = Mixer()
        avEngine.attach(mixer.avAudioNode)
        avEngine.connect(mixer.avAudioNode, to: avEngine.outputNode, format: Settings.audioFormat)
        mainMixerNode = mixer
    }

这个方法中,avEngine 添加 Mixer 节点,并且连好了

/// Add input to the mixer
    /// - Parameter node: Node to add
    public func addInput(_ node: Node) {
        guard !hasInput(node) else {
            print("🛑 Error: Node is already connected to Mixer.")
            return
        }
        connections.append(node)
        makeAVConnections()
    }

可以看出, 这里的逻辑有些冗余,

如果 mixer 有了这个节点,makeAVConnections() 才正好走一遍,

否则,一般会多调用一次

      mainMixerNode?.addInput(node)
      mainMixerNode?.makeAVConnections()

准备音频文件


extension AVAudioPCMBuffer {
   /// Read the contents of the url into this buffer
   public convenience init?(url: URL) throws {
       guard let file = try? AVAudioFile(forReading: url) else { return nil }
       try self.init(file: file)
   }

   /// Read entire file and return a new AVAudioPCMBuffer with its contents
   public convenience init?(file: AVAudioFile) throws {
       file.framePosition = 0

       self.init(pcmFormat: file.processingFormat,
                 frameCapacity: AVAudioFrameCount(file.length))

       try file.read(into: self)
   }
}

关键是这个方法, public convenience init?(file: AVAudioFile) throws

先申请一块内存,

再把音频数据,读进去

去播放

即,给资源,调度资源,去播放

player.schedule(at: nil) 调度音频资源,塞给播放节点,这一步,

public func schedule(at when: AVAudioTime? = nil) {
      if isBuffered, let buffer = buffer {
          playerNode.scheduleBuffer(buffer,
                                    at: nil,
                                    options: bufferOptions,
                                    completionCallbackType: .dataPlayedBack) { _ in
              self.internalCompletionHandler()
          }
          scheduleTime = when ?? AVAudioTime.now()

      } else if let file = file {
          playerNode.scheduleFile(file,
                                  at: when,
                                  completionCallbackType: .dataPlayedBack) { _ in
              self.internalCompletionHandler()
          }
          scheduleTime = when ?? AVAudioTime.now()

      } else {
          print("The player needs a file or a valid buffer to schedule")
          scheduleTime = nil
      }
  }

这里有两种播放的形式,音频缓冲 buffer 和音频文件 file

有了音频播放文件,才好播放

其他功能:

循环播放功能

先改记录的属性

  public var isLooping: Bool = false {
      didSet {
          bufferOptions = isLooping ? .loops : .interrupts
      }
  }

然后是循环播放的时机

调度音频资源方法,有一个完成回调

open func scheduleBuffer(_ buffer: AVAudioPCMBuffer, at when: AVAudioTime?, options: AVAudioPlayerNodeBufferOptions = [], completionCallbackType callbackType: AVAudioPlayerNodeCompletionCallbackType, completionHandler: AVAudioPlayerNodeCompletionHandler? = nil)

其回调方法中,

如果要求循环播放,则再次播放,走 play()


func internalCompletionHandler() {
      guard isPlaying, engine?.isInManualRenderingMode == false else { return }

      scheduleTime = nil
      completionHandler?()
      isPlaying = false

      if !isBuffered, isLooping, engine?.isRunning == true {
          print("Playing loop...")
          play()
          return
      }
  }

音频时长

extension AVAudioFile {
    /// Duration in seconds
    public var duration: TimeInterval {
        Double(length) / fileFormat.sampleRate
    }
}

音频资源文件,里面有多少帧,直接看 length,

第一次获取所有的帧的数目,比较消耗性能

所有的帧的数目 / 采样率,就是音频文件的时长了

github repo