Skip to content

Instantly share code, notes, and snippets.

@aalekz
Created May 26, 2020 11:52
Show Gist options
  • Save aalekz/ca3a22953a519112fbe2edaf3eaf9aa8 to your computer and use it in GitHub Desktop.
Save aalekz/ca3a22953a519112fbe2edaf3eaf9aa8 to your computer and use it in GitHub Desktop.
OpusHandler encode/decode sample
//
// OpusExample.swift
//
import Foundation
import os
final class OpusHandler {
// swiftlint:disable:next type_name
private typealias opus_encoder = OpaquePointer
private typealias opus_decoder = OpaquePointer
private var encoder: opus_encoder // encoder to convert pcm to opus
private var decoder: opus_decoder // decoder to convert oput to pcm
private let pcmRate: Int32 = 48000 // pcm sample rate
private let pcmChannels: Int32 = 1 // pcm channels
private let pcmBytesPerFrame: Int32 = 2 // bytes per frame in the pcm audio
private var pcmData: Data = Data() // decoded pcm data
private let frameSize: Int32 = 960 // frame size
private let maxFrameSize: Int32 = 3840 // maximum size of an opus frame
private let opusRate: Int32 = 48000 // desired sample rate of the opus audio
init() throws {
// status to catch errors when creating encoder
var status = Int32(0)
encoder = opus_encoder_create(opusRate, pcmChannels, OPUS_APPLICATION_VOIP, &status)
guard let error = OpusError(rawValue: status) else {
os_log("Failed to create encoder, error: %@", log: .opus, type: .error, String(cString: opus_strerror(status)))
throw OpusError.internalError
}
guard error == .okay else { throw error }
// status to catch errors when creating decoder
status = Int32(0)
decoder = opus_decoder_create(opusRate, pcmChannels, &status)
guard let error2 = OpusError(rawValue: status) else {
os_log("Failed to create decoder, error: %@", log: .opus, type: .error, String(cString: opus_strerror(status)))
throw OpusError.internalError
}
guard error2 == .okay else { throw error }
}
deinit {
opus_encoder_destroy(encoder)
opus_decoder_destroy(decoder)
}
func encode(pcm: Data) throws -> Data {
try pcm.withUnsafeBytes { bytes in
let buffer: UnsafePointer<Float> = bytes.baseAddress!.assumingMemoryBound(to: Float.self)
return try encode(pcm: buffer, count: pcm.count)
}
}
private func encode(pcm: UnsafePointer<Float>, count: Int) throws -> Data {
// construct audio buffers
var pcm = UnsafeMutablePointer<Float>(mutating: pcm)
var opus = [UInt8](repeating: 0, count: Int(maxFrameSize))
var out = [Float](repeating: 0, count: Int(maxFrameSize))
var count = count
// number of total encoded bytes
var totalBytesEncoded = 0
var opusData = Data()
// encode complete frames
while count >= Int(frameSize) * Int(pcmBytesPerFrame) {
// encode an opus frame
let encodedBytes = opus_encode_float(encoder, pcm, frameSize, &opus, maxFrameSize)
if encodedBytes < 0 {
os_log("Encoding error: %@", log: .opus, type: .error, String(cString: opus_strerror(encodedBytes)))
throw OpusError.internalError
} else {
os_log("%@ bytes encoded", log: .opus, type: .error, String(describing: encodedBytes))
}
opusData.append(contentsOf: opus)
// decode opus frame
let decodedFrames = opus_decode_float(decoder, opus, encodedBytes, &out, maxFrameSize, 0)
if decodedFrames < 0 {
os_log("Decoding error: %@", log: .opus, type: .error, String(cString: opus_strerror(decodedFrames)))
throw OpusError.internalError
} else {
os_log("%@ frames decoded", log: .opus, type: .error, String(describing: decodedFrames))
}
let decodedOpusData = Data(buffer: UnsafeBufferPointer(start: out, count: out.count))
pcmData.append(decodedOpusData)
// advance pcm buffer
let bytesEncoded = Int(frameSize) * Int(pcmBytesPerFrame)
pcm = pcm.advanced(by: bytesEncoded / MemoryLayout<Float32>.stride)
count -= bytesEncoded
totalBytesEncoded += bytesEncoded
}
return pcmData
}
func decode(opusData: Data) throws -> Data {
return try opusData.withUnsafeBytes { bytes in
let buffer: UnsafePointer<UInt8> = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return try decode(opusData: buffer, count: opusData.count)
}
}
private func decode(opusData: UnsafePointer<UInt8>, count: Int) throws -> Data {
// construct audio buffers
var opus = UnsafeMutablePointer<UInt8>(mutating: opusData)
var out = [Float](repeating: 0, count: Int(maxFrameSize))
var count = count
// number of total decoded bytes
var totalBytesDecoded = 0
// decode complete frames
while count >= Int(frameSize) {
let decodedFrames = opus_decode_float(decoder, opus, frameSize, &out, maxFrameSize, 0)
if decodedFrames < 0 {
os_log("Decoding error: %@", log: .opus, type: .error, String(cString: opus_strerror(decodedFrames)))
throw OpusError.internalError
} else {
os_log("Decoding successful: %@ samples decoded", log: .opus, type: .error, String(describing: decodedFrames))
}
// advance opus buffer
let bytesDecoded = Int(frameSize)
opus = opus.advanced(by: bytesDecoded / MemoryLayout<UInt8>.stride)
count -= bytesDecoded
totalBytesDecoded += bytesDecoded
}
return Data(bytes: &opus, count: totalBytesDecoded)
}
}
extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!
static let opus = OSLog(subsystem: subsystem, category: "Opus")
}
enum OpusError: Error {
case okay
case badArgument
case bufferTooSmall
case internalError
case invalidPacket
case unimplemented
case invalidState
case allocationFailure
var rawValue: Int32 {
switch self {
case .okay: return OPUS_OK
case .badArgument: return OPUS_BAD_ARG
case .bufferTooSmall: return OPUS_BUFFER_TOO_SMALL
case .internalError: return OPUS_INTERNAL_ERROR
case .invalidPacket: return OPUS_INVALID_PACKET
case .unimplemented: return OPUS_UNIMPLEMENTED
case .invalidState: return OPUS_INVALID_STATE
case .allocationFailure: return OPUS_ALLOC_FAIL
}
}
init?(rawValue: Int32) {
switch rawValue {
case OPUS_OK: self = .okay
case OPUS_BAD_ARG: self = .badArgument
case OPUS_BUFFER_TOO_SMALL: self = .bufferTooSmall
case OPUS_INTERNAL_ERROR: self = .internalError
case OPUS_INVALID_PACKET: self = .invalidPacket
case OPUS_UNIMPLEMENTED: self = .unimplemented
case OPUS_INVALID_STATE: self = .invalidState
case OPUS_ALLOC_FAIL: self = .allocationFailure
default: return nil
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment