07-02 03:26
Notice
Recent Posts
Recent Comments
07-02 03:26
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

pear

[iOS] record(swift) 응용 본문

iOS/swift

[iOS] record(swift) 응용

pearlab 2023. 6. 16. 10:24

Mobile/Web 등에서 간단한 녹음 API들은 일반적으로 FLOAT32LE/FLOAT32BE 사용한다.

내게 필요한 스펙은 PCM format은 S16LE 이다.

swift record - 1을 참고하여 내 스펙에 맞게 변환

 

import Foundation

import AVFoundation

 

//음성버퍼와 녹음 이벤트를 전달할 프로토콜

protocol RecordProtocol: class

{

    func onBuffer(len:UInt32, buffer:[Int16])

    func onEvent(type:typeRecord, value:Int)

}

 

//녹음 이벤트 상태 값 정의

enum typeRecord: Int{

    case start = 1, stop = 0, used = 2, err = 3

}

 

 

class RecordAudio{

    

    public static let shared = RecordAudio()

    private init(){}

    

    static let SEND_BUFFER_UNIT = 1

    static let SAMPLE_RATE_16K = 16000

    //byte size Unit

//음성버퍼를 전달할 버퍼 단위

    let FRAME_SIZE:Int = getBuffer50ms() * SEND_BUFFER_UNIT

    let SAMPLE_RATE:Double = getSampleRate()

    

    let TAG:String = "RecordAudio"

    

//음성버퍼와 녹음 이벤트를 전달할 프로토콜 등록을 위해

    weak var delegate : RecordProtocol?

    

    var isRecording     =  false

    var mAudioUnit:   AudioUnit?     = nil

    

//녹음을 위한 입력 버퍼 타입 아이폰에서는 1로 고정 인것 같다.

    private let INPUT_BUS: UInt32    =  1

    

    var mSaveBuffer = [Int16]()

    var mRecordBuffer = [Int16]()

    

    

    func startRecording(){

        if isRecording {

            delegate?.onEvent(type: typeRecord.used, value: 0)

            return

        }

        DispatchQueue.global(qos: .userInitiated).async {

            self.mRecordBuffer = [Int16]()

            self.mSaveBuffer = [Int16]()

//오디오 세션설정

            if(AudioSession.shared.startAudioSession() != 1)

            {

                self.delegate?.onEvent(type: typeRecord.err, value: 1)

                return

            }

            self.startAudioUnit()

        }

    }

    func stopRecording() {

        ALOG.i(tag: TAG, msg: "stopRecording")

        if(!isRecording)

        {

            return

        }

 

//백스라운드에서 호출 될경우 메인스레드에서 처리가 필요한 부분

        //DispatchQueue.global(qos: .userInitiated).async {

        DispatchQueue.main.async {

            AudioOutputUnitStop(self.mAudioUnit!)

            AudioUnitUninitialize(self.mAudioUnit!)

//            //remove audio

            AudioComponentInstanceDispose(self.mAudioUnit!)

            self.mAudioUnit = nil

            self.delegate?.onEvent(type: .stop, value: 1)

            self.delegate = nil

            AudioSession.shared.stopAudioSession()

            self.isRecording = false

        }

    }

    

    public func getSaveBuffer()->[Int16]{

        return mSaveBuffer

    }

    

    static func getBuffer50ms() -> Int

    {

        return Int(getSampleRate()) / 20

    }

    

    // desired audio sample rate

    static func getSampleRate() -> Double

    {

        return Double( SAMPLE_RATE_16K )

    }

    

    func startAudioUnit(){

        ALOG.d(tag: TAG, msg: "startAudioUnit called")

        var err: OSStatus = noErr

        

        if self.mAudioUnit == nil {

            setAudioUnit()         // setup once

        }

        guard let au = self.mAudioUnit

            else {

//오디오 유닛 생성 실패

            delegate?.onEvent(type: .err, value: 1)

            return

        }

        

        err = AudioUnitInitialize(au)

        if err != noErr {

//오디오 유닛 초기화 실패

            delegate?.onEvent(type: .err, value: 1)

            return

        }

        

//실제 녹음 시작 요청

        err = AudioOutputUnitStart(au)  // start

        if err == noErr {

            isRecording = true

            delegate?.onEvent(type: .start, value: 1)

        }

        else{

            delegate?.onEvent(type: .start, value: 1)

        }

    }

    

//녹음 음성 데이터 타입설정

