如何在iOS中编码自解析的Opus?

3
我可以使用AVAudioRecorder录制opus,如下所示:

let opusRecordingSettings = [AVFormatIDKey: kAudioFormatOpus,
                             AVSampleRateKey: 16000.0,
                             AVNumberOfChannelsKey: 1] as [String: Any]
                             
do {
    try audioRecordingSession.setCategory(.playAndRecord, mode: .default)
    try audioRecordingSession.setActive(true)

    audioRecorder = try AVAudioRecorder(url: fileUrl(), settings: opusRecordingSettings)
    audioRecorder.delegate = self
    audioRecorder.prepareToRecord()
    audioRecorder.record()
}
catch _ { }

// ... ... ...

现在我需要按以下方式对opus进行编码:
| header | encoded opus data | header | encoded opus data | ... | ... |

每个标题指示opus数据的大小(以字节为单位)。
我猜测(如果我错了,请纠正我):
- AVAudioRecorder在录制完成后将opus包连续放置在一起(因为默认情况下opus包是非定界的)在.opus文件中。 - 默认帧持续时间为20毫秒,因此frameSize = 20 * 16 = 320(因为我使用的采样率为16 kHz)。 - 每个包恰好包含一个帧,因此packetSize == frameSize。
我理解,如果我可以以某种方式循环遍历opus包,则可以计算出每个包的大小并将其附加为标题(dataChunk = header + encodedOpusData)。
我从ietf.org/Self-Delimiting-Framing中了解到自定界opus的概念,但不知道以下内容:
  • 如何创建分隔的Opus音频文件由头部分隔,其中头部指示Opus数据的大小(以字节为单位)
  • 何时以及如何附加头部在录制期间还是录制完成后?

