You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
91 lines
4.2 KiB
91 lines
4.2 KiB
import AudioToolbox
|
|
import Foundation
|
|
import OSLog
|
|
|
|
/// Encodes mono 48 kHz Float32 into 20 ms Opus packets with AudioToolbox.
|
|
/// Not thread-safe; the caller feeds it from one thread.
|
|
final class TalkbackEncoder {
|
|
static let sampleRate = 48_000.0
|
|
static let frameSamples = 960 // 20 ms
|
|
|
|
private var converter: AudioConverterRef?
|
|
private var pending: [Float] = []
|
|
private var timestamp: UInt32 = .random(in: 0...UInt32.max)
|
|
private let log = Logger(subsystem: "studio.zielke.musubi", category: "intercom")
|
|
|
|
init?(bitRate: UInt32 = 32_000) {
|
|
var input = AudioStreamBasicDescription(
|
|
mSampleRate: Self.sampleRate, mFormatID: kAudioFormatLinearPCM,
|
|
mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked,
|
|
mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 1, mBitsPerChannel: 32, mReserved: 0)
|
|
var output = AudioStreamBasicDescription(
|
|
mSampleRate: Self.sampleRate, mFormatID: kAudioFormatOpus, mFormatFlags: 0,
|
|
mBytesPerPacket: 0, mFramesPerPacket: UInt32(Self.frameSamples), mBytesPerFrame: 0, mChannelsPerFrame: 1,
|
|
mBitsPerChannel: 0, mReserved: 0)
|
|
let status = AudioConverterNew(&input, &output, &converter)
|
|
guard status == noErr, let converter else {
|
|
log.error("Opus encoder unavailable: \(status)")
|
|
return nil
|
|
}
|
|
var rate = bitRate
|
|
AudioConverterSetProperty(converter, kAudioConverterEncodeBitRate, UInt32(MemoryLayout<UInt32>.size), &rate)
|
|
}
|
|
|
|
deinit { if let converter { AudioConverterDispose(converter) } }
|
|
|
|
/// Appends mono samples at 48 kHz; returns (packet, RTP timestamp) for every full 20 ms.
|
|
func append(_ samples: UnsafeBufferPointer<Float>) -> [([UInt8], UInt32)] {
|
|
pending += samples
|
|
var out: [([UInt8], UInt32)] = []
|
|
while pending.count >= Self.frameSamples {
|
|
let frame = Array(pending.prefix(Self.frameSamples))
|
|
pending.removeFirst(Self.frameSamples)
|
|
if let packet = encode(frame) { out.append((packet, timestamp)) }
|
|
timestamp &+= UInt32(Self.frameSamples)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/// Input for one converter call; owns stable memory the converter may read after the callback returns.
|
|
private final class Feed {
|
|
let samples: UnsafeMutablePointer<Float>
|
|
let count: Int
|
|
var consumed = false
|
|
init(_ frame: [Float]) {
|
|
count = frame.count
|
|
samples = .allocate(capacity: count)
|
|
samples.initialize(from: frame, count: count)
|
|
}
|
|
deinit { samples.deallocate() }
|
|
}
|
|
|
|
private func encode(_ frame: [Float]) -> [UInt8]? {
|
|
guard let converter else { return nil }
|
|
let feed = Feed(frame)
|
|
var packetSize: UInt32 = 1500
|
|
var outBytes = [UInt8](repeating: 0, count: Int(packetSize))
|
|
var packets: UInt32 = 1
|
|
var description = AudioStreamPacketDescription()
|
|
let status = withExtendedLifetime(feed) { outBytes.withUnsafeMutableBytes { raw -> OSStatus in
|
|
var list = AudioBufferList(mNumberBuffers: 1, mBuffers: AudioBuffer(mNumberChannels: 1, mDataByteSize: packetSize, mData: raw.baseAddress))
|
|
let status = AudioConverterFillComplexBuffer(converter, { _, ioPackets, ioData, _, userData in
|
|
let feed = Unmanaged<Feed>.fromOpaque(userData!).takeUnretainedValue()
|
|
guard !feed.consumed else {
|
|
ioPackets.pointee = 0
|
|
return 1 // no more data for this call
|
|
}
|
|
feed.consumed = true
|
|
ioPackets.pointee = UInt32(feed.count)
|
|
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer(feed.samples)
|
|
ioData.pointee.mBuffers.mDataByteSize = UInt32(feed.count * 4)
|
|
ioData.pointee.mBuffers.mNumberChannels = 1
|
|
return noErr
|
|
}, Unmanaged.passUnretained(feed).toOpaque(), &packets, &list, &description)
|
|
packetSize = list.mBuffers.mDataByteSize
|
|
return status
|
|
} }
|
|
guard (status == noErr || status == 1), packets == 1, packetSize > 0 else { return nil }
|
|
return Array(outBytes.prefix(Int(packetSize)))
|
|
}
|
|
}
|