Swift-AudioUnit音頻采集與播放

最近很懶状知,不想介紹詳細了。支持配置單次采樣個數孽查、采樣率饥悴、位深、聲道

Recorder

import AudioUnit
import AVFoundation

protocol WSAudioRecordDelegate: AnyObject {
    func audioRecorder(recorder: WSAudioUnitRecorder, didRecieve data: Data)
}

extension WSAudioRecordDelegate {
    func audioRecorder(recorder: WSAudioUnitRecorder, didRecieve data: Data) {}
}

class WSAudioUnitRecorder: NSObject {
    private(set) var isRecording = false
    weak var delegate: WSAudioRecordDelegate?

    private var asbd = WSASBD()

    private let InputBus: AudioUnitElement = 1
    private var perPacketSamples: UInt32 = 512
    private var inputGain: Float?
    private var ioUnit: AudioComponentInstance?
    private var bufferList: AudioBufferList?
    private var tempAudio = Data()

    override init() {
        super.init()
        setupAudioSession()
        initRecoder()
    }

    /// 開始采集
    func startRecord() {
        var error = AudioUnitInitialize(ioUnit!)
        if error != noErr {
            print("AudioUnitInitialize error: \(error)")
        }
        error = AudioOutputUnitStart(ioUnit!)
        if error != noErr {
            print("AudioOutputUnitStart error")
        }
        isRecording = true
    }

    /// 停止采集
    func stopRecord() {
        AudioUnitUninitialize(ioUnit!)
        AudioOutputUnitStop(ioUnit!)
        isRecording = false
        tempAudio = Data()
    }

    /// 設置單包采樣數
    /// - Parameter count: 單包需要的采樣點數
    func setPerPacketSamples(count: Int) {
        perPacketSamples = UInt32(count)
        setupAudioSession()
        if isRecording {
            stopRecord()
            DispatchQueue.main.asyncAfter(deadline: .now()+0.3, execute: {
                self.startRecord()
            })
        }
    }

    /// 設置采樣率
    /// - Parameter sampleRate: 采樣率
    func setSampleRate(sampleRate: Double) {
        asbd.sampleRate = sampleRate
        reSetParameter()
    }

    /// 設置聲道
    /// - Parameter channels: 1-單聲道  2-立體聲
    func setChannel(channels: UInt32) {
        asbd.channels = channels
        reSetParameter()
    }

    /// 設置采樣位深
    /// - Parameter bits: 位深 只能為 8盲再、16西设、24、32
    func setBitsPerChannel(bits: UInt32) {
        asbd.mBitsPerChannel = bits
        reSetParameter()
    }

    /// 根據ASDB設置
    /// - Parameter asbd: 音頻描述
    func setASBD(asbd: WSASBD) {
        self.asbd = asbd
        reSetParameter()
    }

    /// 設置麥克風硬件增益
    /// - Parameter inputGain: 取值范圍增益 0~1
    func setInputGain(inputGain: Float) {
        self.inputGain = inputGain
        reSetParameter()
    }
}

extension WSAudioUnitRecorder {
    private func initRecoder() {
        initBufferList(inNumberFrames: perPacketSamples)
        let _ = setupIoUnit()
    }

    private func initBufferList(inNumberFrames: UInt32) {
        let mDataByteSize: UInt32 = inNumberFrames * asbd.mBitsPerChannel / 8 * asbd.channels
        free(bufferList?.mBuffers.mData)
        bufferList = AudioBufferList.init(mNumberBuffers: 1, mBuffers: AudioBuffer.init(mNumberChannels: asbd.channels, mDataByteSize: mDataByteSize, mData: UnsafeMutableRawPointer.allocate(byteCount: Int(mDataByteSize), alignment: 1)))
//        print("設置緩沖區(qū):\(bufferList.mBuffers.mDataByteSize)")
    }

