Hello @LookingForFont, please see below a very basic example to get you started. It taps the engine's input node and uses AVAudioConverter to convert back and forth between PCM and Opus. The example uses InputStream and OutputStream, but it should be straightforward to adapt it to your needs. Note that the example omits checks while unwrapping optionals for simplicity, but an actual implementation should not skip these checks. Please also refer to TN3136 for more information on AVAudioConverter.
import AVFAudio
class AudioManager {
let sizeOfFloat = UInt32(MemoryLayout<Float>.size)
let engine = AVAudioEngine()
var outBuffer: AVAudioPCMBuffer!
var playerNode: AVAudioPlayerNode!
var converter: AVAudioConverter!
var micFormat: AVAudioFormat!
let opusFormat : AVAudioFormat = {
var opusDesc = AudioStreamBasicDescription()
opusDesc.mSampleRate = 48000
opusDesc.mFormatID = kAudioFormatOpus
opusDesc.mChannelsPerFrame = 1
opusDesc.mFramesPerPacket = 960
return AVAudioFormat(streamDescription: &opusDesc)!
}()
func getOffsetBuffer(buffer: AVAudioPCMBuffer, offset: UInt32, count: UInt32) -> AVAudioPCMBuffer? {
let data = UnsafeMutableAudioBufferListPointer(buffer.mutableAudioBufferList).first!.mData! + UnsafeMutableRawPointer.Stride(offset * sizeOfFloat)
var abl = AudioBufferList(mNumberBuffers: 1, mBuffers: AudioBuffer(mNumberChannels: 1, mDataByteSize: count * sizeOfFloat, mData: data))
return AVAudioPCMBuffer(pcmFormat: buffer.format, bufferListNoCopy: &abl)
}
func record(outputStream: OutputStream) {
let sampleRate = engine.outputNode.outputFormat(forBus: 0).sampleRate
micFormat = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)
converter = AVAudioConverter(from: micFormat, to: opusFormat)
converter.bitRateStrategy = AVAudioBitRateStrategy_Constant
engine.inputNode.installTap(onBus: 0, bufferSize: 0, format: micFormat) { buffer, time in
self.encode(buffer: buffer, outputStream: outputStream)
}
let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(.playAndRecord)
try? audioSession.setPreferredIOBufferDuration(1024.0 / sampleRate)
try? audioSession.setActive(true)
try? engine.start()
}
func encode(buffer: AVAudioPCMBuffer, outputStream: OutputStream) {
let bufferLength = buffer.frameLength
var offset: UInt32 = 0
var done = false
while !done {
let opusBuffer = AVAudioCompressedBuffer(format: opusFormat, packetCapacity: 1, maximumPacketSize: converter.maximumOutputPacketSize)
let outputStatus = converter.convert(to: opusBuffer, error: nil) { packetCount, inputStatus in
let count = min(packetCount, buffer.frameLength - offset)
if count == 0 {
inputStatus.pointee = .noDataNow
return nil
}
let buffer = self.getOffsetBuffer(buffer: buffer, offset: offset, count: count)
offset += count
inputStatus.pointee = .haveData
return buffer
}
if outputStatus == .haveData {
outputStream.write(opusBuffer.data, maxLength: 128)
}
if offset >= bufferLength {
done = true
}
}
}
func play(inputStream: InputStream) {
engine.stop()
engine.inputNode.removeTap(onBus: 0)
let sampleRate = engine.outputNode.outputFormat(forBus: 0).sampleRate
micFormat = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)
converter = AVAudioConverter(from: opusFormat, to: micFormat)
let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(.playAndRecord)
try? audioSession.setPreferredIOBufferDuration(1024.0 / sampleRate)
try? audioSession.setActive(true)
outBuffer = AVAudioPCMBuffer(pcmFormat: micFormat, frameCapacity: 1024)
playerNode = AVAudioPlayerNode()
engine.attach(playerNode)
engine.connect(playerNode, to: engine.mainMixerNode, format: micFormat)
try? engine.start()
playerNode.play()
schedule(inputStream: inputStream)
}
func schedule(inputStream: InputStream) {
decode(inputStream: inputStream)
playerNode.scheduleBuffer(outBuffer) {
self.schedule(inputStream: inputStream)
}
}
func decode(inputStream: InputStream) {
var offset: UInt32 = 0
var done = false
while !done {
let outputStatus = converter.convert(to: outBuffer, error: nil) { packetCount, inputStatus in
let opusBuffer = AVAudioCompressedBuffer(format: self.opusFormat, packetCapacity: 1, maximumPacketSize: 128)
inputStream.read(opusBuffer.data, maxLength: 128)
opusBuffer.packetCount = 1
opusBuffer.byteLength = 128
opusBuffer.packetDescriptions![0].mDataByteSize = 128
inputStatus.pointee = .haveData
return opusBuffer
}
if outputStatus == .haveData {
offset += UInt32(outBuffer.frameLength)
}
if offset >= 1024 {
done = true
}
}
}
}