Author : MD TAREQ HASSAN | Updated : 2020/09/29

Inspired by

https://github.com/robertveringa89/OpusKit

Pod dependency

  pod 'opus-ios', :git => 'https://github.com/chrisballinger/Opus-iOS.git'

object-c bridge header

https://github.com/robertveringa89/OpusKit/blob/master/OpusKit/OpusKit.h

OpuslibKit

OpuslibKit.swift


import Foundation
import AVFoundation
import opus

//
// Swift bridge for libopus
//
public class OpuslibKit {

    //
    // Singleton
    //
    private init() {}
    public static let shared = OpuslibKit()
    

    // MARK: - Properties
    
    private var decoder: OpaquePointer!
    private var encoder: OpaquePointer!
    
    private var sampleRate: opus_int32 = OpusSetting.default.sampleRate
    private var channelCount: Int32 = OpusSetting.default.channelCount
    private var frameSize: opus_int32 = OpusSetting.default.frameSize
    private var maxDataBytes: opus_int32 = OpusSetting.default.encoderBufferSize
    
    private static let DECODER_BUFFER_SIZE = 2048

    
    // MARK: - INIT
    
    public func initialize(with opusSetting: OpusSetting) {

        self.sampleRate = opusSetting.sampleRate
        self.channelCount = opusSetting.channelCount
        self.frameSize = opusSetting.frameSize
        self.maxDataBytes = opusSetting.encoderBufferSize
        
        // Status
        var status = Int32(0)
        
        // Decoder
        decoder = opus_decoder_create(sampleRate, channelCount, &status)
        if (status != OPUS_OK) {
            print("OpuslibKit - could not create decoder: \(opusErrorMessage(errorCode: status))")
        }
        
        // Encoder
        encoder = opus_encoder_create(sampleRate, channelCount, OPUS_APPLICATION_VOIP, &status)
        if (status != OPUS_OK) {
            print("OpuslibKit - could not create encoder: \(opusErrorMessage(errorCode: status))")
        }
    }
    
    // MARK: - Decode
    
    public func decode(fromBytes: [UInt8]) -> Data? {
        
        let encodedBytes = fromBytes
        
        var decodedBytes = [opus_int16](repeating: 0, count: OpuslibKit.DECODER_BUFFER_SIZE)
        
        let decodedPacketCountInBytes = opus_decode(self.decoder,
                                                    encodedBytes,
                                                    (opus_int32)(encodedBytes.count),
                                                    &decodedBytes,
                                                    self.frameSize,
                                                    0)
        
        print("decodedPacketCountInBytes: \(decodedPacketCountInBytes)")
        
        let decodedData: NSMutableData = NSMutableData()
        
        if decodedPacketCountInBytes > 0 {
            
            let decodedDataSize = Int(decodedPacketCountInBytes) * MemoryLayout<opus_int16>.size

            decodedData.append(decodedBytes, length: decodedDataSize)
            
        } else {
            
            let code = decodedPacketCountInBytes
            
            Logger.logIt("Libopus Decoding Error - Something went wrong while decoding data: \(opusErrorMessage(errorCode: code))")
        }
        
        return decodedData as Data
    }
    
    public func decode(fromData: Data) -> Data? {
        
        let encodedBytes = [UInt8](fromData)
        
        return decode(fromBytes: encodedBytes)
    }
    
    
    // MARK: - Encode
    
    public func encode(pcmData: Data) -> Data? {
        
        let pcmBytes = pcmData.toArray(type: opus_int16.self)
        
        var encodedOpus = [UInt8]()
        
        let encodedPacketCountInBytes = opus_encode(self.encoder,
                                                    pcmBytes,
                                                    self.frameSize,
                                                    &encodedOpus,
                                                    self.maxDataBytes)
        print("encodedPacketCountInBytes: \(encodedPacketCountInBytes)")
        
        
        if encodedPacketCountInBytes > 0 {
            
            var encodedData = Data()

            encodedData.append(encodedOpus, count: Int(encodedPacketCountInBytes))

            return encodedData
            
        } else {
            
            let code = encodedPacketCountInBytes
            
            Logger.logIt("Opuslib Encoding Error - Something went wrong while encoding data: \(opusErrorMessage(errorCode: code))")
            
            return nil
        }
    }

    
    // MARK: - Error message
    
