What’s really cool about Audio Kit is that MIDI is related to electronic music,
This article briefly looks at the Audio Kit playback related source code
Call part
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)")}} // stop playing func toStop(){player.stop()} // Play 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() }Copy the code
Play using 3 steps:
- Create engine and Player and specify the output
engine.output = player
Open the engine again
- Preparing to play the file
Take the url of the file, create AVAudioFile,
I’m going to take AVAudioFile, I’m going to create AVAudioPCMBuffer,
- Schedule AVAudioPCMBuffer for audio player node to play
Source code
Prepare the engine to work, connect the nodes,
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() } } }Copy the code
Inside the code, mainly is to ring in the New Year
Deprecated: this engine, probably not the first time used, should be removed from its previous state
Orientation: Establish the state we need
Add Player, Mixer, and avEngine. OutputNode to 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
}
Copy the code
In this method, avEngine adds and connects the Mixer nodes
/// 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() }Copy the code
As you can see, the logic here is a little redundant,
If mixer had this node, makeAVConnections() would just go once,
Otherwise, it is generally called one more time
mainMixerNode? .addInput(node) mainMixerNode? .makeAVConnections()Copy the code
Preparing audio Files
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) } }Copy the code
The key is this method, public convenience init? (file: AVAudioFile) throws
We need to get a block of memory,
And read in the audio data
Go to play
That is, give resources, schedule resources to play
Player.schedule (at: nil) schedules audio resources to the player node.
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 } }Copy the code
There are two types of playback, audio buffer and audio file
With audio playback files, it is good to play
Other features:
Loop play function
Change the attributes of the record first
public var isLooping: Bool = false {
didSet {
bufferOptions = isLooping ? .loops : .interrupts
}
}
Copy the code
Then there’s the timing of the loop
Scheduling audio resource methods that have a completion callback
open func scheduleBuffer(_ buffer: AVAudioPCMBuffer, at when: AVAudioTime? , options: AVAudioPlayerNodeBufferOptions = [], completionCallbackType callbackType: AVAudioPlayerNodeCompletionCallbackType, completionHandler: AVAudioPlayerNodeCompletionHandler? = nil)Copy the code
In its callback method,
If loop is required, play again, go 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 } }Copy the code
The audio time
extension AVAudioFile {
/// Duration in seconds
public var duration: TimeInterval {
Double(length) / fileFormat.sampleRate
}
}
Copy the code
Audio resource file, how many frames are in it, just look at length,
Get the number of all frames for the first time
The total number of frames/sample rate is the length of the audio file