diff --git a/.gitignore b/.gitignore index da64042..fcad177 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .build .DS_Store +DerivedData +.swiftpm diff --git a/Package.swift b/Package.swift index 2a853d4..d08e1b6 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,10 @@ let package = Package( name: "Copus", targets: ["Copus"] ), + .library( + name: "Copuswrapper", + targets: ["Copuswrapper"] + ), .library( name: "Opus", targets: ["Opus", "Copus"] @@ -86,8 +90,9 @@ let package = Package( .headerSearchPath("celt/x86"), .headerSearchPath("silk"), .headerSearchPath("silk/float"), - + .define("__OPTIMIZE__"), .define("OPUS_BUILD"), + .define("CUSTOM_MODES"), .define("VAR_ARRAYS", to: "1"), .define("FLOATING_POINT"), // Enable Opus floating-point mode @@ -105,9 +110,17 @@ let package = Package( .define("HAVE_UNISTD_H", to: "1"), ] ), + .target( + name: "Copuswrapper", + dependencies: ["Copus"], + publicHeadersPath: "include", + cSettings: [ + .headerSearchPath("."), + ] + ), .target( name: "Opus", - dependencies: ["Copus"] + dependencies: ["Copus", "Copuswrapper"] ), .testTarget( name: "OpusTests", diff --git a/Sources/Copus b/Sources/Copus index 82ac57d..ccaaffa 160000 --- a/Sources/Copus +++ b/Sources/Copus @@ -1 +1 @@ -Subproject commit 82ac57d9f1aaf575800cf17373348e45b7ce6c0d +Subproject commit ccaaffa9a3ee427e9401c4dcf6462e378d9a4694 diff --git a/Sources/Copuswrapper/include/variadic-wrapper.h b/Sources/Copuswrapper/include/variadic-wrapper.h new file mode 100644 index 0000000..747a477 --- /dev/null +++ b/Sources/Copuswrapper/include/variadic-wrapper.h @@ -0,0 +1,11 @@ +#ifndef __OPUS_VARIADIC_WRAPPER_H__ +#define __OPUS_VARIADIC_WRAPPER_H__ + +//#include "" +#include +#include + +int opus_custom_encoder_ctl_wrapper(OpusCustomEncoder *OPUS_RESTRICT st, int request, opus_int32 val); +int opus_custom_decoder_ctl_wrapper(OpusCustomDecoder *OPUS_RESTRICT st, int request, opus_int32 val); + +#endif diff --git a/Sources/Copuswrapper/variadic-wrapper.c b/Sources/Copuswrapper/variadic-wrapper.c new file mode 100644 index 0000000..5478110 --- /dev/null +++ b/Sources/Copuswrapper/variadic-wrapper.c @@ -0,0 +1,11 @@ +#include "variadic-wrapper.h" + +int opus_custom_encoder_ctl_wrapper(OpusCustomEncoder *OPUS_RESTRICT st, int request, opus_int32 val) +{ + return opus_custom_encoder_ctl(st, request, val); +} + +int opus_custom_decoder_ctl_wrapper(OpusCustomDecoder *OPUS_RESTRICT st, int request, opus_int32 val) +{ + return opus_custom_decoder_ctl(st, request, val); +} diff --git a/Sources/Opus/Opus.Custom.swift b/Sources/Opus/Opus.Custom.swift new file mode 100644 index 0000000..7e42aa2 --- /dev/null +++ b/Sources/Opus/Opus.Custom.swift @@ -0,0 +1,95 @@ +import AVFoundation +import Copus +import Copuswrapper +import Foundation + +public extension Opus { + /// + /// Implements a custom opus encoder / decoder. + /// Custom implementations can have non-standard frame sizes + /// + final class Custom { + private let opusCustomMode: OpaquePointer + let encoder: Opus.Encoder + let decoder: Opus.Decoder + private let format: AVAudioFormat + public let frameSize: Int32 + + public init(format: AVAudioFormat, + application _: Application = .audio, + frameSize: UInt32 = 128) throws + { + if !format.isValidOpusPCMFormat { + throw Opus.Error.badArgument + } + self.format = format + self.frameSize = Int32(frameSize) + + var error: Opus.Error = .ok + + // Create custom parameters + guard let customMode = opus_custom_mode_create( + Int32(format.sampleRate), + Int32(frameSize), + &error.rawValue + ) else { throw error } + opusCustomMode = customMode + + // Create custom encoder + encoder = try Opus.Encoder(customOpus: opusCustomMode, + format: format, + customFrameSize: frameSize) + // Create custom decoder + decoder = try Opus.Decoder(customOpus: opusCustomMode, + format: format, + customFrameSize: frameSize) + } + + deinit { + opus_custom_mode_destroy(opusCustomMode) + } + + /// + /// Wrapper onto the opus_custom_encoder_ctl function + /// https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__encoderctls.html + /// - Parameter request The Opus CTL to change + /// - Parameter value The value to set it to + /// + /// - Returns Opus.Error code + public func encoderCtl(request: Int32, value: Int32) -> Opus.Error { + Opus.Error( + rawValue: opus_custom_encoder_ctl_wrapper(encoder.encoder, request, value) + ) + } + + /// + /// Encode a PCM buffer to data using the custom mode configuration and max size + /// - parameter avData Audio data to compress + /// - parameter compressedSize Opus packet size to compress to + /// - Returns Data containing the Opus packet + public func encode(_ avData: AVAudioPCMBuffer, + compressedSize: Int) throws -> Data + { + try encoder.encode(avData, compressedSize: compressedSize) + } + + /// + /// Decode an opus packet + /// If the data is empty, it is treated as a dropped packet + /// - Parameter data Compressed data + /// - Parameter compressedPacketSize Number of bytes of data + /// - Parameter sampleMultiplier Frame size multiplier if greater than one + /// - Returns Uncompressed audio buffer + public func decode(_ data: Data, + compressedPacketSize: Int32, + sampleMultiplier: Int32 = 1) throws -> AVAudioPCMBuffer + { + guard data.isEmpty || data.count == compressedPacketSize else { + throw Opus.Error.bufferTooSmall + } + return try decoder.decode(data, + compressedPacketSize: compressedPacketSize, + sampleMultiplier: sampleMultiplier) + } + } +} diff --git a/Sources/Opus/Opus.Decoder.swift b/Sources/Opus/Opus.Decoder.swift index 8452403..13ae452 100644 --- a/Sources/Opus/Opus.Decoder.swift +++ b/Sources/Opus/Opus.Decoder.swift @@ -5,9 +5,10 @@ public extension Opus { class Decoder { let format: AVAudioFormat let decoder: OpaquePointer + let customFrameSize: Int32? - // TODO: throw an error if format is unsupported public init(format: AVAudioFormat, application _: Application = .audio) throws { + customFrameSize = nil if !format.isValidOpusPCMFormat { throw Opus.Error.badArgument } @@ -22,6 +23,27 @@ public extension Opus { } } + public init(customOpus: OpaquePointer, + format: AVAudioFormat, + customFrameSize: UInt32) throws + { + self.customFrameSize = Int32(customFrameSize) + if !format.isValidOpusPCMFormat { + throw Opus.Error.badArgument + } + + self.format = format + + // Initialize Opus decoder + var error: Opus.Error = .ok + decoder = opus_custom_decoder_create(customOpus, + Int32(format.channelCount), + &error.rawValue) + if error != .ok { + throw error + } + } + deinit { opus_decoder_destroy(decoder) } @@ -38,28 +60,42 @@ public extension Opus { // MARK: Public decode methods public extension Opus.Decoder { - func decode(_ input: Data) throws -> AVAudioPCMBuffer { + func decode(_ input: Data, + compressedPacketSize: Int32? = nil, + sampleMultiplier: Int32 = 1) throws -> AVAudioPCMBuffer + { try input.withUnsafeBytes { + var output: AVAudioPCMBuffer let input = $0.bindMemory(to: UInt8.self) - let sampleCount = opus_decoder_get_nb_samples(decoder, input.baseAddress!, Int32($0.count)) - if sampleCount < 0 { - throw Opus.Error(sampleCount) + if compressedPacketSize != nil, let frameSize = customFrameSize { + output = AVAudioPCMBuffer( + pcmFormat: format, + frameCapacity: AVAudioFrameCount(frameSize * sampleMultiplier) + )! + } else { + let sampleCount = opus_decoder_get_nb_samples(decoder, input.baseAddress!, Int32($0.count)) + if sampleCount < 0 { + throw Opus.Error(sampleCount) + } + output = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(sampleCount))! } - let output = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(sampleCount))! - try decode(input, to: output) + + try decode(input, to: output, packetSize: compressedPacketSize) return output } } - func decode(_ input: UnsafeBufferPointer, to output: AVAudioPCMBuffer) throws { + func decode(_ input: UnsafeBufferPointer, to output: AVAudioPCMBuffer, + packetSize: Int32? = nil) throws + { let decodedCount: Int switch output.format.commonFormat { case .pcmFormatInt16: let output = UnsafeMutableBufferPointer(start: output.int16ChannelData![0], count: Int(output.frameCapacity)) - decodedCount = try decode(input, to: output) + decodedCount = try decode(input, to: output, packetSize: packetSize) case .pcmFormatFloat32: let output = UnsafeMutableBufferPointer(start: output.floatChannelData![0], count: Int(output.frameCapacity)) - decodedCount = try decode(input, to: output) + decodedCount = try decode(input, to: output, packetSize: packetSize) default: throw Opus.Error.badArgument } @@ -73,30 +109,58 @@ public extension Opus.Decoder { // MARK: Private decode methods extension Opus.Decoder { - private func decode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { - let decodedCount = opus_decode( - decoder, - input.baseAddress!, - Int32(input.count), - output.baseAddress!, - Int32(output.count), - 0 - ) + private func decode(_ input: UnsafeBufferPointer, + to output: UnsafeMutableBufferPointer, + packetSize: Int32?) throws -> Int + { + var decodedCount: Int32 = 0 + if let size = packetSize { + decodedCount = opus_custom_decode( + decoder, + input.isEmpty ? nil : input.baseAddress, + size, + output.baseAddress!, + Int32(output.count) + ) + } else { + decodedCount = opus_decode( + decoder, + input.isEmpty ? nil : input.baseAddress, + Int32(input.count), + output.baseAddress!, + Int32(output.count), + 0 + ) + } if decodedCount < 0 { throw Opus.Error(decodedCount) } return Int(decodedCount) } - private func decode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { - let decodedCount = opus_decode_float( - decoder, - input.baseAddress!, - Int32(input.count), - output.baseAddress!, - Int32(output.count), - 0 - ) + private func decode(_ input: UnsafeBufferPointer, + to output: UnsafeMutableBufferPointer, + packetSize: Int32?) throws -> Int + { + var decodedCount: Int32 = 0 + if let size = packetSize { + decodedCount = opus_custom_decode_float( + decoder, + input.isEmpty ? nil : input.baseAddress, + size, + output.baseAddress!, + Int32(output.count) + ) + } else { + decodedCount = opus_decode_float( + decoder, + input.isEmpty ? nil : input.baseAddress, + Int32(input.count), + output.baseAddress!, + Int32(output.count), + 0 + ) + } if decodedCount < 0 { throw Opus.Error(decodedCount) } diff --git a/Sources/Opus/Opus.Encoder.swift b/Sources/Opus/Opus.Encoder.swift index 37dac29..9aa95cf 100644 --- a/Sources/Opus/Opus.Encoder.swift +++ b/Sources/Opus/Opus.Encoder.swift @@ -6,9 +6,10 @@ public extension Opus { let format: AVAudioFormat let application: Application let encoder: OpaquePointer + let customFrameSize: Int32? - // TODO: throw an error if format is unsupported public init(format: AVAudioFormat, application: Application = .audio) throws { + customFrameSize = nil if !format.isValidOpusPCMFormat { throw Opus.Error.badArgument } @@ -24,11 +25,37 @@ public extension Opus { } } + public init(customOpus: OpaquePointer, + format: AVAudioFormat, + customFrameSize: UInt32, + application: Application = .audio) throws + { + self.customFrameSize = Int32(customFrameSize) + if !format.isValidOpusPCMFormat { + throw Opus.Error.badArgument + } + self.format = format + self.application = application + + // Initialize Opus encoder + var error: Opus.Error = .ok + encoder = opus_custom_encoder_create( + customOpus, + Int32(format.channelCount), + &error.rawValue + ) + if error != .ok { + throw error + } + } + deinit { opus_encoder_destroy(encoder) } public func reset() throws { + // A custom frame size means we're using opus custom + guard customFrameSize == nil else { return } let error = Opus.Error(opus_encoder_init(encoder, Int32(format.sampleRate), Int32(format.channelCount), application.rawValue)) if error != .ok { throw error @@ -47,28 +74,44 @@ public extension Opus.Encoder { return output.count } - func encode(_ input: AVAudioPCMBuffer, to output: inout [UInt8]) throws -> Int { + func encode(_ input: AVAudioPCMBuffer, compressedSize: Int) throws -> Data { + var compressed = Data(repeating: 0, count: compressedSize) + compressed.count = try compressed.withUnsafeMutableBytes( + { try encode(input, to: $0, compressedSize: compressedSize) } + ) + return compressed + } + + func encode(_ input: AVAudioPCMBuffer, to output: inout [UInt8], + compressedSize: Int? = nil) throws -> Int + { try output.withUnsafeMutableBufferPointer { - try encode(input, to: $0) + try encode(input, to: $0, compressedSize: compressedSize) } } - func encode(_ input: AVAudioPCMBuffer, to output: UnsafeMutableRawBufferPointer) throws -> Int { + func encode(_ input: AVAudioPCMBuffer, to output: UnsafeMutableRawBufferPointer, + compressedSize: Int? = nil) throws -> Int + { let output = UnsafeMutableBufferPointer(start: output.baseAddress!.bindMemory(to: UInt8.self, capacity: output.count), count: output.count) - return try encode(input, to: output) + return try encode(input, to: output, compressedSize: compressedSize) } - func encode(_ input: AVAudioPCMBuffer, to output: UnsafeMutableBufferPointer) throws -> Int { + func encode(_ input: AVAudioPCMBuffer, to output: UnsafeMutableBufferPointer, + compressedSize: Int? = nil) throws -> Int + { guard input.format.sampleRate == format.sampleRate, input.format.channelCount == format.channelCount else { throw Opus.Error.badArgument } switch format.commonFormat { case .pcmFormatInt16: - let input = UnsafeBufferPointer(start: input.int16ChannelData![0], count: Int(input.frameLength * format.channelCount)) - return try encode(input, to: output) + let input = UnsafeBufferPointer(start: input.int16ChannelData![0], + count: Int(input.frameLength * format.channelCount)) + return try encode(input, to: output, compressedSize: compressedSize) case .pcmFormatFloat32: - let input = UnsafeBufferPointer(start: input.floatChannelData![0], count: Int(input.frameLength * format.channelCount)) - return try encode(input, to: output) + let input = UnsafeBufferPointer(start: input.floatChannelData![0], + count: Int(input.frameLength * format.channelCount)) + return try encode(input, to: output, compressedSize: compressedSize) default: throw Opus.Error.badArgument } @@ -78,28 +121,58 @@ public extension Opus.Encoder { // MARK: private encode methods extension Opus.Encoder { - private func encode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { - let encodedSize = opus_encode( - encoder, - input.baseAddress!, - Int32(input.count) / Int32(format.channelCount), - output.baseAddress!, - Int32(output.count) - ) + private func encode(_ input: UnsafeBufferPointer, + to output: UnsafeMutableBufferPointer, + compressedSize: Int? = nil) throws -> Int + { + var encodedSize: Int32 = 0 + if let customSize = compressedSize, let frameSize = customFrameSize { + // Custom mode + encodedSize = opus_custom_encode( + encoder, + input.baseAddress!, + frameSize, + output.baseAddress!, + Int32(customSize) + ) + } else { + encodedSize = opus_encode( + encoder, + input.baseAddress!, + Int32(input.count) / Int32(format.channelCount), + output.baseAddress!, + Int32(output.count) + ) + } if encodedSize < 0 { throw Opus.Error(encodedSize) } return Int(encodedSize) } - private func encode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { - let encodedSize = opus_encode_float( - encoder, - input.baseAddress!, - Int32(input.count) / Int32(format.channelCount), - output.baseAddress!, - Int32(output.count) - ) + private func encode(_ input: UnsafeBufferPointer, + to output: UnsafeMutableBufferPointer, + compressedSize: Int? = nil) throws -> Int + { + var encodedSize: Int32 = 0 + if let customSize = compressedSize, let frameSize = customFrameSize { + // Custom mode + encodedSize = opus_custom_encode_float( + encoder, + input.baseAddress!, + frameSize, + output.baseAddress!, + Int32(customSize) + ) + } else { + encodedSize = opus_encode_float( + encoder, + input.baseAddress!, + Int32(input.count) / Int32(format.channelCount), + output.baseAddress!, + Int32(output.count) + ) + } if encodedSize < 0 { throw Opus.Error(encodedSize) }