    private func setAudioUnit() {

        var componentDesc:  AudioComponentDescription

            = AudioComponentDescription(

                componentType:          OSType(kAudioUnitType_Output),

                componentSubType:       OSType(kAudioUnitSubType_RemoteIO),

                componentManufacturer:  OSType(kAudioUnitManufacturer_Apple),

                componentFlags:         UInt32(0),

                componentFlagsMask:     UInt32(0) )

        

        let component: AudioComponent! = AudioComponentFindNext(nil, &componentDesc)

        

        var osErr: OSStatus = noErr

        osErr = AudioComponentInstanceNew(component, &self.mAudioUnit )

        if( osErr != noErr)

        {

            ALOG.w(tag: TAG, msg: "[Error]setAudioUnit \(osErr)")

            return

        }

        

        //maybe not need

        guard let au = self.mAudioUnit

            else {

            ALOG.w(tag: TAG, msg: "[FAIL]AudioComponentInstanceNew \(osErr)")

            return }

        

        // Enable I/O for input.

        var enableInput: UInt32 = 1

        

        //let packet_size = MemoryLayout<UInt32>.size

        let packet_size = MemoryLayout<UInt16>.size

        

        //EnableIO

        osErr = AudioUnitSetProperty(au,

                                     kAudioOutputUnitProperty_EnableIO,

                                     kAudioUnitScope_Input,

                                     INPUT_BUS,

                                     &enableInput,

                                     UInt32(MemoryLayout<UInt32>.size))

        

        // Set format to 16-bit, linear PCM

        let channel = 1  //1 // 2 channel stereo

        var streamFormatDesc:AudioStreamBasicDescription = AudioStreamBasicDescription(

            mSampleRate:        Double( SAMPLE_RATE ),

            mFormatID:          kAudioFormatLinearPCM,

            mFormatFlags:       ( kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked ),

            mBytesPerPacket:    UInt32(channel * packet_size),

            mFramesPerPacket:   1,

            mBytesPerFrame:     UInt32(channel * packet_size),

            mChannelsPerFrame:  UInt32(channel),

            mBitsPerChannel:    UInt32(8 * packet_size),

            mReserved:          UInt32(0)

        )

        

        //StreamFormat

        osErr = AudioUnitSetProperty(au,

                                     kAudioUnitProperty_StreamFormat,

                                     kAudioUnitScope_Output,

                                     INPUT_BUS,

                                     &streamFormatDesc,

                                     UInt32(MemoryLayout<AudioStreamBasicDescription>.size))

        

//recordCallback 등록

        var inputCallbackStruct

            = AURenderCallbackStruct(inputProc: recordCallback,

                                     inputProcRefCon:

                UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))

        

        //SetInputCallback

        osErr = AudioUnitSetProperty(au,

                                     kAudioOutputUnitProperty_SetInputCallback,

                                     kAudioUnitScope_Global,

                                     INPUT_BUS,

                                     &inputCallbackStruct,

                                     UInt32(MemoryLayout<AURenderCallbackStruct>.size))

        

        ALOG.d(tag: TAG, msg: "one_ui32 : \(enableInput)")

        ALOG.d(tag: TAG, msg: "ShouldAllocateBuffer :  \(UInt32(MemoryLayout<UInt32>.size))")

    }

    

//AURenderCallbackStruct에서 등록할 콜백 타입

    let recordCallback: AURenderCallback = { (

        inRefCon,

        ioActionFlags,

        inTimeStamp,

        inBusNumber,

        frameCount,

        ioData ) -> OSStatus in

        let audioObject = unsafeBitCast(inRefCon, to: RecordAudio.self)

        var err: OSStatus = noErr

        //ALOG.d(tag: "AURenderCallback", msg: "recordingCallback called")

        

        // set mData to nil, AudioUnitRender() should be allocating buffers

        var bufferList = AudioBufferList(

            mNumberBuffers: 1,

            mBuffers: AudioBuffer(

                mNumberChannels: UInt32(1),

                mDataByteSize: 16,

                mData: nil))

        

//        ALOG.d(tag: "AURenderCallback", msg: "frameCount : \(frameCount)")

        if let au = audioObject.mAudioUnit {

            err = AudioUnitRender(au,

                                  ioActionFlags,

                                  inTimeStamp,

                                  inBusNumber,

                                  frameCount,

                                  &bufferList)

            

        }

        //ALOG.d(tag: "AURenderCallback", msg: "inBusNumber : \(inBusNumber)")

        

        //send delegate

        audioObject.onBufferShort(bufferList:&bufferList, frameCount: frameCount)

        return 0

    }

    

    func onBufferShort(bufferList : UnsafeMutablePointer<AudioBufferList>, frameCount:UInt32){

        let point = bufferList[0].mBuffers.mData!

        let pPointer = point.bindMemory(to: Int16.self, capacity: Int(frameCount))

        let pBufPointer = UnsafeBufferPointer(start: pPointer, count: Int(frameCount));

        let bufferArray = [Int16](pBufPointer)

        

        mRecordBuffer.append(contentsOf: bufferArray)

        mSaveBuffer.append(contentsOf: bufferArray)

//음성버퍼가 지정한 만큼 모이면 등록된 Procotol로 전달

        if( FRAME_SIZE <= mRecordBuffer.count)

        {

            ALOG.d(tag: TAG, msg: "onBufferShort unit")

            let range = 0..<FRAME_SIZE

            let one_frame = Array( mRecordBuffer[range] )

            mRecordBuffer.removeSubrange(range)

            self.delegate?.onBuffer(len: bufferList[0].mBuffers.mDataByteSize, buffer: one_frame)

        }

    }

    

}

 

 

참고

FLOAT32LE - 32 bit IEEE floating point PCM little endian

FLOAT32BE - 32 bit IEEE floating point PCM big endian

S16LE - Signed 16 integer bit PCM little endian

S16BE - 1Signed 16 integer bit PCM bing endian