    private func opusErrorMessage(errorCode: Int32) -> String {
        switch (errorCode) {
        case OPUS_BAD_ARG:
            return "One or more invalid/out of range arguments."
        case OPUS_BUFFER_TOO_SMALL:
            return "The mode struct passed is invalid."
        case OPUS_INTERNAL_ERROR:
            return "The compressed data passed is corrupted."
        case OPUS_INVALID_PACKET:
            return "Invalid/unsupported request number."
        case OPUS_INVALID_STATE:
            return "An encoder or decoder structure is invalid or already freed."
        case OPUS_UNIMPLEMENTED:
            return "Invalid/unsupported request number."
        case OPUS_ALLOC_FAIL:
            return "Memory allocation has failed."
        default:
            return "Unknown error."
        }
    }
}

OpusSetting

OpusSetting.swift

/**
 Opus encoder and decoder setting
 
 This class will be used to initialize before encoding/decoding
 */
public class OpusSetting {

    private static let OPUS_ENCODER_BUFFER_SIZE: opus_int32 = 1275 // ref: https://stackoverflow.com/a/55707654/4802664
    private static let DIVIDER_1K: opus_int32 = 1000
    
    public static let FRAME_DURATION_DEFAULT: opus_int32 = 20 //ms
    public static let SAMPLE_RATE_DEFAULT: opus_int32 = 16_000
    public static let CHANNEL_COUNT_DEFAULT: opus_int32 = 1
    private static let FRAME_SIZE_DEFAULT: opus_int32 = FRAME_DURATION_DEFAULT * (SAMPLE_RATE_DEFAULT / DIVIDER_1K) * CHANNEL_COUNT_DEFAULT
    
    //
    // Default singleton
    //
    public static let `default` = OpusSetting(sampleRate: SAMPLE_RATE_DEFAULT,
                                              channelCount: CHANNEL_COUNT_DEFAULT)

    //
    // Frame capture duration
    //
    public var frameDuration: opus_int32 = FRAME_DURATION_DEFAULT {
        willSet {
            updateFrameSize()
        }
    }
    
    //
    // Sample rate
    //
    public var sampleRate: opus_int32 = SAMPLE_RATE_DEFAULT {
        willSet {
            updateFrameSize()
        }
    }
    
    //
    // Channel count
    //
    public var channelCount: opus_int32 = CHANNEL_COUNT_DEFAULT {
        willSet {
            updateFrameSize()
        }
    }
    
    //
    // Set frame size based on other properties
    //
    public private(set) var frameSize: opus_int32 = FRAME_SIZE_DEFAULT
    private func updateFrameSize(){
        frameSize = frameDuration * (sampleRate / OpusSetting.DIVIDER_1K) * channelCount
    }
    
    //
    // Designed initilizer
    //
    public init(sampleRate: opus_int32, channelCount: opus_int32){
        self.sampleRate = sampleRate
        self.channelCount = channelCount
        self.frameSize = self.frameDuration * (self.sampleRate / OpusSetting.DIVIDER_1K) * self.channelCount
    }
    
    //
    // Buffer size of encoder
    //
    public var encoderBufferSize: opus_int32 = OPUS_ENCODER_BUFFER_SIZE
    
    //
    // Convinience initilizer
    //
    public convenience init(sampleRate: opus_int32, channelCount: opus_int32, bufferSize: opus_int32){
        self.init(sampleRate: sampleRate, channelCount: channelCount)
        self.encoderBufferSize = bufferSize
    }
}

Extension of Data class

DataExtension.swift

//
// Extension of Data class
//
extension Data {
    
    func toArray<T>(type: T.Type) -> [T] where T: ExpressibleByIntegerLiteral {
        var array = Array<T>(repeating: 0, count: self.count/MemoryLayout<T>.stride)
        _ = array.withUnsafeMutableBytes { copyBytes(to: $0) }
        return array
    }
    
    init<T>(from value: T) {
        self = Swift.withUnsafeBytes(of: value) { Data($0) }
    }
    
    func to<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
        var value: T = 0
        guard count >= MemoryLayout.size(ofValue: value) else { return nil }
        _ = Swift.withUnsafeMutableBytes(of: &value, { copyBytes(to: $0)} )
        return value
    }
}