    func setupAudioSession() {
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setPreferredSampleRate(asbd.sampleRate)
            let time = Double(perPacketSamples) / asbd.sampleRate
            if asbd.sampleRate == 48000 {
                try session.setPreferredIOBufferDuration(time)
            }
            else if asbd.sampleRate == 96000 {
                try session.setPreferredIOBufferDuration(time/2)
            }else {
                try session.setPreferredIOBufferDuration(time)
            }
            
            if inputGain != nil {
                try session.setInputGain(inputGain!)
            }
            try session.setActive(true, options: .notifyOthersOnDeactivation)
        } catch {
            print(error.localizedDescription)
        }
    }

    private func reSetParameter() {
        var isRestoration = false
        if isRecording {
            stopRecord()
            isRestoration = true
        }
        setupAudioSession()
        initRecoder()
        if isRestoration {
            DispatchQueue.main.asyncAfter(deadline: .now()+0.3, execute: {
                self.startRecord()
            })
        }
    }

    private func setupIoUnit() -> Bool {
        var ioDes = AudioComponentDescription.init(
            componentType: kAudioUnitType_Output,
            componentSubType: kAudioUnitSubType_RemoteIO,
            componentManufacturer: kAudioUnitManufacturer_Apple,
            componentFlags: 0,
            componentFlagsMask: 0
        )
        guard let inputComp: AudioComponent = AudioComponentFindNext(nil, &ioDes) else {
            print("outputComp init error")
            return false
        }
        if AudioComponentInstanceNew(inputComp, &ioUnit) != noErr {
            print("io AudioComponentInstanceNew error")
            return false
        }

        var ioFormat = getAudioStreamBasicDescription(des: asbd, isInterleaved: false)
        if AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, InputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
            print("set StreamFormat error")
            return false
        }

        var value: UInt32 = 1
        if AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, InputBus, &value, UInt32(MemoryLayout.size(ofValue: value))) != noErr {
            print("can't enable input io")
            return false
        }

        var recordCallback = AURenderCallbackStruct.init(inputProc: { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData -> OSStatus in

            let bridgeSelf: WSAudioUnitRecorder = bridge(ptr: UnsafeRawPointer.init(inRefCon))
//            if bridgeSelf.asbd.sampleRate == 44100 {
                bridgeSelf.initBufferList(inNumberFrames: inNumberFrames)
//            }
            let error: OSStatus = AudioUnitRender(bridgeSelf.ioUnit!, ioActionFlags, inTimeStamp, bridgeSelf.InputBus, inNumberFrames, &bridgeSelf.bufferList!)
            if error == noErr {
                let bufferData: AudioBuffer = bridgeSelf.bufferList!.mBuffers
                if let mData = bufferData.mData {
                    let data = Data(bytes: mData, count: Int(bufferData.mDataByteSize))
//                    if bridgeSelf.asbd.sampleRate == 44100{
//                    print("采集實際大写鹋蟆:\(data.count)")
                    bridgeSelf.addTempAudio(audio: data)
//                    }else{
//                        bridgeSelf.delegate?.audioRecorder(recorder: bridgeSelf, didRecieve: data)
//                    }
                }
                
            }
            return noErr
        }, inputProcRefCon: UnsafeMutableRawPointer(mutating: bridge(obj: self)))

        if AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, InputBus, &recordCallback, UInt32(MemoryLayout.size(ofValue: recordCallback))) != noErr {
            print("SetRenderCallback error")
            return false
        }

        return true
    }
    
    private func addTempAudio(audio: Data){
        tempAudio.append(audio)
        let mDataByteSize = perPacketSamples * asbd.mBitsPerChannel / 8 * asbd.channels
        if tempAudio.count >= mDataByteSize {
            delegate?.audioRecorder(recorder: self, didRecieve: tempAudio[0...mDataByteSize-1])
            let startIndex = tempAudio.startIndex
            let endIndex = tempAudio.index(startIndex, offsetBy: Int(mDataByteSize))
            let range = startIndex ..< endIndex
            tempAudio.removeSubrange(range)
        }
//        else{
//            let count = tempAudio.count
//            var pcm = tempAudio[0..<count]
//            pcm.append(Data(repeating: 0, count: Int(mDataByteSize)-count))
//            delegate?.audioRecorder(recorder: self, didRecieve: pcm)
//            let startIndex = tempAudio.startIndex
//            let endIndex = tempAudio.index(startIndex, offsetBy: count)
//            let range = startIndex ..< endIndex
//            tempAudio.removeSubrange(range)
//        }
    }
}

Player

import AudioUnit
import AVFoundation

class WSAudioUnitPlayer: NSObject {
    private(set) var isPlaying = false

    private var cacheBufferData = ZGAudioDataBuffer()
    private var ioUnit: AudioComponentInstance?
    private var asbd = WSASBD()
    private let OutputBus: AudioUnitElement = 0
    private var perPacketSamples: UInt32 = 512

    override init() {
        super.init()
        let _ = setupIoUnit()
    }

    /// 添加需要播放的數據
    /// - Parameter data: PCM數據
    func addAudioData(data: Data) {
        guard isPlaying, !data.isEmpty else { return }
//        print("音頻長度:\(data.count)")
        cacheBufferData.appendData(data: data)
    }