AVAudioRecorder的输出是什么?AVAudioRecorder无法为您提供没有任何添加标头的连续不间断的Opus数据包序列,因为该流将毫无意义。因此,我猜测涉及某些容器,也许是Ogg? - Damiano
@Damiano 不涉及任何容器。编码的Opus将发送到服务器。关于播放,我正在解码为PCM并添加WAV头以进行播放(请参见:https://stackoverflow.com/questions/59597315/playing-custom-opus-audio-file-in-ios)。解码正常,我只需要弄清如何编码为 |header|data|header|data|...|...|。我现在正在尝试使用AudioEngine,但还无法使其工作。使用MixtureNode并在其上进行点击,但只能得到静音:(,可能弄错了一些东西(即AVAudioFormat或会话或其他东西,我不知道)。 - MD TAREQ HASSAN
方程式中缺少某些东西,因为即使压缩opus cbr,帧大小也可能变化一些字节。因此,如果AVAudioRecorder没有为每个编码帧设置回调,则必须放置一些框架或使用一些容器。 - Damiano
@Damiano,帧:20毫秒,采样率:16kHz,1个通道=320字节,我将线性PCM分成640字节的块,并使用libopus(2帧)对该块进行编码,编码正常工作(我已测试)。我使用AVAudioRecorder录制了线性PCM并保存为“foo.raw”,然后使用“Data(contentsOf:...)”加载该“foo.raw”,然后将其拆分为640字节的块并提供给libopus“opus_encode(... ... ...)”。 - MD TAREQ HASSAN
然后,您应该使用opus_encode的回调来用帧长度喂入您的输出流,并且如此往复,包含封装格式以外的方式,没有普通的播放器/库能够播放。您需要在其上使用容器/流协议,以使其可供其他应用程序使用。 - Damiano
1个回答

3
我可以使用AVAudioRecorderAVAssetReader来编码自包含的opus(完整解决方案:hovermind.com/ios/libopus-kit)。以下是摘要(代码如下):
  • 使用AVAudioRecorder录制线性PCM并保存录制的音频文件,即temp.wav

  • 使用AVAssetReader读取录制的音频文件并提取PCM(请勿使用Data(contentsOf: ...)加载音频文件,音频文件中存在元数据/头文件会导致一些噪音)

  • 将PCM数据分割成x字节块并循环处理(在我的例子中,x = 640

  • 使用OpusKit pod对x字节PCM块进行编码为opus

  • 计算编码opus的大小并附加为标头

以下是代码(Swift 5,iOS 13,Xcode 11.3):
import UIKit
import MapKit
import MessageKit
import AVFoundation
import OpusKit
import os

class BasicChatViewController: ChatViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Logger.logIt(#function)
        
        Logger.logIt("Initilizing opus lib kit")
        OpusKit.shared.initialize(sampleRate: Opus.SAMPLE_RATE_DEFAULT,
                                  numberOfChannels: Opus.CHANNEL_COUNT_DEFAULT,
                                  packetSize: Opus.OPUS_ENCODER_BUFFER_SIZE,
                                  encodeBlockSize: Opus.FRAME_SIZE_DEFAULT)
                                  
        // configure record button here
    }
    
    //
    // MARK - recording
    //
    var isRecording = false
    var avAudioPlayer: AVAudioPlayer!
    var audioRecorder: AVAudioRecorder!
    
    @objc
    func onTapRecordButton(sender: UIButton){
        
        Logger.logIt(#function)
        
        toggleRecording()
    }
    
    private func toggleRecording(){
        
        Logger.logIt(#function)
        
        Logger.logIt("isRecording: \(isRecording)")
        
        if isRecording {
            
            isRecording = false
            
            stopRecording()
            
        } else {
            
            isRecording = true
            
            checkPermissionAndStartRecording()
        }
    }
    //
    // END - recording
    //
}
//
// Audio recording related extensions
//
extension BasicChatViewController: AVAudioRecorderDelegate {
    
    private func checkPermissionAndStartRecording() {
        
        Logger.logIt(#function)
        
        AudioUtil.checkRecordingPermission() { isPermissionGranted in
            
            Logger.logIt("isPermissionGranted: \(isPermissionGranted)")
            
            if isPermissionGranted {
                self.recordUsingAVAudioRecorder()
            } else {
                Logger.logIt("don't have permission to record")
            }
        }
    }
    
    private func setupRecorder() {
        
        Logger.logIt(#function)
        
        let tempAudioFileUrl = AudioUtil.TEMP_WAV_FILE
        Logger.logIt("tempAudioFileUrl: \(tempAudioFileUrl)")
        
        let linearPcmRecordingSettings = LinearPCMRecording.LINEAR_PCM_RECODING_SETTINGS_DEFAULT
        Logger.logIt("RecordingSettings: \(linearPcmRecordingSettings)")
        
        do {
            
            startRecordingSession()
            
            audioRecorder = try AVAudioRecorder(url: tempAudioFileUrl, settings: linearPcmRecordingSettings)
            audioRecorder.delegate = self
            //audioRecorder.isMeteringEnabled = true
            audioRecorder.prepareToRecord()
        }
        catch {
            Logger.logIt("\(error.localizedDescription)")
        }
    }
    
    private func startRecording() {
        
        Logger.logIt(#function)
        
        if audioRecorder == nil {
            setupRecorder()
        }
        
        audioRecorder.record()
    }
    
    private func stopRecording() {
        
        Logger.logIt(#function)
        
        guard audioRecorder != nil else {
            return
        }
        
        audioRecorder.stop()
    }
    
    private func deleteTempAudioFile(){
        
        Logger.logIt(#function)
        
        guard audioRecorder != nil else {
            return
        }
        
        if audioRecorder.isRecording {
            return
        }
        
        // delete temporary audio file
        let recordingDeleted = audioRecorder.deleteRecording()
        if recordingDeleted {
            Logger.logIt("temp (recorded) audio file deleted")
        } else {
            Logger.logIt("failed to delete temp (recorded) audio file")
        }
    }
    
    private func startRecordingSession(){
        
        Logger.logIt(#function)
        
        do {
            try AVAudioSession.sharedInstance().setCategory(.record, mode: .spokenAudio)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            Logger.logIt("Failed to deactivate recording session")
        }
    }
    
    private func stopRecordingSession(){
        
        Logger.logIt(#function)
        
        do {
            try AVAudioSession.sharedInstance().setActive(false)
        } catch {
            Logger.logIt("Failed to deactivate recording session")
        }
    }
    
    private func recordUsingAVAudioRecorder(){
        
        Logger.logIt(#function)
        
        setupRecorder()
        
        startRecording()
    }
    
    private func encodeRecordedAudio(){
        
        Logger.logIt(#function)
        
        let pcmData = AudioUtil.extractPcmOnly(from: AudioUtil.TEMP_WAV_FILE)
        
        if pcmData.count > 1 {
            
            Logger.logIt("encoding pcm to self-delimited opus")
            
            let encodedOpusData = AudioUtil.encodeToSelfDelimitedOpus(pcmData: pcmData, splitSize: PCM.SPLIT_CHUNK_SIZE_DEFAULT)
            Logger.logIt("encoded opus: \(encodedOpusData)")
            
            
            Logger.logIt("save encoded opus")
            AudioUtil.saveAudio(to: AudioUtil.ENCODED_OPUS_FILE, audioData: encodedOpusData)
            
        } else {
            Logger.logIt("no data to encode")
        }

        deleteTempAudioFile()
        stopRecordingSession()
    }
    
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        
        Logger.logIt(#function)
        
        let finishedSuccessFully = flag
        
        if finishedSuccessFully {
            
            Logger.logIt("finished recording successfully")
            
            encodeRecordedAudio()
            
        } else {
            Logger.logIt("recording failed - audio encoding error")
        }
    }
}

音频工具

import Foundation
import AVFoundation
import OpusKit


//
// Opus audio info.
//
public class OpusAudioInfo {
    
    public static let `default` = OpusAudioInfo()
    
    var channels: opus_int32
    var headerSize: Int // bytes
    var packetSize: opus_int32
    var sampleRate: opus_int32 {
        didSet {
            packetSize = Int32(Opus.FRAME_DURATION_DEFAULT) * (sampleRate / 1000)
        }
    }
    
    public init(sampleRate: opus_int32 = Opus.SAMPLE_RATE_16_KHZ,
                channels: opus_int32 = Opus.CHANNEL_COUNT_DEFAULT,
                headerSize: Int = 1) {
        self.sampleRate = sampleRate
        self.packetSize =  Int32(Opus.FRAME_DURATION_DEFAULT) * (sampleRate / 1000)
        self.channels = channels
        self.headerSize = headerSize
    }
}

//
// RAW PCM info.
//
public class PCMInfo {
    
    public static let `default` = PCMInfo()
    
    var sampleRate:Int32
    var channels:Int16
    var bitDepth:Int16
    
    public init(sampleRate:Int32 = PCM.SAMPLE_RATE_16_KHZ,
                channels:Int16 = Int16(PCM.MONO),
                bitDepth:Int16 = Int16(PCM.BIT_DEPTH_DEFAULT)) {
        self.sampleRate = sampleRate
        self.channels = channels
        self.bitDepth = bitDepth
    }
}

//
// Utility class for audio related operations
//
public class AudioUtil {
    
    private init(){}

    
    
    //
    // Default audio files url in document directory
    //
    public static let RAW_PCM_FILE = FileUtil.createFileUrl(for: "pcm.raw", in: FileUtil.DOCUMENTS_DIR)
    public static let TEMP_WAV_FILE = FileUtil.createFileUrl(for: "wav.wav", in: FileUtil.DOCUMENTS_DIR)
    public static let ENCODED_OPUS_FILE = FileUtil.createFileUrl(for: "encoded_opus_ios.opus", in: FileUtil.DOCUMENTS_DIR)
    public static let DECODED_WAV_WITH_HEADER_FILE = FileUtil.createFileUrl(for: "decoded_wav_with_header.wav", in: FileUtil.DOCUMENTS_DIR)

/**
 Creates fake wav header to play Linear PCM
 
 AVAudioPlayer by default can not play Linear PCM, therefore we need to create a fake wav header
 
 - parameter sampleRate: samples per second
 - parameter channelCount: number of channels
 - parameter bitDepth: bits per sample
 - parameter pcmDataSizeInBytes: PCM data size in bytes
 
 - returns : Data - wav header data
 */
public static func createWavHeader(sampleRate: Int32, channelCount: Int16, bitDepth: Int16, pcmDataSizeInBytes dataSize: Int32) -> Data {
    
    /*
     
     WAV header details: http://www.topherlee.com/software/pcm-tut-wavformat.html
     
     Positions    Sample Value    Description
     1 - 4    "RIFF"    Marks the file as a riff file. Characters are each 1 byte long.
     5 - 8    File size (integer)    Size of the overall file - 8 bytes, in bytes (32-bit integer). Typically, you'd fill this in after creation.
     9 -12    "WAVE"    File Type Header. For our purposes, it always equals "WAVE".
     13-16    "fmt "    Format chunk marker. Includes trailing null
     17-20    16    Length of format data as listed above
     21-22    1    Type of format (1 is PCM) - 2 byte integer
     23-24    2    Number of Channels - 2 byte integer
     25-28    44100    Sample Rate - 32 byte integer. Common values are 44100 (CD), 48000 (DAT). Sample Rate = Number of Samples per second, or Hertz.
     29-32    176400    (Sample Rate * BitsPerSample * Channels) / 8.
     33-34    4    (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16 bit mono4 - 16 bit stereo
     35-36    16    Bits per sample
     37-40    "data"    "data" chunk header. Marks the beginning of the data section.
     41-44    File size (data)    Size of the data section.
     Sample values are given above for a 16-bit stereo source.
     
     
     
     An example in swift :
     
     let WAV_HEADER: [Any] = [
     "R","I","F","F",
     0xFF,0xFF,0xFF,0x7F,  // file size
     "W","A","V","E",
     "f","m","t"," ",      // Chunk ID
     0x10,0x00,0x00,0x00,  // Chunk Size - length of format above
     0x01,0x00,            // Format Code: 1 is PCM, 3 is IEEE float
     0x01,0x00,            // Number of Channels (e.g. 2)
     0x80,0xBB,0x00,0x00,  // Samples per Second, Sample Rate (e.g. 48000)
     0x00,0xDC,0x05,0x00,  // Bytes per second, byte rate = sample rate * bits per sample * channels / 8
     0x08,0x00,            // Bytes per Sample Frame, block align = bits per sample * channels / 8
     0x20,0x00,            // bits per sample (16 for PCM, 32 for float)
     "d","a","t","a",
     0xFF,0xFF,0xFF,0x7F   // size of data section
     ]
     */
    
    let WAV_HEADER_SIZE:Int32 = 44
    let FORMAT_CODE_PCM:Int16 = 1
    
    let fileSize:Int32 = dataSize + WAV_HEADER_SIZE

    let sampleRate:Int32 = sampleRate
    let subChunkSize:Int32 = 16
    let format:Int16 = FORMAT_CODE_PCM
    let channels:Int16 = channelCount
    let bitsPerSample:Int16 = bitDepth
    let byteRate:Int32 = sampleRate * Int32(channels * bitsPerSample / 8)
    let blockAlign: Int16 = (bitsPerSample * channels) / 8
    
    let header = NSMutableData()
    
    header.append([UInt8]("RIFF".utf8), length: 4)
    
    header.append(byteArray(from: fileSize), length: 4)
    
    //WAVE
    header.append([UInt8]("WAVE".utf8), length: 4)
    
    //FMT
    header.append([UInt8]("fmt ".utf8), length: 4)
    header.append(byteArray(from: subChunkSize), length: 4)
    
    header.append(byteArray(from: format), length: 2)
    header.append(byteArray(from: channels), length: 2)
    header.append(byteArray(from: sampleRate), length: 4)
    header.append(byteArray(from: byteRate), length: 4)
    header.append(byteArray(from: blockAlign), length: 2)
    header.append(byteArray(from: bitsPerSample), length: 2)
    
    
    header.append([UInt8]("data".utf8), length: 4)
    header.append(byteArray(from: dataSize), length: 4)
    
    return header as Data
}

/**
 Creates default wav header based on default PCM constants
 
 - parameter dataSize: size of PCM data in bytes
 
 - returns : Data - wav header data
 */
public static func createDefaultWavHeader(dataSize: Int32) -> Data {
    
    return createWavHeader(sampleRate: PCM.SAMPLE_RATE_DEFAULT,
                           channelCount: Int16(PCM.CHANNEL_COUNT_DEFAULT),
                           bitDepth: Int16(PCM.BIT_DEPTH_DEFAULT),
                           pcmDataSizeInBytes: dataSize)
}

    /**
     Converts given value to byte array
     
     - parameter value:FixedWidthInteger type
     
     - returns: array of bytes
     */
    public static func byteArray<T>(from value: T) -> [UInt8] where T: FixedWidthInteger {
        // https://dev59.com/97Xna4cB1Zd3GeqPDQDk#56964191
        // .littleEndian is required
        return withUnsafeBytes(of: value.littleEndian) { Array($0) }
    }
    
    /**
     Generates wav audio data buffer from given header and raw PCM
     
     - parameter wavHeader: a fake RIFF WAV header (appended to PCM)
     - parameter pcmData: Linear PCM data
     
     - returns: Data
     */
    public static func generateWav(header wavHeader: Data, pcmData: Data) -> Data {
        
        var wavData = Data()
        
        wavData.append(wavHeader)
        wavData.append(pcmData)
        
        return wavData
    }
    
    /**
     Checks permission for recording and invokes callback with flag
     
     - parameter callback: clouser to invoked after checking permission
     */
    public static func checkRecordingPermission(onPermissionChecked callback: @escaping(_ isPermissionGranted: Bool) -> Void) {
        
        Logger.logIt(#function)
        
        var isPermissionGranted = false
        
        switch AVAudioSession.sharedInstance().recordPermission {
            
        case .granted:
            isPermissionGranted = true
            break
            
        case .denied:
            isPermissionGranted = false
            break
            
        case .undetermined:
            AVAudioSession.sharedInstance().requestRecordPermission({ (allowed) in
                if allowed {
                    isPermissionGranted = true
                } else {
                    isPermissionGranted = false
                }
            })
            break
            
        default:
            isPermissionGranted = false
            break
        }
        
        callback(isPermissionGranted)
    }

    /**
     Saves given audio data to specified url
     
     - parameter fileUri: file url where audio data will be saved
     */
    public static func saveAudio(to fileUri: URL, audioData: Data) {
        
        Logger.logIt(#function)
        Logger.logIt("save to: \(fileUri)")
        
        do {
            try audioData.write(to: fileUri)
        } catch {
            Logger.logIt(error.localizedDescription)
        }
    }
    
    /**
     Encodes given PCM data into self delimited opus (`|header|data|header|data|...|`) using libopus
     
     - parameter pcmData: Linear PCM data buffer (loaded from file or coming from AudioEngine tapping)
     - parameter splitSize: size of chunk to split the given pcmData
     - returns : encoded data (encoded as: `|header|data|header|data|...|`)
     */
    public static func encodeToSelfDelimitedOpus(pcmData: Data, splitSize: Int) -> Data {
        
        Logger.logIt(#function)
        
        var encodedData = Data()
        
        var readIndex = 0
        var readStart = 0
        var readEnd = 0
        
        var pcmChunk: Data
        
        var readCount = 1
        let splitCount = (pcmData.count / splitSize)
        Logger.logIt("split count: \(splitCount)")
        
        var header: Data
        
        while readCount <=  splitCount {
            
            readStart = readIndex
            readEnd = readStart + splitSize
            
            //
            // to prevent index out of bound exception
            // check readEnd index
            //
            if(readEnd >= pcmData.count){
                readEnd = readStart + (pcmData.count - readIndex)
            }
            
            pcmChunk = pcmData[readStart..<readEnd]
            //print("chunk: \(pcmChunk)")
            
            if let encodedChunk = OpusKit.shared.encodeData(pcmChunk) {
                
                //
                // header is exactly one byte
                // header indicates size of the encoded opus data
                //
                header = Data(from: encodedChunk.count)[0..<1]
                //Logger.logIt("header: \([UInt8](header))")
                
                encodedData.append(header)
                encodedData.append(encodedChunk)
            } else {
                print("failed to encode at index: \(readStart)")
            }
            
            readIndex += splitSize
            readCount += 1
        }
        
        //
        // remaining data
        //
        //Logger.logIt("append remaining data")
        pcmChunk = pcmData[readIndex..<pcmData.count]
        
        if let encodedChunk = OpusKit.shared.encodeData(pcmChunk) {
            header = Data(from: encodedChunk.count)[0..<1]
            //Logger.logIt("header: \([UInt8](header))")
            
            encodedData.append(header)
            encodedData.append(encodedChunk)
        } else {
            print("failed to encode at index: \(readIndex)")
        }
        
        return encodedData
    }
    
    /**
     Decodes given self delimited opus data to PCM
     
     Custom opus is encoded as `|header|data|header|data|...|`
     Loops over the data, reads data size from header and takes slice/chunk of given opus data based on data size from header. Then each chunk is decode using libopus
     
     - parameter opusData: Encoded opus data buffer
     - parameter headerSizeInBytes: size of header in bytes (default is 1)
     - returns : decoded pcm data
     */
    public static  func decodeSelfDelimitedOpusToPcm(opusData: Data, headerSizeInBytes headerSize: Int = 1) -> Data {
        
        var decodedData: Data = Data()
        
        var headerData: Data
        var opusChunkSizeFromHeader = 0
        var readIndex = 0
        var readStart = 0
        var readEnd = 0
        var extractedOpusChunk: Data
        
        
        while readIndex < opusData.count {
            
            headerData = opusData[readIndex..<(readIndex + headerSize)]
            //Logger.logIt("headerData: \([UInt8](headerData))")
            
            opusChunkSizeFromHeader = Int([UInt8](headerData)[0])
            
            readStart = readIndex + headerSize
            readEnd = readStart + opusChunkSizeFromHeader
            
            extractedOpusChunk = opusData[readStart..<readEnd]
            //Logger.logIt("extracted: \(extractedOpusChunk)")
            
            if let decodedDataChunk = OpusKit.shared.decodeData(extractedOpusChunk) {
                //Logger.logIt("decodedDataChunk: \(decodedDataChunk)")
                decodedData.append(decodedDataChunk)
            } else {
                print("failed to decode at index: \(readStart)")
            }
            
            readIndex += (headerSize + opusChunkSizeFromHeader)
        }
        
        return decodedData
    }
    
    /**
     Extracts PCM only from a audio file using AVAssetReader
     
     Normally system will append some meta data while saving audio file with extension, and therefore we need to use AVAssetReader to get PCM only
     
     - parameter fileUrl : audio file url
     
     - returns: PCM  Data
     */
    public static func extractPcmOnly(from fileUrl: URL) -> Data {
        
        let pcmOnly = NSMutableData()

        do {
            
            let asset = AVAsset(url: fileUrl)
            let assetReader = try AVAssetReader(asset: asset)
            let track = asset.tracks(withMediaType: AVMediaType.audio).first
            let outputSettings = LinearPCMRecording.LINEAR_PCM_RECODING_SETTINGS_DEFAULT
            
            let trackOutput = AVAssetReaderTrackOutput(track: track!, outputSettings: outputSettings)
            
            assetReader.add(trackOutput)
            assetReader.startReading()
            
            Logger.logIt("reading data with AVAssetReader")
            while assetReader.status == AVAssetReader.Status.reading {
                
                if let sampleBufferRef = trackOutput.copyNextSampleBuffer() {
                    
                    if let blockBufferRef = CMSampleBufferGetDataBuffer(sampleBufferRef) {
                        
                        let bufferLength = CMBlockBufferGetDataLength(blockBufferRef)
                        let data = NSMutableData(length: bufferLength)
                        
                        // func CMBlockBufferCopyDataBytes(_ theSourceBuffer: CMBlockBuffer, atOffset offsetToData: Int, dataLength: Int, destination: UnsafeMutableRawPointer) -> OSStatus
                        CMBlockBufferCopyDataBytes(blockBufferRef, atOffset: 0, dataLength: bufferLength, destination: data!.mutableBytes)
                        
                        let samples = data!.mutableBytes.assumingMemoryBound(to: UInt16.self)
                        pcmOnly.append(samples, length: bufferLength)
                        CMSampleBufferInvalidate(sampleBufferRef)
                    }
                    
                } else {
                    Logger.logIt("failed to copy next")
                }
            }
        } catch {
            Logger.logIt(error.localizedDescription)
        }
        
        return pcmOnly as Data
    }
}

附加类

import Foundation
import AVFoundation
import OpusKit


public class Audio {
    public static let SAMPLE_RATE_16_KHZ: opus_int32 = 16_000
    public static let SAMPLE_RATE_8_KHZ: opus_int32 = 8_000
    public static let SAMPLE_RATE_DEFAULT = SAMPLE_RATE_16_KHZ
    public static let MONO:Int32 = 1
    public static let CHANNEL_COUNT_DEFAULT:Int32 = MONO
    public static let BIT_DEPTH_DEFAULT:Int32 = 16
    public static let FRAME_DURATION_DEFAULT = 20 // milliseconds
    // FRAME_SIZE = FRAME (duration in millisecond) * SAMPLE_RATE
    public static let FRAME_SIZE_DEFAULT:Int32 = (SAMPLE_RATE_DEFAULT / 1000) *  Int32(FRAME_DURATION_DEFAULT)
}

public class PCM: Audio {
    public static let SPLIT_CHUNK_SIZE_DEFAULT:Int = Int(FRAME_SIZE_DEFAULT * (BIT_DEPTH_DEFAULT / 8))
}

public class WAV: Audio {
    public static let HEADER_SIZE:Int32 = 44  // always 44 bytes
    public static let WAV_HEADER_FORMAT_PCM:Int16 = 1
    public static let WAV_HEADER_SUB_CHUNK_SIZE:Int32 = 16 // always 16
}

public class Opus: Audio {
    public static let ENCODED_OUTPUT_MEMORY_SIZE_LIMIT:Int32 = 255 // Size of the allocated memory for the output payload
    public static let OPUS_ENCODER_BUFFER_SIZE:Int32 = 1275 // ref: https://stackoverflow.com/a/55707654/4802664
}

public class PCMRecordingSetting {

    private static let SAMPLE_RATE_16_KHZ = 16_000
    private static let BIT_DEPTH_16 = 16
    private static let CHANNEL_MONO = 1
 
    public var sampleRate:Int = SAMPLE_RATE_16_KHZ {
        willSet {
            updateBitRate()
            updateLinearPCMRecordingSettings()
        }
    }

    public var channelCount:Int = CHANNEL_MONO {
        willSet {
            updateBitRate()
            updateLinearPCMRecordingSettings()
        }
    }

    public var bitDepth:Int = BIT_DEPTH_16 {
        willSet {
            updateBitRate()
            updateLinearPCMRecordingSettings()
        }
    }

    public private(set) var bitRate = SAMPLE_RATE_16_KHZ * BIT_DEPTH_16 * CHANNEL_MONO
    private func updateBitRate(){
        bitRate = sampleRate * bitDepth * channelCount
    }

    public static let LINEAR_PCM_DEFAULT = [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVSampleRateKey: SAMPLE_RATE_16_KHZ,
        AVNumberOfChannelsKey: CHANNEL_MONO,
        AVLinearPCMBitDepthKey: BIT_DEPTH_16,
        AVLinearPCMIsFloatKey: false
        ] as [String : Any]
    
    public var recordingSettings = LINEAR_PCM_DEFAULT
    
    private func updateLinearPCMRecordingSettings(){
        
        Logger.debug(#function)
        
        recordingSettings = [
            AVFormatIDKey: kAudioFormatLinearPCM,
            AVSampleRateKey: sampleRate,
            AVNumberOfChannelsKey: channelCount,
            AVLinearPCMBitDepthKey: bitDepth,
            AVLinearPCMIsFloatKey: false
            ] as [String : Any]
    }

    public init(sampleRate: Int, channelCount: Int, bitDepth: Int){
        self.sampleRate = sampleRate
        self.channelCount = channelCount
        self.bitDepth = bitDepth
        updateBitRate()
        updateLinearPCMRecordingSettings()
    }

    public static let `default` = PCMRecordingSetting(sampleRate: SAMPLE_RATE_16_KHZ, channelCount: CHANNEL_MONO, bitDepth: BIT_DEPTH_16)
}

@user2875404 Opus 是一个包含一些常量的类。我已经更新了答案,请查看底部的“附加类”。 - MD TAREQ HASSAN
哦,还有LinearPCMRecording - user2875404
@user2875404,我试图让答案简短明了。现在请检查 => 我添加了所有额外的类。 - MD TAREQ HASSAN
1
根据我的理解,opusKit没有这样的功能(在我看来,您不需要设置比特率)。如果您真的想设置比特率,请检查opusKit的源代码,并直接从Swift调用“native c”代码。 - MD TAREQ HASSAN
@TuracBey 请在此处查看:https://hovermind.com/ios/libopus-kit 并查看出了什么问题。 - MD TAREQ HASSAN
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接