This article continues with the AudioToolBox and wav player
Play routine in three steps:
Data is read first and sampled data is restored by file
For Audio resource files, use Audio File Services, and Audio File Stream Services
This step, which the next two blogs focus on,
Learn AudioToolBox services from wav player
From PCM player, continue learning about AudioToolBox services with uncompressed formats
Sample data, centralized for audio buffer
Pass the PCM buffer to AVAudioPlayerNode and it will play
The third step is easier
This paper focuses on the second step, sampling data and transferring to buffer
In this article, the first step is merged into the second
Sampling data and transferring audio buffering is a common practice
Set up a timer, call the following method repeatedly, call back through AVAudioPCMBuffer to generate a new buffer,
Until the data is read, the exception reachedEndOfFile is thrown
Let isEndOfData = packetIndex >= packets.count – 1,
It returns
public func read(_ frames: AVAudioFrameCount) throws -> AVAudioPCMBuffer { let framesPerPacket = ReadFormat. StreamDescription. Pointee. MFramesPerPacket var packets = frames/framesPerPacket / / new buffer guard let buffer = AVAudioPCMBuffer(pcmFormat: readFormat, frameCapacity: Frames) else {throw ReaderError. FailedToCreatePCMBuffer} buffer. FrameLength = frames/try/fill data queue. The sync {let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) let status = AudioConverterFillComplexBuffer(converter! , ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil) guard status == noErr else { // ... throw ReaderError.reachedEndOfFile } } return buffer }Copy the code
Go through the following callback via AVAudioPCMBuffer,
The key thing is that reader.currentPacket = reader.currentPacket + 1,
Here’s the data source reader.parser.packetsx, take a data from the data source, carry one bit,
func ReaderConverterCallback(_ converter: AudioConverterRef, _ packetCount: UnsafeMutablePointer<UInt32>, _ ioData: UnsafeMutablePointer<AudioBufferList>, _ outPacketDescriptions: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>? >? , _ context: UnsafeMutableRawPointer?) -> OSStatus { let reader = Unmanaged<Reader>.fromOpaque(context!) .takeUnretainedValue() guard let _ = reader.parser.dataFormatD else { return ReaderMissingSourceFormatError } let packetIndex = Int(reader.currentPacket) let packets = reader.parser.packetsX let isEndOfData = packetIndex >= packets.count - 1 if isEndOfData { packetCount.pointee = 0 return ReaderReachedEndOfDataError } var data = packets[packetIndex] let dataCount = data.count ioData.pointee.mNumberBuffers = 1 ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: dataCount, alignment: 0) _ = data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in memcpy((ioData.pointee.mBuffers.mData? .assumingMemoryBound(to: UInt8.self))! , bytes, dataCount) } ioData.pointee.mBuffers.mDataByteSize = UInt32(dataCount) packetCount.pointee = 1 reader.currentPacket = reader.currentPacket + 1 return noErr }Copy the code
The focus is on the seek
Drag the progress bar to jump time,
When we get a moment, we use the frameOffset method to figure out what frame it is,
For PCM, a packet contains a frame,
Method packetOffset, didn’t do anything
Call the stop method of the playerNode, playernode.stop (), to clear the audio buffer allocated to the playerNode,
Get the packet number, give it to currentPacket in the code above,
In this way, the jump to play time is to clear the audio buffer of the player node, and then read the audio data at the time needed, and allocate the corresponding audio bufferbuffer
Play nodeplayerNode
In this article’s Github repo, the audio file lasts 63.158 seconds and has 1010528 frames,
16000 frames per second, the sampling rate for audio files should be 16000 Hz
In this way, cache sampling data, SEEK can be accurate to a specific frame, SEEK’s accuracy is very high, is 1/16000, up to 0.0000625 seconds
guard let frameOffset = parser.frameOffset(forTime: time), let packetOffset = parser.packetOffset(forFrame: frameOffset) else { return } // ... / / the stack, Void playerNode.stop() volume = 0 // Perform the seek to the proper packet offset do {try reader. Seek (packetOffset)} catch { os_log("Failed to seek: %@", log: Streamer.logger, type: .error, error.localizedDescription) return }Copy the code
Instead, buffer AVAudioPCMBuffer
When we initialize, we read the audio file, we get all the sampled data, just like before,
And then convert all the sampled data into an audio buffer,
The previous method is to take the corresponding sampling data as required and temporarily transform it to produce the desired audio buffer.
The current approach is to take the corresponding audio buffer as needed, without temporary conversion
Restore all sampled data first, corresponding to the previous Parser
Then get all buffers
public required init(src path: URL, readFormat readF: AVAudioFormat, bufferSize size: AVAudioFrameCount) throws { readFormat = readF readBufferSize = size Utility.check(error: AudioFileOpenURL(path as CFURL, .readPermission, 0, &playbackFile) , // set on output to the AudioFileID operation: "AudioFileOpenURL failed") guard let file = playbackFile else { return } var numPacketsToRead: UInt32 = 0 GetPropertyValue(val: &numPacketsToRead, file: file, prop: kAudioFilePropertyAudioDataPacketCount) var asbdFormat = AudioStreamBasicDescription() GetPropertyValue(val: &asbdFormat, file: file, prop: kAudioFilePropertyDataFormat) dataFormatD = AVAudioFormat(streamDescription: &asbdFormat) /// At this point we should definitely have a data format var bytesRead: UInt32 = 0 GetPropertyValue(val: &bytesRead, file: file, prop: kAudioFilePropertyAudioDataByteCount) guard let dataFormat = dataFormatD else { return } let format = dataFormat.streamDescription.pointee let bytesPerPacket = Int(format.mBytesPerPacket) for i in 0 .. < Int(numPacketsToRead) { var packetSize = UInt32(bytesPerPacket) let packetStart = Int64(i * bytesPerPacket) let dataPt: UnsafeMutableRawPointer = malloc(MemoryLayout<UInt8>.size * bytesPerPacket) AudioFileReadBytes(file, false, packetStart, &packetSize, dataPt) let startPt = dataPt.bindMemory(to: UInt8.self, capacity: bytesPerPacket) let buffer = UnsafeBufferPointer(start: startPt, count: bytesPerPacket) let array = Array(buffer) packetsX.append(Data(array)) } print("packetsX.count = \(packetsX.count)") // Front is to merge the parser / / behind is to get all the buffer let sourceFormat = dataFormat. StreamDescription let commonFormat = readF.streamDescription let result = AudioConverterNew(sourceFormat, commonFormat, &converter) guard result == noErr else { throw ReaderError.unableToCreateConverter(result) } let framesPerPacket = readFormat.streamDescription.pointee.mFramesPerPacket var packets = readBufferSize / framesPerPacket // print("frames: \(frames)") // print("framesPerPacket: \(framesPerPacket)") totalPacketCount = AVAudioPacketCount(packetsX.count) while true { /// Allocate a buffer to hold the target audio data in the Read format guard let buffer = AVAudioPCMBuffer(pcmFormat: readFormat, frameCapacity: readBufferSize) else { throw ReaderError.failedToCreatePCMBuffer } buffer.frameLength = readBufferSize // Try to read the frames from the parser let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) let status = AudioConverterFillComplexBuffer(converter! , ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil) guard status == noErr else { switch status { case ReaderMissingSourceFormatError: print("parserMissingDataFormat") throw ReaderError.parserMissingDataFormat case ReaderReachedEndOfDataError: print("reachedEndOfFile: buffers.count = \(buffers.count)") packetsX.removeAll() return case ReaderNotEnoughDataError: print("notEnoughData") throw ReaderError.notEnoughData default: print("converterFailed") throw ReaderError.converterFailed(status) } } buffers.append(buffer) } }Copy the code
And then you get buffer, and it’s easy to play,
You don’t need to transform
public func read() throws -> AVAudioPCMBuffer {
guard currentBuffer < buffers.count else {
throw ReaderError.reachedEndOfFile
}
let buff = buffers[currentBuffer]
currentBuffer += 1
return buff
}
Copy the code
Focus on seek
Drag the progress bar to adjust the playback time
So instead of being accurate to a Frame,
Because framesPerPacket = 1 in PCM data, a packet = a frame
In this github repo, the duration of the audio file is 63.158 seconds and there are 1359 audio buffers.
(The current buffer size used in this paper is 2 KB, 2048)
21.517 audio buffers per second,
One audio buffer, corresponding to 0.0465 seconds
In this way, cache sampling data, SEEK can only be accurate to the specific buffer, SEEK accuracy to 0.0465 seconds
The accuracy of packet (frame under PCM) above is 0.0000625 seconds.
This depends on the business, the high fidelity sound quality should not be acceptable, the voice to listen to the song acceptable
public func seek(buffer ratio: TimeInterval) throws {
currentBuffer = Int(TimeInterval(buffers.count) * ratio)
}
Copy the code
For example, in this paper, the buffer size is 4 KB, 4096,
There are 679 buffers with an audio duration of 63.158 seconds
10.75 buffers per second,
One audio buffer, for 0.093 seconds