从 wav 播放器,学习 AudioToolBox 的 services

1,576 阅读4分钟

AudioToolBox 的工具分两类,

有处理音频数据的,播放即输出,录音即输入等,

主要用到的是 AVAudioEngine,

稍微底层一些,就是音频队列 Audio Queues

使用 Audio Queues 播放,即拿音频数据,注入 buffer, 将 buffer 提交给 Audio Queues

buffer 很好理解,

不用缓冲 buffer,逐个提交音频数据給声卡,也是可以的

坏处是,声卡消费数据慢,内存准备数据快,这是由计算机硬件决定的

使用 buffer, 采样数据注满到一个容量 capability, 即可以一起提交给声卡去消费

内存与声卡的性能,平衡了一些

buffer 大,声卡去消费的时间长,时间延迟 latency 大一些

buffer 小,声卡去消费快,时延小

有文件相关的,协助音频处理的,各种服务 service

Audio File Services, 处理本地音频资源文件

Audio File Stream Services, 处理流媒体,

即网络上的音频资源文件


本文的例子,主要是开发一个本地的 wav 播放器

播放器的流程,套路 3 步走

  • 读取本地的音频资源文件,还原成 UInt8 的采样数据

例子中的文件是 wav 格式,非压缩格式,

采样率 16000,位深 bit depth 是16位 pcm_s16le,

一个 16 位的采样,需要两个 UInt8 数据来表达

  • UInt8 的采样数据,转化为 AVAudioPCMBuffer

  • 最后一步,播放

将 AVAudioPCMBuffer 交给 AVAudioPlayerNode, 启动 AVAudioEngine,

让 AVAudioPlayerNode 播放,就好了

第一步对应本文例子代码中的 Parser,

第二步,Reader

第三步, Streamer

本文着重第一步,把音频文件还原为采样数据,

其余见往期博客,GitHub repo 上面有索引的


使用 Audio File Services 来还原

拿文件地址,获取文件

使用 AudioFileOpenURL, 打开文件,通过文件 ID ( AudioFileID ), 使用资源

然后去获取音频文件的属性,

使用 kAudioFilePropertyAudioDataPacketCount, 知道有多少个包 Packet,

使用 kAudioFilePropertyDataFormat, 拿到 wav 文件的描述信息 ASBD, Audio Stream Basic Description.

ASBD 里面有一个包,含有多少字节

非压缩文件,计算比较简单,每个包 packet 的大小都相等,

采样数据分布是线性的,

使用 AudioFileReadBytes, 一次读取一个包的数据,

将读取到包里面数据的头指针,分配一块内存,

指定大小 size, 数组初始化,塞到 Data 里面,就成了

接近点底层的,分配内存,完成初始化

一般用 Swift,接触不到



var playbackFile: AudioFileID?
public internal(set) var dataFormatD: AVAudioFormat?
public internal(set) var packetsX = [Data]()


public func parse(url src: URL) throws {
        Utility.check(error:  AudioFileOpenURL(src as CFURL, .readPermission, 0,  &playbackFile),  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))
        }
   
    }
    

上面是 AudioFileReadBytes, 一次读取一个包,

等价于下面,一次读取一个 UInt8 数据,


       for i in 0 ..< Int(numPacketsToRead) {
            var array = [UInt8]()
            for j in 0..<bytesPerPacket{
                var one: UInt32 = 1
                let packetStart = Int64(i * bytesPerPacket + j)
                let dataPt: UnsafeMutableRawPointer = malloc(MemoryLayout<UInt8>.size)
                AudioFileReadBytes(file, false, packetStart, &one, dataPt)
                let data = dataPt.assumingMemoryBound(to: UInt8.self).pointee
                array.append(data)
            }
            packetsX.append(Data(array))
        }

使用稍微底层点的服务,就是指针甩来甩去

获取音频文件信息的辅助方法:

都是这个套路,

先用 AudioFileGetPropertyInfo, 拿到相关信息内存大小,size

AudioFileGetProperty, 获取相关信息

然后内存大小,初始化,属性就取到了

func GetPropertyValue<T>(val value: inout T, file f: AudioFileID, prop propertyID: AudioFilePropertyID) {
    var propSize: UInt32 = 0
    guard AudioFileGetPropertyInfo(f, propertyID, &propSize, nil) == noErr else {
 
        return
    }
    
    guard AudioFileGetProperty(f, propertyID, &propSize, &value) == noErr else {
        
        return
    }
}

使用 Audio File Stream Services 来还原

先要自个取出数据,交给 Audio File Stream Services

自个拿到 sourceURL, 加载出 Data

    public var sourceURL: URL? {
        didSet {
            resetStream()

            if let src = sourceURL{
                do {
                    let data = try Data(contentsOf: src)
                    load(didReceiveData: data)
                } catch {
                    print(error)
                }
            }
        }
    }

初始化,就建立了一条音频流,

喂数据,就走两个回调方法

  • ParserPropertyChangeCallback,属性回调中,获取想要的属性

  • ParserPacketCallback, 数据回调中,还原 16 位的采样数据

上面的 Audio File Services, 先取属性,再还原采样,

Audio File Stream Services 就绕了一点点

    public init() throws {
        let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        guard AudioFileStreamOpen(context, ParserPropertyChangeCallback, ParserPacketCallback, kAudioFileWAVEType, &streamID) == noErr else {
            throw ParserError.streamCouldNotOpen
        }
    }
    

将音频文件中读取的数据,交给 Audio File Stream Services

AudioFileStreamParseBytes 将 UInt8 数据,交给一开始就建立好了的音频流 Stream

    // MARK: - Methods
    
    public func parse(data: Data) throws {
        os_log("%@ - %d", log: Parser.logger, type: .debug, #function, #line)
        
        let streamID = self.streamID!
        let count = data.count
        _ = try data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) in
            let result = AudioFileStreamParseBytes(streamID, UInt32(count), bytes, [])
            guard result == noErr else {
                os_log("Failed to parse bytes", log: Parser.logger, type: .error)
                throw ParserError.failedToParseBytes(result)
            }
        }
    }

数据到了音频流 Stream,就会走回调

在回调中,还原了 16 位的采样数据,使用两个 UInt8 表示

func ParserPacketCallback(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ data: UnsafeRawPointer, _ packetDescriptions: Optional<UnsafeMutablePointer<AudioStreamPacketDescription>>) {
    let parser = Unmanaged<Parser>.fromOpaque(context).takeUnretainedValue()
    
    /// At this point we should definitely have a data format
    guard let dataFormat = parser.dataFormatD else {
        return
    }
    
   
    let format = dataFormat.streamDescription.pointee
    let bytesPerPacket = Int(format.mBytesPerPacket)

    
    for i in 0 ..< Int(packetCount) {
        let packetStart = i * bytesPerPacket
        let packetSize = bytesPerPacket

        let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)

        parser.packetsX.append(packetData)
    }
    
}

在属性回调中,拿到音频文件的 AudioStreamBasicDescription 等信息,

具体见 GitHub repo

获取音频文件信息的辅助方法:

都是这个套路,

先拿到属性的内存大小,再拿到属性

与上面的对比,就是服务换了一下, 现在是 AudioFileStream


`func GetPropertyValue<T>(_ value: inout T, _ streamID: AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID) {
    var propSize: UInt32 = 0
    guard AudioFileStreamGetPropertyInfo(streamID, propertyID, &propSize, nil) == noErr else {
        return
    }
    
    guard AudioFileStreamGetProperty(streamID, propertyID, &propSize, &value) == noErr else {
        return
    }
}

怎么 debug?

看数据

不停的 print, 由表及里

OSStatus

代码接近底层,就很容易各种出错,

苹果会给出一个 OSStatus, 不是很好阅读

这个网站良心啊 osstatus

github repo