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 bufferbufferPlay 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

github repo