    /// 開始播放
    func startPlay() {
        guard !isPlaying else { return }
        setupAudioSession()
        isPlaying = true
        cacheBufferData.clear()
        var error = AudioUnitInitialize(ioUnit!)
        if error != noErr {
            print("AudioUnitInitialize error: \(error)")
        }
        error = AudioOutputUnitStart(ioUnit!)
        if error != noErr {
            print("AudioOutputUnitStart error")
        }
    }

    /// 停止播放
    func stopPlay() {
        guard isPlaying else { return }
        isPlaying = false
        AudioUnitUninitialize(ioUnit!)
        AudioOutputUnitStop(ioUnit!)
        cacheBufferData.clear()
    }

    /// 每次需要向緩沖池填充的采樣點數
    /// 為了使生產與消費速率匹配贷揽,麥克風單包采樣數應該與播放器填充采樣數一致
    /// - Parameter count: 單包需要的采樣點數
    func setPerPacketSamples(count: Int) {
        perPacketSamples = UInt32(count)
        if isPlaying {
            stopPlay()
            startPlay()
        }
    }

    /// 設置采樣率
    /// - Parameter sampleRate: 采樣率
    func setSampleRate(sampleRate: Double) {
        asbd.sampleRate = sampleRate
        reSetParameter()
    }

    /// 設置聲道
    /// - Parameter channels: 1-單聲道  2-立體聲
    func setChannel(channels: UInt32) {
        asbd.channels = channels
        reSetParameter()
    }

    /// 設置采樣位深
    /// - Parameter bits: 位深 只能為 8、16梦碗、24禽绪、32
    func setBitsPerChannel(bits: UInt32) {
        asbd.mBitsPerChannel = bits
        reSetParameter()
    }

    /// 根據ASDB設置
    /// - Parameter asbd: 音頻描述
    func setASBD(asbd: WSASBD) {
        self.asbd = asbd
        reSetParameter()
    }
}

extension WSAudioUnitPlayer {
    private func setupIoUnit() -> Bool {
        var ioDes = AudioComponentDescription.init(
            componentType: kAudioUnitType_Output,
            componentSubType: kAudioUnitSubType_RemoteIO,
            componentManufacturer: kAudioUnitManufacturer_Apple,
            componentFlags: 0,
            componentFlagsMask: 0
        )
        guard let inputComp: AudioComponent = AudioComponentFindNext(nil, &ioDes) else {
            print("outputComp init error")
            return false
        }
        if AudioComponentInstanceNew(inputComp, &ioUnit) != noErr {
            print("io AudioComponentInstanceNew error")
            return false
        }

        var value = 1
        if AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OutputBus, &value, UInt32(MemoryLayout.size(ofValue: value))) != noErr {
            print("can't enable output io")
            return false
        }

        var ioFormat = getAudioStreamBasicDescription(des: asbd, isInterleaved: false)
        if AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, OutputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
            print("set StreamFormat error")
            return false
        }

        var playCallback = getPlayCallBack()

        if AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, OutputBus, &playCallback, UInt32(MemoryLayout.size(ofValue: playCallback))) != noErr {
            print("SetRenderCallback error")
            return false
        }
        return true
    }

    private func getPlayCallBack() -> AURenderCallbackStruct {
        return AURenderCallbackStruct.init(inputProc: { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData -> OSStatus in
            let bridgeSelf: WSAudioUnitPlayer = bridge(ptr: UnsafeRawPointer.init(inRefCon))
            let bufferData: AudioBuffer = ioData!.pointee.mBuffers
            let len = Int(bufferData.mDataByteSize)
//            print("需要大斜途取:\(len)")
            let pcm = bridgeSelf.cacheBufferData.getData(len: len)
            NSData(data: pcm).getBytes(bufferData.mData!, range: NSMakeRange(0, len))
            return noErr
        }, inputProcRefCon: UnsafeMutableRawPointer(mutating: bridge(obj: self)))
    }

    func setupAudioSession() {
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setPreferredSampleRate(asbd.sampleRate)
            let time = Double(perPacketSamples) / asbd.sampleRate
            if asbd.sampleRate == 48000 {
                try session.setPreferredIOBufferDuration(time)
            }
            else if asbd.sampleRate == 96000 {
                try session.setPreferredIOBufferDuration(time/2)
            }else {
                try session.setPreferredIOBufferDuration(time)
            }
            try session.setActive(true, options: .notifyOthersOnDeactivation)
        } catch {
            print(error.localizedDescription)
        }
    }

    private func reSetParameter() {
//        print("重置:\(asbd)")
        var isRestoration = false
        if isPlaying {
            stopPlay()
            isRestoration = true
        }
        let _ = setupIoUnit()
        if isRestoration {
            startPlay()
        }
    }
}

