├── .editorconfig ├── .github ├── dependabot.yaml └── workflows │ └── swift.yaml ├── .gitignore ├── .gitmodules ├── .swift-version ├── .swiftformat ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Copuswrapper │ ├── include │ │ └── variadic-wrapper.h │ └── variadic-wrapper.c └── Opus │ ├── AVAudioFormat+Extensions.swift │ ├── AVAudioFrameCount+Extensions.swift │ ├── Double+Extensions.swift │ ├── Opus.Application.swift │ ├── Opus.Decoder.swift │ ├── Opus.Encoder.swift │ ├── Opus.Error.swift │ └── Opus.swift └── Tests └── OpusTests ├── AVAudioFormatTests.swift ├── OpusApplicationTests.swift ├── OpusDecoderTests.swift ├── OpusEncoderTests.swift ├── OpusErrorTests.swift ├── OpusRoundTripTests.swift ├── OpusTests.swift └── Resources ├── MuteMono.wav └── UnmuteMono.wav /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | tab_width = 8 7 | max_line_length = 2048 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | 8 | - package-ecosystem: gitsubmodule 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/workflows/swift.yaml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/swift.yaml" 9 | - ".gitmodules" 10 | - "**.swift" 11 | - "Package.resolved" 12 | - "Sources/Copus" 13 | pull_request: 14 | paths: 15 | - ".github/workflows/swift.yaml" 16 | - ".gitmodules" 17 | - "**.swift" 18 | - "Package.resolved" 19 | - "Sources/Copus" 20 | 21 | jobs: 22 | swiftformat: 23 | name: SwiftFormat 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repo 27 | uses: actions/checkout@v3 28 | with: 29 | submodules: recursive 30 | 31 | - name: Install SwiftFormat 32 | uses: Cyberbeni/install-swift-tool@v2 33 | with: 34 | url: https://github.com/nicklockwood/SwiftFormat 35 | 36 | - name: Format Swift code 37 | run: swiftformat --verbose . 38 | 39 | - name: Verify formatted code is unchanged 40 | run: git diff --exit-code HEAD -w -G'(^[^# /])|(^#\w)|(^\s+[^#/])' # Ignore whitespace and comments 41 | 42 | test: 43 | name: Test 44 | runs-on: macos-latest 45 | timeout-minutes: 10 46 | steps: 47 | - name: Checkout repo 48 | uses: actions/checkout@v3 49 | with: 50 | submodules: recursive 51 | 52 | - name: Test 53 | run: swift test 54 | 55 | - name: Generate release build 56 | run: swift build -c release 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | .DS_Store 3 | DerivedData 4 | .swiftpm 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Sources/Copus"] 2 | path = Sources/Copus 3 | url = https://github.com/xiph/opus.git 4 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.3 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --exclude **/.build 2 | --exclude **/.git 3 | --exclude **/.swiftpm 4 | --indent tab 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions which should be recommended for users of this workspace. 3 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 4 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 5 | "recommendations": [ 6 | "davidanson.vscode-markdownlint", 7 | "eamodio.gitlens", 8 | "eriklynd.json-tools", 9 | "foxundermoon.shell-format", 10 | "redhat.vscode-yaml", 11 | "stkb.rewrap", 12 | "vknabel.vscode-swiftformat", 13 | "xaver.clang-format", 14 | ], 15 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 16 | "unwantedRecommendations": [] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Editor 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "editor.detectIndentation": false, 7 | "editor.formatOnSave": true, 8 | "editor.insertSpaces": false, 9 | "editor.tabSize": 8, 10 | "files.eol": "\n", 11 | // 12 | // Files 13 | "files.exclude": { 14 | "**/.build": true, 15 | "**/.git": true, 16 | // "Sources/Copus": true, 17 | }, 18 | "files.associations": {}, 19 | "files.insertFinalNewline": true, 20 | "files.watcherExclude": { 21 | "**/.git/objects/**": true, 22 | "**/.git/subtree-cache/**": true, 23 | }, 24 | "files.trimTrailingWhitespace": true, 25 | // 26 | // HTML 27 | "html.format.contentUnformatted": "pre,code,textarea,script", 28 | "html.format.unformatted": "pre,code,textarea,script,wbr", 29 | "html.format.endWithNewline": true, 30 | // 31 | // JSON 32 | "[json]": { 33 | "editor.defaultFormatter": "vscode.json-language-features", 34 | }, 35 | "[jsonc]": { 36 | "editor.defaultFormatter": "vscode.json-language-features", 37 | }, 38 | // 39 | // Make 40 | "[makefile]": { 41 | "editor.tabSize": 8 42 | }, 43 | // 44 | // Markdown 45 | "[markdown]": { 46 | "editor.rulers": [ 47 | 80 48 | ], 49 | "editor.codeActionsOnSave": { 50 | "source.fixAll.markdownlint": true 51 | } 52 | }, 53 | "markdown.preview.breaks": true, 54 | "markdownlint.config": { 55 | "default": true, 56 | "MD022": false, 57 | "MD024": false, 58 | "MD032": false 59 | }, 60 | // 61 | // Property Lists 62 | "[plist]": { 63 | "editor.formatOnSave": true, 64 | "editor.tabSize": 8, 65 | }, 66 | // 67 | // Shell 68 | "[shellscript]": { 69 | "editor.defaultFormatter": "foxundermoon.shell-format", 70 | }, 71 | // 72 | // Swift 73 | "[swift]": { 74 | "editor.detectIndentation": true, 75 | "editor.insertSpaces": false, 76 | "editor.tabSize": 8, 77 | }, 78 | // 79 | // YAML 80 | "[yaml]": { 81 | "editor.autoIndent": "keep", 82 | "editor.detectIndentation": true, 83 | "editor.insertSpaces": true, 84 | "editor.tabSize": 2, 85 | "editor.quickSuggestions": { 86 | "other": true, 87 | "comments": false, 88 | "strings": true 89 | } 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Alta Software 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Opus", 6 | platforms: [ 7 | .macOS(.v10_12), 8 | .iOS(.v12), 9 | .tvOS(.v12), 10 | .watchOS(.v6), 11 | ], 12 | products: [ 13 | .library( 14 | name: "Copus", 15 | targets: ["Copus"] 16 | ), 17 | .library( 18 | name: "Copuswrapper", 19 | targets: ["Copuswrapper"] 20 | ), 21 | .library( 22 | name: "Opus", 23 | targets: ["Opus", "Copus"] 24 | ), 25 | ], 26 | dependencies: [], 27 | targets: [ 28 | .target( 29 | name: "Copus", 30 | dependencies: [], 31 | exclude: [ 32 | "AUTHORS", 33 | "autogen.sh", 34 | "celt/arm", 35 | "celt_headers.mk", 36 | "celt_sources.mk", 37 | "celt/dump_modes", 38 | "celt/meson.build", 39 | "celt/opus_custom_demo.c", 40 | "celt/tests", 41 | "celt/x86", 42 | "ChangeLog", 43 | "cmake", 44 | "CMakeLists.txt", 45 | "configure.ac", 46 | "COPYING", 47 | "doc", 48 | "include/meson.build", 49 | "LICENSE_PLEASE_READ.txt", 50 | "m4", 51 | "m4/opus-intrinsics.m4", 52 | "Makefile.am", 53 | "Makefile.mips", 54 | "Makefile.unix", 55 | "meson_options.txt", 56 | "meson.build", 57 | "meson", 58 | "NEWS", 59 | "opus_headers.mk", 60 | "opus_sources.mk", 61 | "opus-uninstalled.pc.in", 62 | "opus.m4", 63 | "opus.pc.in", 64 | "README.draft", 65 | "README", 66 | "releases.sha2", 67 | "scripts/dump_rnn.py", 68 | "scripts/rnn_train.py", 69 | "silk_headers.mk", 70 | "silk_sources.mk", 71 | "silk/arm", 72 | "silk/fixed", 73 | "silk/meson.build", 74 | "silk/mips", 75 | "silk/tests", 76 | "silk/x86", 77 | "src/meson.build", 78 | "src/opus_compare.c", 79 | "src/opus_demo.c", 80 | "src/repacketizer_demo.c", 81 | "tests", 82 | "training", 83 | "update_version", 84 | "win32", 85 | ], 86 | publicHeadersPath: "include", 87 | cSettings: [ 88 | .headerSearchPath("."), 89 | .headerSearchPath("celt"), 90 | .headerSearchPath("celt/x86"), 91 | .headerSearchPath("silk"), 92 | .headerSearchPath("silk/float"), 93 | 94 | .define("OPUS_BUILD"), 95 | .define("CUSTOM_MODES"), 96 | .define("VAR_ARRAYS", to: "1"), 97 | .define("FLOATING_POINT"), // Enable Opus floating-point mode 98 | 99 | .define("HAVE_DLFCN_H", to: "1"), 100 | .define("HAVE_INTTYPES_H", to: "1"), 101 | .define("HAVE_LRINT", to: "1"), 102 | .define("HAVE_LRINTF", to: "1"), 103 | .define("HAVE_MEMORY_H", to: "1"), 104 | .define("HAVE_STDINT_H", to: "1"), 105 | .define("HAVE_STDLIB_H", to: "1"), 106 | .define("HAVE_STRING_H", to: "1"), 107 | .define("HAVE_STRINGS_H", to: "1"), 108 | .define("HAVE_SYS_STAT_H", to: "1"), 109 | .define("HAVE_SYS_TYPES_H", to: "1"), 110 | .define("HAVE_UNISTD_H", to: "1"), 111 | ] 112 | ), 113 | .target( 114 | name: "Copuswrapper", 115 | dependencies: ["Copus"], 116 | publicHeadersPath: "include", 117 | cSettings: [ 118 | .headerSearchPath(".") 119 | ] 120 | ), 121 | .target( 122 | name: "Opus", 123 | dependencies: ["Copus", "Copuswrapper"] 124 | ), 125 | .testTarget( 126 | name: "OpusTests", 127 | dependencies: ["Opus"], 128 | resources: [.copy("Resources")] 129 | ), 130 | ] 131 | ) 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Opus 2 | 3 | Type-safe [Swift](https://swift.org/) bindings for the [Opus audio codec](https://opus-codec.org/) on Apple platforms (iOS, tvOS, macOS, watchOS). 4 | 5 | This package enables low-level Opus packet encoding and decoding to an `AVAudioPCMBuffer` suitable for playback via an `AVAudioEngine` and `AVAudioPlayerNode`. This was built for a now-defunct audio app for iOS and macOS, and runs reliably with multiple 48khz Opus audio channels over a typical 4G connection on modern iPhone devices. 6 | ## Installation 7 | 8 | Use [Swift Package Manager](https://swift.org/package-manager/) to add this to your Xcode project or Swift package. 9 | 10 | ### Note 11 | 12 | This package neither vendors the original Opus source code or embeds precompiled libraries or binary frameworks. It embeds the current Opus C source as a [git submodule](Sources/Copus), which Swift Package Manager will automatically download as part of the build process. See [Package.swift](Package.swift) for details. 13 | 14 | ## Usage 15 | 16 | TODO 17 | 18 | ## License 19 | 20 | See [LICENSE](LICENSE) for more information. 21 | -------------------------------------------------------------------------------- /Sources/Copuswrapper/include/variadic-wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef __OPUS_VARIADIC_WRAPPER_H__ 2 | #define __OPUS_VARIADIC_WRAPPER_H__ 3 | 4 | //#include "" 5 | #include 6 | #include 7 | 8 | int opus_custom_encoder_ctl_wrapper(OpusCustomEncoder *OPUS_RESTRICT st, int request, opus_int32 val); 9 | int opus_custom_decoder_ctl_wrapper(OpusCustomDecoder *OPUS_RESTRICT st, int request, opus_int32 val); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/Copuswrapper/variadic-wrapper.c: -------------------------------------------------------------------------------- 1 | #include "variadic-wrapper.h" 2 | 3 | int opus_custom_encoder_ctl_wrapper(OpusCustomEncoder *OPUS_RESTRICT st, int request, opus_int32 val) 4 | { 5 | opus_custom_encoder_ctl(st, request, val); 6 | } 7 | 8 | int opus_custom_decoder_ctl_wrapper(OpusCustomDecoder *OPUS_RESTRICT st, int request, opus_int32 val) 9 | { 10 | opus_custom_decoder_ctl(st, request, val); 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Opus/AVAudioFormat+Extensions.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | public extension AVAudioFormat { 4 | enum OpusPCMFormat { 5 | case int16 6 | case float32 7 | } 8 | 9 | convenience init?(opusPCMFormat: OpusPCMFormat, sampleRate: Double, channels: AVAudioChannelCount) { 10 | switch opusPCMFormat { 11 | case .int16: 12 | self.init(commonFormat: .pcmFormatInt16, sampleRate: sampleRate, channels: channels, interleaved: channels != 1) 13 | case .float32: 14 | self.init(commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: channels, interleaved: channels != 1) 15 | } 16 | if !isValidOpusPCMFormat { 17 | return nil 18 | } 19 | } 20 | 21 | var isValidOpusPCMFormat: Bool { 22 | switch sampleRate { 23 | case .opus8khz, .opus12khz, .opus16khz, .opus24khz, .opus48khz: 24 | break 25 | default: 26 | return false 27 | } 28 | 29 | switch channelCount { 30 | case 1, 2: 31 | break 32 | default: 33 | return false 34 | } 35 | 36 | if channelCount != 1, !isInterleaved { 37 | return false 38 | } 39 | 40 | if commonFormat == .pcmFormatInt16 || commonFormat == .pcmFormatFloat32 { 41 | return true 42 | } 43 | 44 | let desc = streamDescription.pointee 45 | if desc.mFormatID != kAudioFormatLinearPCM { 46 | return false 47 | } 48 | if desc.mFormatFlags & kLinearPCMFormatFlagIsSignedInteger != 0, desc.mBitsPerChannel != 16 { 49 | return false 50 | } 51 | if desc.mFormatFlags & kLinearPCMFormatFlagIsFloat != 0, desc.mBitsPerChannel != 32 { 52 | return false 53 | } 54 | 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Opus/AVAudioFrameCount+Extensions.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | public extension AVAudioFrameCount { 4 | // Opus can encode packets as small as 2.5ms at 8khz (20 samples) 5 | static let opusMin: Self = 20 6 | 7 | // Opus can encode packets as large as to 120ms at 48khz (5760 samples) 8 | static let opusMax: Self = 5760 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Opus/Double+Extensions.swift: -------------------------------------------------------------------------------- 1 | public extension Double { 2 | static let opus8khz: Self = 8000 3 | static let opus12khz: Self = 12000 4 | static let opus16khz: Self = 16000 5 | static let opus24khz: Self = 24000 6 | static let opus48khz: Self = 48000 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Opus/Opus.Application.swift: -------------------------------------------------------------------------------- 1 | public extension Opus { 2 | struct Application: Equatable, RawRepresentable, ExpressibleByIntegerLiteral { 3 | public typealias IntegerLiteralType = Int32 4 | public var rawValue: IntegerLiteralType 5 | 6 | public static let audio = Self(OPUS_APPLICATION_AUDIO) 7 | public static let voip = Self(OPUS_APPLICATION_VOIP) 8 | public static let restrictedLowDelay = Self(OPUS_APPLICATION_RESTRICTED_LOWDELAY) 9 | 10 | public init(rawValue: IntegerLiteralType) { 11 | self.rawValue = rawValue 12 | } 13 | 14 | public init(integerLiteral value: IntegerLiteralType) { 15 | self.init(rawValue: value) 16 | } 17 | 18 | public init(_ value: T) { 19 | self.init(rawValue: IntegerLiteralType(value)) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Opus/Opus.Decoder.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Copus 3 | 4 | public extension Opus { 5 | class Decoder { 6 | let format: AVAudioFormat 7 | let decoder: OpaquePointer 8 | 9 | // TODO: throw an error if format is unsupported 10 | public init(format: AVAudioFormat, application _: Application = .audio) throws { 11 | if !format.isValidOpusPCMFormat { 12 | throw Opus.Error.badArgument 13 | } 14 | 15 | self.format = format 16 | 17 | // Initialize Opus decoder 18 | var error: Opus.Error = .ok 19 | decoder = opus_decoder_create(Int32(format.sampleRate), Int32(format.channelCount), &error.rawValue) 20 | if error != .ok { 21 | throw error 22 | } 23 | } 24 | 25 | deinit { 26 | opus_decoder_destroy(decoder) 27 | } 28 | 29 | public func reset() throws { 30 | let error = Opus.Error(opus_decoder_init(decoder, Int32(format.sampleRate), Int32(format.channelCount))) 31 | if error != .ok { 32 | throw error 33 | } 34 | } 35 | } 36 | } 37 | 38 | // MARK: Public decode methods 39 | 40 | public extension Opus.Decoder { 41 | func decode(_ input: Data) throws -> AVAudioPCMBuffer { 42 | try input.withUnsafeBytes { 43 | let input = $0.bindMemory(to: UInt8.self) 44 | let sampleCount = opus_decoder_get_nb_samples(decoder, input.baseAddress!, Int32($0.count)) 45 | if sampleCount < 0 { 46 | throw Opus.Error(sampleCount) 47 | } 48 | let output = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(sampleCount))! 49 | try decode(input, to: output) 50 | return output 51 | } 52 | } 53 | 54 | func decode(_ input: UnsafeBufferPointer, to output: AVAudioPCMBuffer) throws { 55 | let decodedCount: Int 56 | switch output.format.commonFormat { 57 | case .pcmFormatInt16: 58 | let output = UnsafeMutableBufferPointer(start: output.int16ChannelData![0], count: Int(output.frameCapacity)) 59 | decodedCount = try decode(input, to: output) 60 | case .pcmFormatFloat32: 61 | let output = UnsafeMutableBufferPointer(start: output.floatChannelData![0], count: Int(output.frameCapacity)) 62 | decodedCount = try decode(input, to: output) 63 | default: 64 | throw Opus.Error.badArgument 65 | } 66 | if decodedCount < 0 { 67 | throw Opus.Error(decodedCount) 68 | } 69 | output.frameLength = AVAudioFrameCount(decodedCount) 70 | } 71 | } 72 | 73 | // MARK: Private decode methods 74 | 75 | extension Opus.Decoder { 76 | private func decode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { 77 | let decodedCount = opus_decode( 78 | decoder, 79 | input.baseAddress!, 80 | Int32(input.count), 81 | output.baseAddress!, 82 | Int32(output.count), 83 | 0 84 | ) 85 | if decodedCount < 0 { 86 | throw Opus.Error(decodedCount) 87 | } 88 | return Int(decodedCount) 89 | } 90 | 91 | private func decode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { 92 | let decodedCount = opus_decode_float( 93 | decoder, 94 | input.baseAddress!, 95 | Int32(input.count), 96 | output.baseAddress!, 97 | Int32(output.count), 98 | 0 99 | ) 100 | if decodedCount < 0 { 101 | throw Opus.Error(decodedCount) 102 | } 103 | return Int(decodedCount) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Opus/Opus.Encoder.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Copus 3 | 4 | public extension Opus { 5 | class Encoder { 6 | let format: AVAudioFormat 7 | let application: Application 8 | let encoder: OpaquePointer 9 | 10 | // TODO: throw an error if format is unsupported 11 | public init(format: AVAudioFormat, application: Application = .audio) throws { 12 | if !format.isValidOpusPCMFormat { 13 | throw Opus.Error.badArgument 14 | } 15 | 16 | self.format = format 17 | self.application = application 18 | 19 | // Initialize Opus encoder 20 | var error: Opus.Error = .ok 21 | encoder = opus_encoder_create(Int32(format.sampleRate), Int32(format.channelCount), application.rawValue, &error.rawValue) 22 | if error != .ok { 23 | throw error 24 | } 25 | } 26 | 27 | deinit { 28 | opus_encoder_destroy(encoder) 29 | } 30 | 31 | public func reset() throws { 32 | let error = Opus.Error(opus_encoder_init(encoder, Int32(format.sampleRate), Int32(format.channelCount), application.rawValue)) 33 | if error != .ok { 34 | throw error 35 | } 36 | } 37 | } 38 | } 39 | 40 | // MARK: Public encode methods 41 | 42 | public extension Opus.Encoder { 43 | func encode(_ input: AVAudioPCMBuffer, to output: inout Data) throws -> Int { 44 | output.count = try output.withUnsafeMutableBytes { 45 | try encode(input, to: $0) 46 | } 47 | return output.count 48 | } 49 | 50 | func encode(_ input: AVAudioPCMBuffer, to output: inout [UInt8]) throws -> Int { 51 | try output.withUnsafeMutableBufferPointer { 52 | try encode(input, to: $0) 53 | } 54 | } 55 | 56 | func encode(_ input: AVAudioPCMBuffer, to output: UnsafeMutableRawBufferPointer) throws -> Int { 57 | let output = UnsafeMutableBufferPointer(start: output.baseAddress!.bindMemory(to: UInt8.self, capacity: output.count), count: output.count) 58 | return try encode(input, to: output) 59 | } 60 | 61 | func encode(_ input: AVAudioPCMBuffer, to output: UnsafeMutableBufferPointer) throws -> Int { 62 | guard input.format.sampleRate == format.sampleRate, input.format.channelCount == format.channelCount else { 63 | throw Opus.Error.badArgument 64 | } 65 | switch format.commonFormat { 66 | case .pcmFormatInt16: 67 | let input = UnsafeBufferPointer(start: input.int16ChannelData![0], count: Int(input.frameLength * format.channelCount)) 68 | return try encode(input, to: output) 69 | case .pcmFormatFloat32: 70 | let input = UnsafeBufferPointer(start: input.floatChannelData![0], count: Int(input.frameLength * format.channelCount)) 71 | return try encode(input, to: output) 72 | default: 73 | throw Opus.Error.badArgument 74 | } 75 | } 76 | } 77 | 78 | // MARK: private encode methods 79 | 80 | extension Opus.Encoder { 81 | private func encode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { 82 | let encodedSize = opus_encode( 83 | encoder, 84 | input.baseAddress!, 85 | Int32(input.count) / Int32(format.channelCount), 86 | output.baseAddress!, 87 | Int32(output.count) 88 | ) 89 | if encodedSize < 0 { 90 | throw Opus.Error(encodedSize) 91 | } 92 | return Int(encodedSize) 93 | } 94 | 95 | private func encode(_ input: UnsafeBufferPointer, to output: UnsafeMutableBufferPointer) throws -> Int { 96 | let encodedSize = opus_encode_float( 97 | encoder, 98 | input.baseAddress!, 99 | Int32(input.count) / Int32(format.channelCount), 100 | output.baseAddress!, 101 | Int32(output.count) 102 | ) 103 | if encodedSize < 0 { 104 | throw Opus.Error(encodedSize) 105 | } 106 | return Int(encodedSize) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Opus/Opus.Error.swift: -------------------------------------------------------------------------------- 1 | public extension Opus { 2 | struct Error: Swift.Error, Equatable, RawRepresentable, ExpressibleByIntegerLiteral { 3 | public typealias IntegerLiteralType = Int32 4 | public var rawValue: IntegerLiteralType 5 | 6 | public static let ok = Self(OPUS_OK) 7 | public static let badArgument = Self(OPUS_BAD_ARG) 8 | public static let bufferTooSmall = Self(OPUS_BUFFER_TOO_SMALL) 9 | public static let internalError = Self(OPUS_INTERNAL_ERROR) 10 | public static let invalidPacket = Self(OPUS_INVALID_PACKET) 11 | public static let unimplemented = Self(OPUS_UNIMPLEMENTED) 12 | public static let invalidState = Self(OPUS_INVALID_STATE) 13 | public static let allocationFailure = Self(OPUS_ALLOC_FAIL) 14 | 15 | public init(rawValue: IntegerLiteralType) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | public init(integerLiteral value: IntegerLiteralType) { 20 | self.init(rawValue: value) 21 | } 22 | 23 | public init(_ value: T) { 24 | self.init(rawValue: IntegerLiteralType(value)) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Opus/Opus.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import CoreAudio 3 | 4 | @_exported import Copus 5 | 6 | public enum Opus: CaseIterable {} 7 | -------------------------------------------------------------------------------- /Tests/OpusTests/AVAudioFormatTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import XCTest 3 | 4 | @testable import Opus 5 | 6 | final class AVAudioFormatTests: XCTestCase { 7 | static let nilFormats = [ 8 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: 44100, channels: 1), 9 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: 44100, channels: 2), 10 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: 44100, channels: 1), 11 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: 44100, channels: 2), 12 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus48khz, channels: 0), 13 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus48khz, channels: 3), 14 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus48khz, channels: 4), 15 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus48khz, channels: 5), 16 | ] 17 | 18 | func testInitializer() throws { 19 | Self.nilFormats.forEach { 20 | XCTAssertNil($0) 21 | } 22 | } 23 | 24 | static let validFormats = [ 25 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 48000, channels: 1, interleaved: true)!, 26 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 48000, channels: 2, interleaved: true)!, 27 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 8000, channels: 2, interleaved: true)!, 28 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 8000, channels: 2, interleaved: true)!, 29 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 8000, channels: 2, interleaved: true)!, 30 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 8000, channels: 2, interleaved: true)!, 31 | AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 48000, channels: 1, interleaved: true)!, 32 | AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 48000, channels: 2, interleaved: true)!, 33 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: false, AVLinearPCMBitDepthKey: 16, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 1, AVLinearPCMIsNonInterleaved: false])!, 34 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: false, AVLinearPCMBitDepthKey: 16, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 2, AVLinearPCMIsNonInterleaved: false])!, 35 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: true, AVLinearPCMBitDepthKey: 32, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 1, AVLinearPCMIsNonInterleaved: false])!, 36 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: true, AVLinearPCMBitDepthKey: 16, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 2, AVLinearPCMIsNonInterleaved: false])!, 37 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus12khz, channels: 1)!, 38 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus12khz, channels: 2)!, 39 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus16khz, channels: 1)!, 40 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus16khz, channels: 2)!, 41 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus24khz, channels: 1)!, 42 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus24khz, channels: 2)!, 43 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus48khz, channels: 1)!, 44 | AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus48khz, channels: 2)!, 45 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus12khz, channels: 1)!, 46 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus12khz, channels: 2)!, 47 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus16khz, channels: 1)!, 48 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus16khz, channels: 2)!, 49 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus24khz, channels: 1)!, 50 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus24khz, channels: 2)!, 51 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus48khz, channels: 1)!, 52 | AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus48khz, channels: 2)!, 53 | ] 54 | 55 | static let invalidFormats = [ 56 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 48000, channels: 2, interleaved: false)!, 57 | AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 44100, channels: 2, interleaved: true)!, 58 | AVAudioFormat(commonFormat: .pcmFormatInt32, sampleRate: 48000, channels: 1, interleaved: true)!, 59 | AVAudioFormat(commonFormat: .pcmFormatInt32, sampleRate: 48000, channels: 2, interleaved: true)!, 60 | AVAudioFormat(commonFormat: .pcmFormatFloat64, sampleRate: 48000, channels: 1, interleaved: true)!, 61 | AVAudioFormat(commonFormat: .pcmFormatFloat64, sampleRate: 48000, channels: 2, interleaved: true)!, 62 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: false, AVLinearPCMBitDepthKey: 16, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 2, AVLinearPCMIsNonInterleaved: true])!, 63 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: false, AVLinearPCMBitDepthKey: 32, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 1, AVLinearPCMIsNonInterleaved: false])!, 64 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatLinearPCM, AVLinearPCMIsFloatKey: false, AVLinearPCMBitDepthKey: 32, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 2, AVLinearPCMIsNonInterleaved: false])!, 65 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatOpus, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 1])!, 66 | AVAudioFormat(settings: [AVFormatIDKey: kAudioFormatOpus, AVSampleRateKey: 48000, AVNumberOfChannelsKey: 2])!, 67 | ] 68 | 69 | func testIsValidFormat() throws { 70 | Self.validFormats.forEach { 71 | XCTAssert($0.isValidOpusPCMFormat, $0.description) 72 | } 73 | 74 | Self.invalidFormats.forEach { 75 | XCTAssertFalse($0.isValidOpusPCMFormat, $0.description) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/OpusTests/OpusApplicationTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Opus 3 | import XCTest 4 | 5 | final class OpusApplicationTests: XCTestCase { 6 | func testInitializer() { 7 | _ = Opus.Application(Int(0)) 8 | _ = Opus.Application(UInt8(0)) 9 | _ = Opus.Application(UInt16(0)) 10 | _ = Opus.Application(UInt32(0)) 11 | _ = Opus.Application(UInt64(0)) 12 | _ = Opus.Application(Int8(0)) 13 | _ = Opus.Application(Int16(0)) 14 | _ = Opus.Application(Int32(0)) 15 | _ = Opus.Application(Int64(0)) 16 | } 17 | 18 | func testValues() { 19 | XCTAssertEqual(Opus.Application.audio.rawValue, OPUS_APPLICATION_AUDIO) 20 | XCTAssertEqual(Opus.Application.voip.rawValue, OPUS_APPLICATION_VOIP) 21 | XCTAssertEqual(Opus.Application.restrictedLowDelay.rawValue, OPUS_APPLICATION_RESTRICTED_LOWDELAY) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/OpusTests/OpusDecoderTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import XCTest 3 | 4 | @testable import Opus 5 | 6 | final class OpusDecoderTests: XCTestCase { 7 | func testInit() throws { 8 | try AVAudioFormatTests.validFormats.forEach { 9 | _ = try Opus.Decoder(format: $0) 10 | } 11 | 12 | try AVAudioFormatTests.invalidFormats.forEach { 13 | XCTAssertThrowsError(try Opus.Decoder(format: $0)) { error in 14 | XCTAssertEqual(error as! Opus.Error, Opus.Error.badArgument) 15 | } 16 | } 17 | } 18 | 19 | func testCorruptedOpusDataThrowsInvalidPacketError() throws { 20 | guard let opusFormat = AVAudioFormat(opusPCMFormat: .int16, sampleRate: .opus16khz, channels: 1) else { 21 | XCTFail("Unable to generate desired opusFormat") 22 | return 23 | } 24 | 25 | let opusDecoder = try Opus.Decoder(format: opusFormat) 26 | // A corrupted, 40 bytes opus package, generated by a buggy Bluetooth piece of software. Other scenarios may also be the cause of a similar issue. 27 | let corruptedOpusData = Data([191, 232, 174, 215, 224, 130, 236, 126, 177, 204, 165, 85, 230, 201, 43, 16, 207, 120, 223, 247, 59, 117, 41, 235, 55, 96, 78, 68, 7, 207, 184, 255, 254, 0, 0, 0, 0, 0, 0, 0]) 28 | 29 | XCTAssertThrowsError(try opusDecoder.decode(corruptedOpusData)) { error in 30 | guard let opusError = error as? Opus.Error else { 31 | XCTFail("Unable to cast error to Opus.Error") 32 | return 33 | } 34 | 35 | XCTAssertEqual(opusError, Opus.Error.invalidPacket) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/OpusTests/OpusEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import XCTest 3 | 4 | @testable import Opus 5 | 6 | final class OpusEncoderTests: XCTestCase { 7 | func testInit() throws { 8 | try AVAudioFormatTests.validFormats.forEach { 9 | _ = try Opus.Encoder(format: $0) 10 | } 11 | 12 | try AVAudioFormatTests.invalidFormats.forEach { 13 | XCTAssertThrowsError(try Opus.Encoder(format: $0)) { error in 14 | XCTAssertEqual(error as! Opus.Error, Opus.Error.badArgument) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/OpusTests/OpusErrorTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Opus 3 | import XCTest 4 | 5 | final class OpusErrorTests: XCTestCase { 6 | func testInitializer() { 7 | _ = Opus.Error(Int(0)) 8 | _ = Opus.Error(UInt8(0)) 9 | _ = Opus.Error(UInt16(0)) 10 | _ = Opus.Error(UInt32(0)) 11 | _ = Opus.Error(UInt64(0)) 12 | _ = Opus.Error(Int8(0)) 13 | _ = Opus.Error(Int16(0)) 14 | _ = Opus.Error(Int32(0)) 15 | _ = Opus.Error(Int64(0)) 16 | } 17 | 18 | func testValues() { 19 | XCTAssertEqual(Opus.Error.ok.rawValue, OPUS_OK) 20 | XCTAssertEqual(Opus.Error.badArgument.rawValue, OPUS_BAD_ARG) 21 | XCTAssertEqual(Opus.Error.bufferTooSmall.rawValue, OPUS_BUFFER_TOO_SMALL) 22 | XCTAssertEqual(Opus.Error.internalError.rawValue, OPUS_INTERNAL_ERROR) 23 | XCTAssertEqual(Opus.Error.invalidPacket.rawValue, OPUS_INVALID_PACKET) 24 | XCTAssertEqual(Opus.Error.unimplemented.rawValue, OPUS_UNIMPLEMENTED) 25 | XCTAssertEqual(Opus.Error.invalidState.rawValue, OPUS_INVALID_STATE) 26 | XCTAssertEqual(Opus.Error.allocationFailure.rawValue, OPUS_ALLOC_FAIL) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/OpusTests/OpusRoundTripTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import XCTest 3 | 4 | @testable import Opus 5 | 6 | final class OpusRoundTripTests: XCTestCase { 7 | let engine = AVAudioEngine() 8 | 9 | func testSilence() throws { 10 | let format = AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus48khz, channels: 1)! 11 | let input = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: .opusMax)! 12 | input.frameLength = input.frameCapacity // Silence 13 | _ = try encodeAndDecode(input) 14 | } 15 | 16 | func testSoundFile() throws { 17 | let url = Bundle.module.url(forResource: "MuteMono", withExtension: "wav")! 18 | let audioFile = try AVAudioFile(forReading: url) 19 | let format = AVAudioFormat(opusPCMFormat: .float32, sampleRate: .opus48khz, channels: 1)! 20 | let input = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: .opusMax)! 21 | try audioFile.read(into: input, frameCount: input.frameCapacity) 22 | _ = try encodeAndDecode(input) 23 | } 24 | 25 | func encodeAndDecode(_ input: AVAudioPCMBuffer) throws -> AVAudioPCMBuffer { 26 | let encoder = try Opus.Encoder(format: input.format) 27 | let decoder = try Opus.Decoder(format: input.format) 28 | var data = Data(count: 1500) 29 | _ = try encoder.encode(input, to: &data) 30 | let output = try decoder.decode(data) 31 | assertSimilar(input, output) 32 | // try play(input) 33 | // try play(output) 34 | return output 35 | } 36 | 37 | func play(_ buffer: AVAudioPCMBuffer) throws { 38 | _ = engine.mainMixerNode 39 | try engine.start() 40 | let player = AVAudioPlayerNode() 41 | engine.attach(player) 42 | engine.connect(player, to: engine.mainMixerNode, format: buffer.format) 43 | player.play() 44 | player.scheduleBuffer(buffer) { 45 | DispatchQueue.main.async { 46 | self.engine.detach(player) 47 | } 48 | } 49 | Thread.sleep(forTimeInterval: Double(buffer.frameLength) / Double(buffer.format.sampleRate)) 50 | } 51 | 52 | func assertSimilar(_ a: AVAudioPCMBuffer, _ b: AVAudioPCMBuffer, epsilon _: Float32 = 0.2) { 53 | XCTAssertTrue(a.format.isEqual(b.format), "a.format == b.format") 54 | XCTAssertEqual(a.frameLength, b.frameLength, "a.frameLength == b.frameLength") 55 | // for i in 0 ... a.frameLength { 56 | // var x: Float32 = 0 57 | // var y: Float32 = 0 58 | // switch a.format.commonFormat { 59 | // case .pcmFormatInt16: 60 | // x = Float32(a.int16ChannelData![0][Int(i)]) / 32768.0 61 | // y = Float32(b.int16ChannelData![0][Int(i)]) / 32768.0 62 | // case .pcmFormatFloat32: 63 | // x = a.floatChannelData![0][Int(i)] 64 | // y = b.floatChannelData![0][Int(i)] 65 | // default: 66 | // XCTFail("unknown audio format: \(a.format)") 67 | // } 68 | // let delta = abs(abs(x) - abs(y)) 69 | // XCTAssert(delta < epsilon, String(delta)) 70 | // } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/OpusTests/OpusTests.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Opus 3 | import XCTest 4 | 5 | final class OpusTests: XCTestCase { 6 | // Validate that namespaces are empty enums, with no values. 7 | func testEnumCases() { 8 | XCTAssertEqual(Opus.allCases.count, 0) 9 | } 10 | 11 | func testMemorySizes() { 12 | // These are implementation independent, and may change: 13 | XCTAssertEqual(opus_decoder_get_size(1), 18228) 14 | XCTAssertEqual(opus_decoder_get_size(2), 26996) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/OpusTests/Resources/MuteMono.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emlynmac/swift-opus/00d42cfda30df1b37ce90b2804a790c42f6b3153/Tests/OpusTests/Resources/MuteMono.wav -------------------------------------------------------------------------------- /Tests/OpusTests/Resources/UnmuteMono.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emlynmac/swift-opus/00d42cfda30df1b37ce90b2804a790c42f6b3153/Tests/OpusTests/Resources/UnmuteMono.wav --------------------------------------------------------------------------------