Other

import AudioUnit

struct WSASBD {
    /// 聲道數
    var channels: UInt32 = 1
    /// 采樣率:
    var sampleRate: Double = 48000
    /// 位深
    var mBitsPerChannel: UInt32 = 16
}

func getAudioStreamBasicDescription(des: WSASBD, isInterleaved: Bool) -> AudioStreamBasicDescription {
    var asbd = AudioStreamBasicDescription()
    let bytesPerSample = des.mBitsPerChannel / 8
    asbd.mChannelsPerFrame = des.channels
    asbd.mBitsPerChannel = 8 * bytesPerSample
    asbd.mBytesPerFrame = des.channels * bytesPerSample
    asbd.mFramesPerPacket = 1
    asbd.mBytesPerPacket = des.channels * bytesPerSample
    if isInterleaved {
        asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
    } else {
        asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked 
    }
    asbd.mFormatID = kAudioFormatLinearPCM
    asbd.mSampleRate = des.sampleRate
    asbd.mReserved = 0
    return asbd
}



func bridge<T: AnyObject>(ptr: UnsafeRawPointer) -> T {
    return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue()
}

func bridge<T: AnyObject>(obj: T) -> UnsafeRawPointer {
    return UnsafeRawPointer(Unmanaged.passUnretained(obj).toOpaque())
}


class ZGAudioDataBuffer {
    
    private var buffer = Data()
    private var startIndex = 0
    private let lock = NSLock()

    func appendData(data: Data) {
        lock.lock()
        defer { lock.unlock() }
        buffer.append(data)
       //控制緩沖區(qū)最大長度
        let cacheLen = 1024*1024*5
        if buffer.count > cacheLen {
            let excessLength = buffer.count - cacheLen
            startIndex -= excessLength
            buffer = buffer.subdata(in: excessLength..<buffer.count)
        }
    }

    func getData(len: Int) -> Data {
        
        lock.lock()
        defer { lock.unlock() }
        
        guard len > 0 else {
            return Data(repeating: 0, count: len)
        }

        let availableLength = buffer.count - startIndex
        if availableLength <= 0 {
            return Data(repeating: 0, count: len)
        }

        let actualLength = min(len, availableLength)
        var data = buffer.subdata(in: startIndex..<startIndex+actualLength)
        startIndex += actualLength
        if data.count < len {
            data.append(Data(repeating: 0, count: len-data.count))
        }
        return data
    }
    
    func clear(){
        lock.lock()
        defer { lock.unlock() }
        buffer.removeAll()
        startIndex = 0
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市印屁,隨后出現的幾起案子藻糖,更是在濱河造成了極大的恐慌,老刑警劉巖库车,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巨柒,死亡現場離奇詭異,居然都是意外死亡柠衍,警方通過查閱死者的電腦和手機洋满,發(fā)現死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來珍坊,“玉大人牺勾,你說我怎么就攤上這事≌舐” “怎么了驻民?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長履怯。 經常有香客問我回还,道長,這世上最難降的妖魔是什么叹洲? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任柠硕,我火速辦了婚禮,結果婚禮上运提,老公的妹妹穿的比我還像新娘蝗柔。我一直安慰自己,他們只是感情好民泵,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布癣丧。 她就那樣靜靜地躺著,像睡著了一般栈妆。 火紅的嫁衣襯著肌膚如雪胁编。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天签钩,我揣著相機與錄音掏呼,去河邊找鬼。 笑死铅檩,一個胖子當著我的面吹牛憎夷,可吹牛的內容都是我干的。 我是一名探鬼主播昧旨,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拾给,長吁一口氣:“原來是場噩夢啊……” “哼祥得!你這毒婦竟也來了?” 一聲冷哼從身側響起蒋得,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤级及,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后额衙,有當地人在樹林里發(fā)現了一具尸體饮焦,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年窍侧,在試婚紗的時候發(fā)現自己被綠了县踢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡伟件,死狀恐怖硼啤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情斧账,我是刑警寧澤谴返,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站咧织,受9級特大地震影響嗓袱,放射性物質發(fā)生泄漏。R本人自食惡果不足惜拯爽,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一索抓、第九天 我趴在偏房一處隱蔽的房頂上張望钧忽。 院中可真熱鬧毯炮,春花似錦、人聲如沸耸黑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽大刊。三九已至为迈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缺菌,已是汗流浹背葫辐。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伴郁,地道東北人耿战。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像焊傅,于是被迫代替她去往敵國和親剂陡。 傳聞我的和親對象是個殘疾皇子狈涮,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容