├── settings.gradle ├── native ├── cmake │ ├── zig-ar │ ├── zig-ar.bat │ ├── zig-cc │ ├── zig-cc.bat │ ├── zig-ranlib │ ├── zig-ranlib.bat │ ├── zig-aarch64-macos.cmake │ ├── zig-aarch64-linux-gnu.cmake │ ├── zig-x86_64-macos.cmake │ ├── zig-x86_64-linux-gnu.cmake │ ├── zig-aarch64-windows-gnu.cmake │ ├── zig-x86_64-windows-gnu.cmake │ └── zig-base.cmake ├── include │ └── exceptions.h ├── CMakeLists.txt └── src │ ├── exceptions.c │ ├── encoder.c │ └── decoder.c ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml └── workflows │ └── release.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── src ├── main │ ├── resources │ │ └── OPUS_LICENSE │ └── java │ │ └── de │ │ └── maxhenkel │ │ └── opus4j │ │ ├── OpusEncoder.java │ │ └── OpusDecoder.java └── test │ └── java │ └── de │ └── maxhenkel │ └── opus4j │ ├── OpusEncoderTest.java │ └── OpusDecoderTest.java ├── .gitignore ├── readme.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'opus4j' 2 | -------------------------------------------------------------------------------- /native/cmake/zig-ar: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | zig ar "$@" 3 | -------------------------------------------------------------------------------- /native/cmake/zig-ar.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | zig ar %* 3 | -------------------------------------------------------------------------------- /native/cmake/zig-cc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | zig cc "$@" 3 | -------------------------------------------------------------------------------- /native/cmake/zig-cc.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | zig cc %* 3 | -------------------------------------------------------------------------------- /native/cmake/zig-ranlib: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | zig ranlib "$@" 3 | -------------------------------------------------------------------------------- /native/cmake/zig-ranlib.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | zig ranlib %* 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkelmax/opus4j/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | shadow_version=8.3.6 2 | maven_settings_version=0.5 3 | junit_version=5.13.4 4 | nativeutils_version=1.0.2 5 | 6 | library_version=2.1.3 7 | -------------------------------------------------------------------------------- /native/cmake/zig-aarch64-macos.cmake: -------------------------------------------------------------------------------- 1 | set(TARGET_TRIPLE "aarch64-macos") 2 | set(CMAKE_SYSTEM_NAME "Darwin") 3 | set(CMAKE_SYSTEM_PROCESSOR "arm64") 4 | 5 | include("${CMAKE_CURRENT_LIST_DIR}/zig-base.cmake") 6 | -------------------------------------------------------------------------------- /native/cmake/zig-aarch64-linux-gnu.cmake: -------------------------------------------------------------------------------- 1 | set(TARGET_TRIPLE "aarch64-linux-gnu") 2 | set(CMAKE_SYSTEM_NAME "Linux") 3 | set(CMAKE_SYSTEM_PROCESSOR "arm64") 4 | 5 | include("${CMAKE_CURRENT_LIST_DIR}/zig-base.cmake") 6 | -------------------------------------------------------------------------------- /native/cmake/zig-x86_64-macos.cmake: -------------------------------------------------------------------------------- 1 | set(TARGET_TRIPLE "x86_64-macos") 2 | set(CMAKE_SYSTEM_NAME "Darwin") 3 | set(CMAKE_SYSTEM_PROCESSOR "x86_64") 4 | set(CPU_ARCHITECTURE_TARGET "x86_64") 5 | 6 | include("${CMAKE_CURRENT_LIST_DIR}/zig-base.cmake") 7 | -------------------------------------------------------------------------------- /native/cmake/zig-x86_64-linux-gnu.cmake: -------------------------------------------------------------------------------- 1 | set(TARGET_TRIPLE "x86_64-linux-gnu") 2 | set(CMAKE_SYSTEM_NAME "Linux") 3 | set(CMAKE_SYSTEM_PROCESSOR "x86_64") 4 | set(CPU_ARCHITECTURE_TARGET "x86_64") 5 | 6 | include("${CMAKE_CURRENT_LIST_DIR}/zig-base.cmake") 7 | -------------------------------------------------------------------------------- /native/cmake/zig-aarch64-windows-gnu.cmake: -------------------------------------------------------------------------------- 1 | set(TARGET_TRIPLE "aarch64-windows-gnu") 2 | set(CMAKE_SYSTEM_NAME "Windows") 3 | set(CMAKE_SYSTEM_PROCESSOR "aarch64") 4 | 5 | set(OPUS_DISABLE_INTRINSICS ON) 6 | 7 | include("${CMAKE_CURRENT_LIST_DIR}/zig-base.cmake") 8 | -------------------------------------------------------------------------------- /native/cmake/zig-x86_64-windows-gnu.cmake: -------------------------------------------------------------------------------- 1 | set(TARGET_TRIPLE "x86_64-windows-gnu") 2 | set(CMAKE_SYSTEM_NAME "Windows") 3 | set(CMAKE_SYSTEM_PROCESSOR "x86_64") 4 | set(CPU_ARCHITECTURE_TARGET "x86_64") 5 | 6 | include("${CMAKE_CURRENT_LIST_DIR}/zig-base.cmake") 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /native/include/exceptions.h: -------------------------------------------------------------------------------- 1 | #ifndef EXCEPTIONS_H 2 | #define EXCEPTIONS_H 3 | 4 | char *string_format(const char *fmt, ...); 5 | 6 | void throw_runtime_exception(JNIEnv *env, const char *message); 7 | 8 | void throw_illegal_state_exception(JNIEnv *env, const char *message); 9 | 10 | void throw_io_exception(JNIEnv *env, const char *message); 11 | 12 | void throw_illegal_argument_exception(JNIEnv *env, const char *message); 13 | 14 | void throw_opus_io_exception(JNIEnv *env, const int error, const char *message); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | workflow_dispatch: 8 | 9 | env: 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 12 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | - name: Set up Java 21 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: 'temurin' 24 | java-version: '21' 25 | server-id: henkelmax.public 26 | server-username: MAVEN_USERNAME 27 | server-password: MAVEN_PASSWORD 28 | - name: Setup zig 29 | uses: mlugg/setup-zig@v2 30 | with: 31 | version: 0.14.1 32 | - name: Build and deploy 33 | run: | 34 | ./gradlew publish 35 | mkdir -p release 36 | cp $(find ./build/libs -maxdepth 1 -type f -name "*.jar") ./release/ 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: opus4j-java 40 | path: ./release/ 41 | - name: Upload release asset 42 | uses: AButler/upload-release-assets@v3.0 43 | with: 44 | files: ./release/* 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: [triage] 4 | assignees: henkelmax 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | > [!WARNING] 10 | > This form is **only for bug reports**! 11 | > Please don't abuse this for feature requests or questions. 12 | > Forms that are not filled out properly will be closed without response! 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Bug description 17 | description: A clear and concise description of what the bug is. 18 | validations: 19 | required: true 20 | - type: input 21 | id: version 22 | attributes: 23 | label: Version 24 | description: The version of the library. 25 | placeholder: 1.0.0 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: steps 30 | attributes: 31 | label: Steps to reproduce 32 | description: | 33 | Steps to reproduce the issue. 34 | Please **don't** report issues that are not reproducible. 35 | placeholder: | 36 | 1. Go to '...' 37 | 2. Click on '...' 38 | 3. Scroll down to '...' 39 | 4. See error 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: expected 44 | attributes: 45 | label: Expected behavior 46 | description: A clear and concise description of what you expected to happen. 47 | validations: 48 | required: false 49 | -------------------------------------------------------------------------------- /native/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project(opus4j LANGUAGES C) 4 | 5 | set(JDK_8_VERSION "jdk8u462-ga") 6 | set(OPUS_VERSION "v1.5.2") 7 | 8 | if (WIN32) 9 | set(JNI_MD_URL "https://raw.githubusercontent.com/openjdk/jdk8u/refs/tags/${JDK_8_VERSION}/jdk/src/windows/javavm/export/jni_md.h") 10 | elseif (APPLE) 11 | set(JNI_MD_URL "https://raw.githubusercontent.com/openjdk/jdk8u/refs/tags/${JDK_8_VERSION}/jdk/src/macosx/javavm/export/jni_md.h") 12 | elseif (UNIX) 13 | set(JNI_MD_URL "https://raw.githubusercontent.com/openjdk/jdk8u/refs/tags/${JDK_8_VERSION}/jdk/src/solaris/javavm/export/jni_md.h") 14 | else () 15 | message(WARNING "Unknown OS, not compiling jni_md.h") 16 | endif () 17 | 18 | file(DOWNLOAD 19 | "https://raw.githubusercontent.com/openjdk/jdk8u/refs/tags/${JDK_8_VERSION}/jdk/src/share/javavm/export/jni.h" 20 | ${CMAKE_BINARY_DIR}/jni_headers/jni.h 21 | ) 22 | 23 | if (DEFINED JNI_MD_URL) 24 | file(DOWNLOAD 25 | ${JNI_MD_URL} 26 | ${CMAKE_BINARY_DIR}/jni_headers/jni_md.h 27 | ) 28 | endif () 29 | 30 | include(FetchContent) 31 | FetchContent_Declare( 32 | opus 33 | GIT_REPOSITORY https://github.com/xiph/opus.git 34 | GIT_TAG ${OPUS_VERSION} 35 | ) 36 | FetchContent_MakeAvailable(opus) 37 | 38 | add_library(opus4j SHARED 39 | src/encoder.c 40 | src/decoder.c 41 | src/exceptions.c 42 | ) 43 | 44 | target_include_directories(opus4j PRIVATE 45 | ${CMAKE_SOURCE_DIR}/include 46 | ${JNI_INCLUDE_DIRS} 47 | ${opus_SOURCE_DIR}/include 48 | ${CMAKE_BINARY_DIR}/jni_headers 49 | ) 50 | 51 | target_link_libraries(opus4j PRIVATE 52 | ${JNI_LIBRARIES} 53 | opus 54 | ) 55 | 56 | message(STATUS "C compiler executable: ${CMAKE_C_COMPILER}") 57 | -------------------------------------------------------------------------------- /src/main/resources/OPUS_LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic, 2 | Jean-Marc Valin, Timothy B. Terriberry, 3 | CSIRO, Gregory Maxwell, Mark Borgerding, 4 | Erik de Castro Lopo, Mozilla, Amazon 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | - Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | - Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | - Neither the name of Internet Society, IETF or IETF Trust, nor the 18 | names of specific contributors, may be used to endorse or promote 19 | products derived from this software without specific prior written 20 | permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | Opus is subject to the royalty-free patent licenses which are 35 | specified at: 36 | 37 | Xiph.Org Foundation: 38 | https://datatracker.ietf.org/ipr/1524/ 39 | 40 | Microsoft Corporation: 41 | https://datatracker.ietf.org/ipr/1914/ 42 | 43 | Broadcom Corporation: 44 | https://datatracker.ietf.org/ipr/1526/ 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | target/ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | *.iml 10 | *.ipr 11 | *.iws 12 | 13 | # IntelliJ 14 | out/ 15 | # mpeltonen/sbt-idea plugin 16 | .idea_modules/ 17 | 18 | # JIRA plugin 19 | atlassian-ide-plugin.xml 20 | 21 | # Compiled class file 22 | *.class 23 | 24 | # Log file 25 | *.log 26 | 27 | # BlueJ files 28 | *.ctxt 29 | 30 | # Package Files # 31 | *.jar 32 | *.war 33 | *.nar 34 | *.ear 35 | *.zip 36 | *.tar.gz 37 | *.rar 38 | 39 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 40 | hs_err_pid* 41 | 42 | *~ 43 | 44 | # temporary files which can be created if a process still has a handle open of a deleted file 45 | .fuse_hidden* 46 | 47 | # KDE directory preferences 48 | .directory 49 | 50 | # Linux trash folder which might appear on any partition or disk 51 | .Trash-* 52 | 53 | # .nfs files are created when an open file is removed but is still being accessed 54 | .nfs* 55 | 56 | # General 57 | .DS_Store 58 | .AppleDouble 59 | .LSOverride 60 | 61 | # Icon must end with two \r 62 | Icon 63 | 64 | # Thumbnails 65 | ._* 66 | 67 | # Files that might appear in the root of a volume 68 | .DocumentRevisions-V100 69 | .fseventsd 70 | .Spotlight-V100 71 | .TemporaryItems 72 | .Trashes 73 | .VolumeIcon.icns 74 | .com.apple.timemachine.donotpresent 75 | 76 | # Directories potentially created on remote AFP share 77 | .AppleDB 78 | .AppleDesktop 79 | Network Trash Folder 80 | Temporary Items 81 | .apdisk 82 | 83 | # Windows thumbnail cache files 84 | Thumbs.db 85 | Thumbs.db:encryptable 86 | ehthumbs.db 87 | ehthumbs_vista.db 88 | 89 | # Dump file 90 | *.stackdump 91 | 92 | # Folder config file 93 | [Dd]esktop.ini 94 | 95 | # Recycle Bin used on file shares 96 | $RECYCLE.BIN/ 97 | 98 | # Windows Installer files 99 | *.cab 100 | *.msi 101 | *.msix 102 | *.msm 103 | *.msp 104 | 105 | # Windows shortcuts 106 | *.lnk 107 | 108 | .gradle 109 | build/ 110 | 111 | # Ignore Gradle GUI config 112 | gradle-app.setting 113 | 114 | # Cache of project 115 | .gradletasknamecache 116 | 117 | **/build/ 118 | 119 | # Common working directory 120 | run/ 121 | 122 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 123 | !gradle-wrapper.jar 124 | -------------------------------------------------------------------------------- /native/src/exceptions.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "opus_defines.h" 5 | 6 | /** 7 | * Formats a string with the given printf-style format and arguments. 8 | * 9 | * @param fmt The printf-style format string. 10 | * @param ... Arguments matching the format specifiers in fmt. 11 | * @return A pointer to a newly allocated, null-terminated string containing 12 | * the formatted text. The caller is responsible for freeing it 13 | * via free(). Returns NULL on allocation failure. 14 | */ 15 | char *string_format(const char *fmt, ...) { 16 | if (!fmt) { 17 | return NULL; 18 | } 19 | 20 | va_list args; 21 | va_start(args, fmt); 22 | 23 | // Determine required length 24 | va_list args_copy; 25 | va_copy(args_copy, args); 26 | const int needed = vsnprintf(NULL, 0, fmt, args_copy); 27 | va_end(args_copy); 28 | 29 | if (needed < 0) { 30 | va_end(args); 31 | return NULL; 32 | } 33 | 34 | // Allocate buffer (plus null terminator) 35 | char *buffer = malloc((size_t) needed + 1); 36 | if (!buffer) { 37 | va_end(args); 38 | return NULL; 39 | } 40 | 41 | // Print into buffer 42 | vsnprintf(buffer, (size_t) needed + 1, fmt, args); 43 | va_end(args); 44 | 45 | return buffer; 46 | } 47 | 48 | void throw_exception(JNIEnv *env, const char *class_name, const char *message) { 49 | const jclass runtime_exception = (*env)->FindClass(env, class_name); 50 | if (runtime_exception == NULL) { 51 | char *formatted = string_format("Could not find class %s", class_name); 52 | (*env)->FatalError(env, formatted); 53 | free(formatted); 54 | return; 55 | } 56 | (*env)->ThrowNew(env, runtime_exception, message); 57 | } 58 | 59 | void throw_runtime_exception(JNIEnv *env, const char *message) { 60 | throw_exception(env, "java/lang/RuntimeException", message); 61 | } 62 | 63 | void throw_illegal_state_exception(JNIEnv *env, const char *message) { 64 | throw_exception(env, "java/lang/IllegalStateException", message); 65 | } 66 | 67 | void throw_io_exception(JNIEnv *env, const char *message) { 68 | throw_exception(env, "java/io/IOException", message); 69 | } 70 | 71 | void throw_illegal_argument_exception(JNIEnv *env, const char *message) { 72 | throw_exception(env, "java/lang/IllegalArgumentException", message); 73 | } 74 | 75 | void throw_opus_io_exception(JNIEnv *env, const int error, const char *message) { 76 | char *formatted = string_format("%s: %s", message, opus_strerror(error)); 77 | throw_io_exception(env, formatted); 78 | free(formatted); 79 | } 80 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Opus4J 2 | 3 | A Java wrapper for the [Opus Codec](https://opus-codec.org/) written in C using JNI. 4 | 5 | Java 8+ is required to use this library. 6 | 7 | ## Supported Platforms 8 | 9 | - `Windows x86_64` 10 | - `Windows aarch64` 11 | - `macOS x86_64` 12 | - `macOS aarch64` 13 | - `Linux x86_64` 14 | - `Linux aarch64` 15 | 16 | ## Usage 17 | 18 | **Maven** 19 | 20 | ``` xml 21 | 22 | de.maxhenkel.opus4j 23 | opus4j 24 | 2.1.0 25 | 26 | 27 | 28 | 29 | henkelmax.public 30 | https://maven.maxhenkel.de/repository/public 31 | 32 | 33 | ``` 34 | 35 | **Gradle** 36 | 37 | ``` groovy 38 | dependencies { 39 | implementation 'de.maxhenkel.opus4j:opus4j:2.1.0' 40 | } 41 | 42 | repositories { 43 | maven { 44 | name = "henkelmax.public" 45 | url = 'https://maven.maxhenkel.de/repository/public' 46 | } 47 | } 48 | ``` 49 | 50 | ### Example Code 51 | 52 | **Encoding** 53 | 54 | ``` java 55 | short[] rawAudio = ...; 56 | 57 | // Creates a new encoder instance with 48kHz mono VOIP 58 | OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP); 59 | 60 | // Sets the max payload size to 1500 bytes 61 | encoder.setMaxPayloadSize(1500); 62 | 63 | // Sets the max packet loss percentage to 1% for in-band FEC 64 | encoder.setMaxPacketLossPercentage(0.01F); 65 | 66 | // Encodes the raw audio 67 | byte[] encoded = encoder.encode(rawAudio); 68 | 69 | // Resets the encoder state 70 | encoder.resetState(); 71 | 72 | ... 73 | 74 | // Closes the encoder - Not calling this will cause a memory leak! 75 | encoder.close(); 76 | ``` 77 | 78 | **Decoding** 79 | 80 | ``` java 81 | byte[] encodedAudio = ...; 82 | 83 | // Creates a new decoder instance with 48kHz mono 84 | OpusDecoder decoder = new OpusDecoder(48000, 1); 85 | 86 | // Sets the frame size to 960 samples 87 | // If this is not set properly, decoded PLC/FEC frames will have the wrong size 88 | decoder.setFrameSize(960); 89 | 90 | // Decodes the encoded audio 91 | short[] decoded = decoder.decode(encodedAudio); 92 | 93 | // Decode a missing packet with PLC (Packet Loss Concealment) 94 | decoded = decoder.decode(null); 95 | 96 | // Decode a missing packet and the current packet with FEC (Forward Error Correction) 97 | short[][] decodedFec = decoder.decode(encodedAudio, 2); 98 | 99 | // Resets the decoder state 100 | decoder.resetState(); 101 | 102 | ... 103 | 104 | // Closes the decoder - Not calling this will cause a memory leak! 105 | decoder.close(); 106 | ``` 107 | 108 | ## Building from Source 109 | 110 | ### Prerequisites 111 | 112 | - [Java](https://www.java.com/en/) 21 113 | - [Zig](https://ziglang.org/) 0.14.1 114 | - [Ninja](https://ninja-build.org/) 115 | 116 | ### Building 117 | 118 | ``` bash 119 | ./gradlew build 120 | ``` 121 | 122 | ## Credits 123 | 124 | - [Opus](https://opus-codec.org/) 125 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /native/cmake/zig-base.cmake: -------------------------------------------------------------------------------- 1 | if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 2 | set(CUSTOM_CC ${CMAKE_CURRENT_LIST_DIR}/zig-cc.bat) 3 | set(CUSTOM_AR ${CMAKE_CURRENT_LIST_DIR}/zig-ar.bat) 4 | set(CUSTOM_RANLIB ${CMAKE_CURRENT_LIST_DIR}/zig-ranlib.bat) 5 | else () 6 | set(CUSTOM_CC ${CMAKE_CURRENT_LIST_DIR}/zig-cc) 7 | set(CUSTOM_AR ${CMAKE_CURRENT_LIST_DIR}/zig-ar) 8 | set(CUSTOM_RANLIB ${CMAKE_CURRENT_LIST_DIR}/zig-ranlib) 9 | endif () 10 | 11 | set(CMAKE_C_COMPILER "${CUSTOM_CC}" CACHE FILEPATH "" FORCE) 12 | set(CMAKE_C_COMPILER_AR "${CUSTOM_AR}" CACHE FILEPATH "" FORCE) 13 | set(CMAKE_C_COMPILER_RANLIB "${CUSTOM_RANLIB}" CACHE FILEPATH "" FORCE) 14 | set(CMAKE_CXX_COMPILER "${CUSTOM_CC}" CACHE FILEPATH "" FORCE) 15 | set(CMAKE_CXX_COMPILER_AR "${CUSTOM_AR}" CACHE FILEPATH "" FORCE) 16 | set(CMAKE_CXX_COMPILER_RANLIB "${CUSTOM_RANLIB}" CACHE FILEPATH "" FORCE) 17 | set(CMAKE_AR "${CUSTOM_AR}" CACHE FILEPATH "" FORCE) 18 | set(CMAKE_RANLIB "${CUSTOM_RANLIB}" CACHE FILEPATH "" FORCE) 19 | 20 | 21 | set(CMAKE_C_COMPILER_WORKS TRUE) 22 | set(CMAKE_CXX_COMPILER_WORKS TRUE) 23 | 24 | set(BASE_COMPILER_FLAGS "") 25 | 26 | if (DEFINED CPU_ARCHITECTURE_TARGET) 27 | string(APPEND BASE_COMPILER_FLAGS " -march=${CPU_ARCHITECTURE_TARGET}") 28 | endif () 29 | 30 | string(APPEND BASE_COMPILER_FLAGS " -Oz") 31 | string(APPEND BASE_COMPILER_FLAGS " -ffunction-sections") 32 | string(APPEND BASE_COMPILER_FLAGS " -fdata-sections") 33 | string(APPEND BASE_COMPILER_FLAGS " -fno-unwind-tables") 34 | string(APPEND BASE_COMPILER_FLAGS " -fno-asynchronous-unwind-tables") 35 | string(APPEND BASE_COMPILER_FLAGS " -fvisibility=hidden") 36 | 37 | 38 | set(BASE_LINKER_FLAGS "") 39 | 40 | if (CMAKE_SYSTEM_NAME STREQUAL "Windows") 41 | string(APPEND BASE_LINKER_FLAGS " -Wl,--gc-sections") 42 | string(APPEND BASE_LINKER_FLAGS " -Wl,-s") 43 | elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") 44 | string(APPEND BASE_LINKER_FLAGS " -Wl,-dead_strip") 45 | string(APPEND BASE_LINKER_FLAGS " -Wl,-dead_strip_dylibs") 46 | string(APPEND BASE_LINKER_FLAGS " -Wl,-x") 47 | elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") 48 | # Requires -ffunction-sections and -fdata-sections 49 | string(APPEND BASE_LINKER_FLAGS " -Wl,--gc-sections") 50 | string(APPEND BASE_LINKER_FLAGS " -Wl,-s") 51 | string(APPEND BASE_LINKER_FLAGS " -Wl,--as-needed") 52 | endif () 53 | 54 | 55 | set(CMAKE_C_FLAGS_INIT "${BASE_COMPILER_FLAGS}") 56 | set(CMAKE_C_FLAGS_RELEASE_INIT "${BASE_COMPILER_FLAGS}") 57 | set(CMAKE_CXX_FLAGS_INIT "${BASE_COMPILER_FLAGS}") 58 | set(CMAKE_CXX_FLAGS_RELEASE_INIT "${BASE_COMPILER_FLAGS}") 59 | set(CMAKE_ASM_FLAGS_INIT "${BASE_COMPILER_FLAGS}") 60 | set(CMAKE_ASM_FLAGS_RELEASE_INIT "${BASE_COMPILER_FLAGS}") 61 | set(CMAKE_EXE_LINKER_FLAGS_INIT "${BASE_LINKER_FLAGS}") 62 | set(CMAKE_SHARED_LINKER_FLAGS_INIT "${BASE_LINKER_FLAGS}") 63 | set(CMAKE_MODULE_LINKER_FLAGS_INIT "${BASE_LINKER_FLAGS}") 64 | 65 | set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) 66 | set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) 67 | 68 | if (DEFINED TARGET_TRIPLE) 69 | set(CMAKE_C_COMPILER_TARGET "${TARGET_TRIPLE}" CACHE STRING "" FORCE) 70 | set(CMAKE_CXX_COMPILER_TARGET "${TARGET_TRIPLE}" CACHE STRING "" FORCE) 71 | endif () 72 | 73 | set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) 74 | 75 | message(STATUS "Compiling for ${TARGET_TRIPLE} with ${CMAKE_C_COMPILER} and ${CMAKE_AR}") 76 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/opus4j/OpusEncoder.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.opus4j; 2 | 3 | import de.maxhenkel.nativeutils.NativeInitializer; 4 | import de.maxhenkel.nativeutils.UnknownPlatformException; 5 | 6 | import java.io.IOException; 7 | 8 | public class OpusEncoder implements AutoCloseable { 9 | 10 | private long encoder; 11 | 12 | /** 13 | * Creates a new Opus encoder. 14 | * 15 | * @param sampleRate the sample rate (8000, 12000, 16000, 24000, or 48000) 16 | * @param channels the number of channels (1 or 2) 17 | * @param application the application (VOIP, AUDIO, or LOW_DELAY) 18 | * @throws UnknownPlatformException if the operating system is not supported 19 | * @throws IOException if the native library could not be extracted 20 | */ 21 | public OpusEncoder(int sampleRate, int channels, Application application) throws IOException, UnknownPlatformException { 22 | NativeInitializer.load("libopus4j"); 23 | encoder = createEncoder0(sampleRate, channels, application); 24 | } 25 | 26 | private static native String getOpusVersion0(); 27 | 28 | public String getOpusVersion() { 29 | synchronized (this) { 30 | return getOpusVersion0(); 31 | } 32 | } 33 | 34 | private static native long createEncoder0(int sampleRate, int channels, Application application) throws IOException; 35 | 36 | private native void setMaxPayloadSize0(long encoderPointer, int maxPayloadSize); 37 | 38 | public void setMaxPayloadSize(int maxPayloadSize) { 39 | synchronized (this) { 40 | setMaxPayloadSize0(encoder, maxPayloadSize); 41 | } 42 | } 43 | 44 | private native int getMaxPayloadSize0(long encoderPointer); 45 | 46 | public int getMaxPayloadSize() { 47 | synchronized (this) { 48 | return getMaxPayloadSize0(encoder); 49 | } 50 | } 51 | 52 | private native void setMaxPacketLossPercentage0(long encoderPointer, float maxPacketLossPercentage); 53 | 54 | public void setMaxPacketLossPercentage(float maxPacketLossPercentage) { 55 | synchronized (this) { 56 | setMaxPacketLossPercentage0(encoder, maxPacketLossPercentage); 57 | } 58 | } 59 | 60 | private native float getMaxPacketLossPercentage0(long encoderPointer); 61 | 62 | public float getMaxPacketLossPercentage() { 63 | synchronized (this) { 64 | return getMaxPacketLossPercentage0(encoder); 65 | } 66 | } 67 | 68 | private native byte[] encode0(long encoderPointer, short[] input); 69 | 70 | public byte[] encode(short[] input) { 71 | synchronized (this) { 72 | return encode0(encoder, input); 73 | } 74 | } 75 | 76 | private native void resetState0(long encoderPointer); 77 | 78 | public void resetState() { 79 | synchronized (this) { 80 | resetState0(encoder); 81 | } 82 | } 83 | 84 | private native void destroyEncoder0(long encoderPointer); 85 | 86 | @Override 87 | public void close() { 88 | synchronized (this) { 89 | destroyEncoder0(encoder); 90 | encoder = 0L; 91 | } 92 | } 93 | 94 | public boolean isClosed() { 95 | synchronized (this) { 96 | return encoder == 0L; 97 | } 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | synchronized (this) { 103 | return String.format("OpusEncoder[%d]", encoder); 104 | } 105 | } 106 | 107 | public static enum Application { 108 | VOIP(0), 109 | AUDIO(1), 110 | LOW_DELAY(2); 111 | 112 | private final int value; 113 | 114 | Application(int value) { 115 | this.value = value; 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/de/maxhenkel/opus4j/OpusDecoder.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.opus4j; 2 | 3 | import de.maxhenkel.nativeutils.NativeInitializer; 4 | import de.maxhenkel.nativeutils.UnknownPlatformException; 5 | 6 | import javax.annotation.Nullable; 7 | import java.io.IOException; 8 | 9 | public class OpusDecoder implements AutoCloseable { 10 | 11 | private long decoder; 12 | 13 | /** 14 | * Creates a new Opus decoder. 15 | * 16 | * @param sampleRate the sample rate (8000, 12000, 16000, 24000, or 48000) 17 | * @param channels the number of channels (1 or 2) 18 | * @throws UnknownPlatformException if the operating system is not supported 19 | * @throws IOException if the native library could not be extracted 20 | */ 21 | public OpusDecoder(int sampleRate, int channels) throws IOException, UnknownPlatformException { 22 | synchronized (OpusDecoder.class) { 23 | NativeInitializer.load("libopus4j"); 24 | decoder = createDecoder0(sampleRate, channels); 25 | } 26 | } 27 | 28 | private static native String getOpusVersion0(); 29 | 30 | public String getOpusVersion() { 31 | synchronized (this) { 32 | return getOpusVersion0(); 33 | } 34 | } 35 | 36 | private static native long createDecoder0(int sampleRate, int channels) throws IOException; 37 | 38 | private native void setFrameSize0(long decoderPointer, int frameSize); 39 | 40 | public void setFrameSize(int frameSize) { 41 | synchronized (this) { 42 | setFrameSize0(decoder, frameSize); 43 | } 44 | } 45 | 46 | private native int getFrameSize0(long decoderPointer); 47 | 48 | public int getFrameSize() { 49 | synchronized (this) { 50 | return getFrameSize0(decoder); 51 | } 52 | } 53 | 54 | private native short[] decode0(long decoderPointer, @Nullable byte[] input, boolean fec); 55 | 56 | /** 57 | * Decodes the provided packet. 58 | * 59 | * @param input the input packet or null to do PLC 60 | * @param fec whether to do PLC 61 | * @return the decoded audio 62 | * @deprecated use {@link #decode(byte[])} with null to do PLC 63 | */ 64 | @Deprecated 65 | public short[] decode(@Nullable byte[] input, boolean fec) { 66 | synchronized (this) { 67 | return decode0(decoder, input, fec); 68 | } 69 | } 70 | 71 | /** 72 | * Decodes the provided packet. 73 | * 74 | * @param input the input packet or null to do PLC 75 | * @return the decoded audio 76 | */ 77 | public short[] decode(@Nullable byte[] input) { 78 | return decode(input, false); 79 | } 80 | 81 | /** 82 | * @return a PLC frame 83 | * @deprecated use {@link #decode(byte[])} with null to do PLC 84 | */ 85 | @Deprecated 86 | public short[] decodeFec() { 87 | return decode(null, true); 88 | } 89 | 90 | private native short[][] decodeRecover0(long decoderPointer, byte[] input, int frames); 91 | 92 | /** 93 | * Decodes the provided packet and recovers previous lost frames using FEC. 94 | *
95 | * Note that you need to set {@link OpusEncoder#setMaxPacketLossPercentage(float)} 96 | * to a non-zero value to enable FEC, otherwise PLC will be used. 97 | * 98 | *
99 | * If {@param frames} is 1, only the current frame will be decoded. 100 | *
101 | * If {@param frames} is 2, the previous frame will be recovered using in-band FEC (if enabled). 102 | *
103 | * If {@param frames} is >=3, all frames other than the current and last frame will use PLC. 104 | * 105 | * @param input the input packet 106 | * @param frames the number of frames to return (min 1 for just the current frame) 107 | * @return an array containing the decoded frames - the length of the array is equal to {@param frames} 108 | */ 109 | public short[][] decode(byte[] input, int frames) { 110 | synchronized (this) { 111 | return decodeRecover0(decoder, input, frames); 112 | } 113 | } 114 | 115 | private native void resetState0(long decoderPointer); 116 | 117 | public void resetState() { 118 | synchronized (this) { 119 | resetState0(decoder); 120 | } 121 | } 122 | 123 | private native void destroyDecoder0(long decoderPointer); 124 | 125 | @Override 126 | public void close() { 127 | synchronized (this) { 128 | destroyDecoder0(decoder); 129 | decoder = 0L; 130 | } 131 | } 132 | 133 | public boolean isClosed() { 134 | synchronized (this) { 135 | return decoder == 0L; 136 | } 137 | } 138 | 139 | @Override 140 | public String toString() { 141 | synchronized (this) { 142 | return String.format("OpusDecoder[%d]", decoder); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /native/src/encoder.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "opus.h" 9 | #include "exceptions.h" 10 | 11 | #define DEFAULT_MAX_PAYLOAD_SIZE 1024 12 | #define MAX_MAX_PAYLOAD_SIZE 4096 13 | #define DEFAULT_PACKET_LOSS_PERC 0 14 | 15 | typedef struct Encoder { 16 | OpusEncoder *encoder; 17 | uint32_t channels; 18 | jint max_payload_size; 19 | jfloat packet_loss_perc; 20 | } Encoder; 21 | 22 | /** 23 | * 24 | * @param sample_rate the sample rate 25 | * @param channels the number of channels 26 | * @param application the application 27 | * @param error the error if the encoder could not be created 28 | * @return the encoder or NULL if the encoder could not be created 29 | */ 30 | Encoder *create_encoder(const opus_int32 sample_rate, const int channels, const int application, int *error) { 31 | Encoder *encoder = malloc(sizeof(Encoder)); 32 | int err = 0; 33 | encoder->encoder = opus_encoder_create(sample_rate, channels, application, &err); 34 | *error = err; 35 | if (err < 0) { 36 | free(encoder); 37 | return NULL; 38 | } 39 | 40 | opus_encoder_ctl(encoder->encoder, OPUS_SET_INBAND_FEC(2)); 41 | *error = err; 42 | if (err < 0) { 43 | free(encoder->encoder); 44 | free(encoder); 45 | return NULL; 46 | } 47 | 48 | opus_encoder_ctl(encoder->encoder, OPUS_SET_PACKET_LOSS_PERC(DEFAULT_PACKET_LOSS_PERC)); 49 | encoder->packet_loss_perc = (float) DEFAULT_PACKET_LOSS_PERC / 100.0f; 50 | *error = err; 51 | if (err < 0) { 52 | free(encoder->encoder); 53 | free(encoder); 54 | return NULL; 55 | } 56 | encoder->channels = channels; 57 | encoder->max_payload_size = DEFAULT_MAX_PAYLOAD_SIZE; 58 | return encoder; 59 | } 60 | 61 | void destroy_encoder(Encoder *encoder) { 62 | opus_encoder_destroy(encoder->encoder); 63 | free(encoder); 64 | } 65 | 66 | /** 67 | * Gets the encoder from the encoder java object. 68 | * 69 | * @param env the JNI environment 70 | * @param encoder_pointer the pointer to the encoder 71 | * @return the encoder or NULL - If the encoder could not be retrieved, this will throw a runtime exception in Java 72 | */ 73 | Encoder *get_encoder(JNIEnv *env, const jlong encoder_pointer) { 74 | const jlong pointer = encoder_pointer; 75 | if (pointer == 0) { 76 | throw_runtime_exception(env, "Encoder is closed"); 77 | return NULL; 78 | } 79 | return (Encoder *) (uintptr_t) pointer; 80 | } 81 | 82 | JNIEXPORT jstring JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_getOpusVersion0( 83 | JNIEnv *env, 84 | jclass clazz 85 | ) { 86 | return (*env)->NewStringUTF(env, opus_get_version_string()); 87 | } 88 | 89 | JNIEXPORT jlong JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_createEncoder0( 90 | JNIEnv *env, 91 | jclass clazz, 92 | const jint sample_rate, 93 | const jint channels, 94 | jobject application 95 | ) { 96 | if (channels != 1 && channels != 2) { 97 | char *message = string_format("Invalid number of channels: %d", channels); 98 | throw_illegal_argument_exception(env, message); 99 | free(message); 100 | return 0; 101 | } 102 | const jint application_int = (*env)->GetIntField(env, application, 103 | (*env)->GetFieldID(env, (*env)->GetObjectClass(env, application), 104 | "value", "I")); 105 | 106 | int opus_application; 107 | switch (application_int) { 108 | case 1: 109 | opus_application = OPUS_APPLICATION_AUDIO; 110 | break; 111 | case 2: 112 | opus_application = OPUS_APPLICATION_RESTRICTED_LOWDELAY; 113 | break; 114 | default: 115 | opus_application = OPUS_APPLICATION_VOIP; 116 | break; 117 | } 118 | 119 | int err = 0; 120 | Encoder *encoder = create_encoder(sample_rate, channels, opus_application, &err); 121 | if (err < 0) { 122 | throw_opus_io_exception(env, err, "Failed to create encoder"); 123 | if (encoder != NULL) { 124 | destroy_encoder(encoder); 125 | } 126 | return 0; 127 | } 128 | 129 | return (jlong) (uintptr_t) encoder; 130 | } 131 | 132 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_setMaxPayloadSize0( 133 | JNIEnv *env, 134 | jobject obj, 135 | const jlong encoder_pointer, 136 | const jint max_payload_size 137 | ) { 138 | if (max_payload_size <= 0) { 139 | char *message = string_format("Invalid maximum payload size: %d", max_payload_size); 140 | throw_illegal_argument_exception(env, message); 141 | free(message); 142 | return; 143 | } 144 | if (max_payload_size > MAX_MAX_PAYLOAD_SIZE) { 145 | char *message = string_format("Maximum payload size too large: %d", max_payload_size); 146 | throw_illegal_argument_exception(env, message); 147 | free(message); 148 | return; 149 | } 150 | Encoder *encoder = get_encoder(env, encoder_pointer); 151 | if (encoder == NULL) { 152 | return; 153 | } 154 | encoder->max_payload_size = max_payload_size; 155 | } 156 | 157 | JNIEXPORT jint JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_getMaxPayloadSize0( 158 | JNIEnv *env, 159 | jobject obj, 160 | const jlong encoder_pointer 161 | ) { 162 | const Encoder *encoder = get_encoder(env, encoder_pointer); 163 | if (encoder == NULL) { 164 | return 0; 165 | } 166 | return encoder->max_payload_size; 167 | } 168 | 169 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_setMaxPacketLossPercentage0( 170 | JNIEnv *env, 171 | jobject obj, 172 | const jlong encoder_pointer, 173 | const jfloat packet_loss_perc 174 | ) { 175 | if (isnan(packet_loss_perc) || isinf(packet_loss_perc) || packet_loss_perc < 0.0f || packet_loss_perc > 1.0f || ( 176 | packet_loss_perc > 0.0f && packet_loss_perc < FLT_MIN)) { 177 | char *message = string_format("Invalid max packet loss percentage: %g", packet_loss_perc); 178 | throw_illegal_argument_exception(env, message); 179 | free(message); 180 | return; 181 | } 182 | Encoder *encoder = get_encoder(env, encoder_pointer); 183 | if (encoder == NULL) { 184 | return; 185 | } 186 | encoder->packet_loss_perc = packet_loss_perc; 187 | opus_encoder_ctl(encoder->encoder, OPUS_SET_PACKET_LOSS_PERC((opus_int32) (packet_loss_perc * 100.0f))); 188 | } 189 | 190 | JNIEXPORT jfloat JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_getMaxPacketLossPercentage0( 191 | JNIEnv *env, 192 | jobject obj, 193 | const jlong encoder_pointer 194 | ) { 195 | const Encoder *encoder = get_encoder(env, encoder_pointer); 196 | if (encoder == NULL) { 197 | return 0.0f; 198 | } 199 | return encoder->packet_loss_perc; 200 | } 201 | 202 | JNIEXPORT jbyteArray JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_encode0( 203 | JNIEnv *env, 204 | jobject obj, 205 | const jlong encoder_pointer, 206 | const jshortArray input 207 | ) { 208 | const Encoder *encoder = get_encoder(env, encoder_pointer); 209 | if (encoder == NULL) { 210 | return NULL; 211 | } 212 | const jint input_length = (*env)->GetArrayLength(env, input); 213 | const jint max_payload_size = encoder->max_payload_size; 214 | 215 | const opus_int16 *opus_input = (*env)->GetShortArrayElements(env, input, false); 216 | 217 | unsigned char *output = malloc(max_payload_size); 218 | 219 | const int result = opus_encode(encoder->encoder, opus_input, input_length / (jint) encoder->channels, output, 220 | max_payload_size); 221 | (*env)->ReleaseShortArrayElements(env, input, (jshort *) opus_input, JNI_ABORT); 222 | if (result < 0) { 223 | free(output); 224 | throw_opus_io_exception(env, result, "Failed to encode"); 225 | return NULL; 226 | } 227 | const jbyteArray java_output = (*env)->NewByteArray(env, result); 228 | (*env)->SetByteArrayRegion(env, java_output, 0, result, (jbyte *) output); 229 | free(output); 230 | return java_output; 231 | } 232 | 233 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_resetState0( 234 | JNIEnv *env, 235 | jobject obj, 236 | const jlong encoder_pointer 237 | ) { 238 | const Encoder *encoder = get_encoder(env, encoder_pointer); 239 | if (encoder == NULL) { 240 | return; 241 | } 242 | const int err = opus_encoder_ctl(encoder->encoder, OPUS_RESET_STATE); 243 | if (err < 0) { 244 | throw_opus_io_exception(env, err, "Failed to reset state"); 245 | } 246 | } 247 | 248 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusEncoder_destroyEncoder0( 249 | JNIEnv *env, 250 | jobject obj, 251 | const jlong encoder_pointer 252 | ) { 253 | if (encoder_pointer == 0) { 254 | return; 255 | } 256 | Encoder *encoder = (Encoder *) (uintptr_t) encoder_pointer; 257 | destroy_encoder(encoder); 258 | } 259 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /native/src/decoder.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "exceptions.h" 6 | #include "opus.h" 7 | 8 | #define DEFAULT_FRAME_SIZE 960 9 | 10 | typedef struct Decoder { 11 | OpusDecoder *decoder; 12 | int frame_size; 13 | int channels; 14 | } Decoder; 15 | 16 | /** 17 | * @param sample_rate the sample rate 18 | * @param channels the number of channels 19 | * @param error the error if the decoder could not be created 20 | * @return the decoder or NULL if the decoder could not be created 21 | */ 22 | Decoder *create_decoder(const opus_int32 sample_rate, const int channels, int *error) { 23 | Decoder *decoder = malloc(sizeof(Decoder)); 24 | int err = 0; 25 | decoder->decoder = opus_decoder_create(sample_rate, channels, &err); 26 | *error = err; 27 | if (err < 0) { 28 | free(decoder); 29 | return NULL; 30 | } 31 | decoder->frame_size = DEFAULT_FRAME_SIZE; 32 | decoder->channels = channels; 33 | return decoder; 34 | } 35 | 36 | void destroy_decoder(Decoder *decoder) { 37 | opus_decoder_destroy(decoder->decoder); 38 | free(decoder); 39 | } 40 | 41 | /** 42 | * Gets the decoder from the decoder java object. 43 | * 44 | * @param env the JNI environment 45 | * @param decoder_pointer the pointer to the decoder 46 | * @return the decoder or NULL - If the decoder could not be retrieved, this will throw a runtime exception in Java 47 | */ 48 | Decoder *get_decoder(JNIEnv *env, const jlong decoder_pointer) { 49 | if (decoder_pointer == 0) { 50 | throw_runtime_exception(env, "Decoder is closed"); 51 | return NULL; 52 | } 53 | return (Decoder *) (uintptr_t) decoder_pointer; 54 | } 55 | 56 | JNIEXPORT jstring JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_getOpusVersion0( 57 | JNIEnv *env, 58 | jclass clazz 59 | ) { 60 | return (*env)->NewStringUTF(env, opus_get_version_string()); 61 | } 62 | 63 | JNIEXPORT jlong JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_createDecoder0( 64 | JNIEnv *env, 65 | jclass clazz, 66 | const jint sample_rate, 67 | const jint channels 68 | ) { 69 | if (channels != 1 && channels != 2) { 70 | char *message = string_format("Invalid number of channels: %d", channels); 71 | throw_illegal_argument_exception(env, message); 72 | free(message); 73 | return 0; 74 | } 75 | 76 | int err = 0; 77 | Decoder *decoder = create_decoder(sample_rate, channels, &err); 78 | if (err < 0) { 79 | throw_opus_io_exception(env, err, "Failed to create decoder"); 80 | if (decoder != NULL) { 81 | destroy_decoder(decoder); 82 | } 83 | return 0; 84 | } 85 | 86 | return (jlong) (uintptr_t) decoder; 87 | } 88 | 89 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_setFrameSize0( 90 | JNIEnv *env, 91 | jobject obj, 92 | const jlong decoder_pointer, 93 | const jint frame_size 94 | ) { 95 | if (frame_size <= 0) { 96 | char *message = string_format("Invalid frame size: %d", frame_size); 97 | throw_illegal_argument_exception(env, message); 98 | free(message); 99 | return; 100 | } 101 | Decoder *decoder = get_decoder(env, decoder_pointer); 102 | if (decoder == NULL) { 103 | return; 104 | } 105 | decoder->frame_size = frame_size; 106 | } 107 | 108 | JNIEXPORT jint JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_getFrameSize0( 109 | JNIEnv *env, 110 | jobject obj, 111 | const jlong decoder_pointer 112 | ) { 113 | const Decoder *decoder = get_decoder(env, decoder_pointer); 114 | if (decoder == NULL) { 115 | return 0; 116 | } 117 | return decoder->frame_size; 118 | } 119 | 120 | JNIEXPORT jshortArray JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_decode0( 121 | JNIEnv *env, 122 | jobject obj, 123 | const jlong decoder_pointer, 124 | const jbyteArray input, 125 | const jboolean fec 126 | ) { 127 | const Decoder *decoder = get_decoder(env, decoder_pointer); 128 | if (decoder == NULL) { 129 | return NULL; 130 | } 131 | 132 | bool use_fec = fec; 133 | jsize input_length; 134 | unsigned char *opus_input; 135 | 136 | if (input == NULL) { 137 | use_fec = true; 138 | input_length = 0; 139 | opus_input = NULL; 140 | } else { 141 | input_length = (*env)->GetArrayLength(env, input); 142 | opus_input = (unsigned char *) (*env)->GetByteArrayElements(env, input, false); 143 | } 144 | 145 | const int output_length = decoder->frame_size * decoder->channels; 146 | 147 | opus_int16 *opus_output = calloc(output_length, sizeof(opus_int16)); 148 | 149 | const int result = opus_decode(decoder->decoder, opus_input, input_length, opus_output, 150 | decoder->frame_size, use_fec); 151 | 152 | if (input != NULL) { 153 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 154 | } 155 | 156 | if (result < 0) { 157 | throw_opus_io_exception(env, result, "Failed to decode"); 158 | free(opus_output); 159 | return NULL; 160 | } 161 | 162 | if (result > output_length) { 163 | char *message = string_format("Invalid output length: %d>%d", result, output_length); 164 | throw_illegal_state_exception(env, message); 165 | free(message); 166 | free(opus_output); 167 | return NULL; 168 | } 169 | 170 | const int total_samples = result * decoder->channels; 171 | const jshortArray java_output = (*env)->NewShortArray(env, total_samples); 172 | (*env)->SetShortArrayRegion(env, java_output, 0, total_samples, opus_output); 173 | free(opus_output); 174 | return java_output; 175 | } 176 | 177 | jobjectArray create_short_short_array(JNIEnv *env, const int length, const int inner_length) { 178 | const jclass shortArrayCls = (*env)->FindClass(env, "[S"); 179 | if (shortArrayCls == NULL) { 180 | throw_illegal_state_exception(env, "Failed to find short array class"); 181 | return NULL; 182 | } 183 | 184 | const jobjectArray short_short_array = (*env)->NewObjectArray(env, length, shortArrayCls, NULL); 185 | for (int i = 0; i < length; i++) { 186 | (*env)->SetObjectArrayElement(env, short_short_array, i, (*env)->NewShortArray(env, inner_length)); 187 | } 188 | return short_short_array; 189 | } 190 | 191 | void fill_short_short_array(JNIEnv *env, const jobjectArray short_short_array, const int index, const opus_int16 *data, 192 | const int length) { 193 | const jshortArray short_array = (*env)->GetObjectArrayElement(env, short_short_array, index); 194 | const jsize array_length = (*env)->GetArrayLength(env, short_array); 195 | if (array_length != length) { 196 | (*env)->DeleteLocalRef(env, short_array); 197 | throw_illegal_state_exception(env, "Invalid array length"); 198 | return; 199 | } 200 | (*env)->SetShortArrayRegion(env, short_array, 0, length, data); 201 | (*env)->DeleteLocalRef(env, short_array); 202 | } 203 | 204 | JNIEXPORT jobjectArray JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_decodeRecover0( 205 | JNIEnv *env, 206 | jobject obj, 207 | const jlong decoder_pointer, 208 | const jbyteArray input, 209 | const jint frames 210 | ) { 211 | if (frames <= 0) { 212 | throw_illegal_argument_exception(env, "Frames must be greater than 0"); 213 | return NULL; 214 | } 215 | const Decoder *decoder = get_decoder(env, decoder_pointer); 216 | if (decoder == NULL) { 217 | return NULL; 218 | } 219 | if (input == NULL) { 220 | throw_illegal_argument_exception(env, "Can't recover without input"); 221 | return NULL; 222 | } 223 | 224 | const jsize input_length = (*env)->GetArrayLength(env, input); 225 | const unsigned char *opus_input = (unsigned char *) (*env)->GetByteArrayElements(env, input, false); 226 | const int output_length = decoder->frame_size * decoder->channels; 227 | 228 | const jobjectArray recovered = create_short_short_array(env, frames, output_length); 229 | 230 | opus_int16 *opus_output = calloc(output_length, sizeof(opus_int16)); 231 | 232 | // Recover frames if more than one got lost 233 | if (frames > 2) { 234 | for (int i = 0; i < frames - 2; i++) { 235 | const int result = opus_decode(decoder->decoder, NULL, 0, opus_output, decoder->frame_size, false); 236 | if (result < 0) { 237 | throw_opus_io_exception(env, result, "Failed to decode"); 238 | free(opus_output); 239 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 240 | return NULL; 241 | } 242 | if (result > output_length) { 243 | char *message = string_format("Invalid output length: %d>%d", result, output_length); 244 | throw_illegal_state_exception(env, message); 245 | free(message); 246 | free(opus_output); 247 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 248 | return NULL; 249 | } 250 | fill_short_short_array(env, recovered, i, opus_output, result * decoder->channels); 251 | } 252 | } 253 | // Recover the last lost frame using FEC 254 | if (frames > 1) { 255 | const int result = opus_decode(decoder->decoder, opus_input, input_length, opus_output, decoder->frame_size, 256 | true); 257 | if (result < 0) { 258 | throw_opus_io_exception(env, result, "Failed to decode"); 259 | free(opus_output); 260 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 261 | return NULL; 262 | } 263 | if (result > output_length) { 264 | char *message = string_format("Invalid output length: %d>%d", result, output_length); 265 | throw_illegal_state_exception(env, message); 266 | free(message); 267 | free(opus_output); 268 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 269 | return NULL; 270 | } 271 | fill_short_short_array(env, recovered, frames - 2, opus_output, result * decoder->channels); 272 | } 273 | // Decode the actual frame 274 | const int result = opus_decode(decoder->decoder, opus_input, input_length, opus_output, decoder->frame_size, false); 275 | if (result < 0) { 276 | throw_opus_io_exception(env, result, "Failed to decode"); 277 | free(opus_output); 278 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 279 | return NULL; 280 | } 281 | if (result > output_length) { 282 | char *message = string_format("Invalid output length: %d>%d", result, output_length); 283 | throw_illegal_state_exception(env, message); 284 | free(message); 285 | free(opus_output); 286 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 287 | return NULL; 288 | } 289 | fill_short_short_array(env, recovered, frames - 1, opus_output, result * decoder->channels); 290 | 291 | free(opus_output); 292 | (*env)->ReleaseByteArrayElements(env, input, (jbyte *) opus_input, JNI_ABORT); 293 | return recovered; 294 | } 295 | 296 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_resetState0( 297 | JNIEnv *env, 298 | jobject obj, 299 | const jlong decoder_pointer 300 | ) { 301 | const Decoder *decoder = get_decoder(env, decoder_pointer); 302 | if (decoder == NULL) { 303 | return; 304 | } 305 | const int err = opus_decoder_ctl(decoder->decoder, OPUS_RESET_STATE); 306 | if (err < 0) { 307 | throw_opus_io_exception(env, err, "Failed to reset state"); 308 | } 309 | } 310 | 311 | JNIEXPORT void JNICALL Java_de_maxhenkel_opus4j_OpusDecoder_destroyDecoder0( 312 | JNIEnv *env, 313 | jobject obj, 314 | const jlong decoder_pointer 315 | ) { 316 | if (decoder_pointer == 0) { 317 | return; 318 | } 319 | Decoder *decoder = (Decoder *) (uintptr_t) decoder_pointer; 320 | destroy_decoder(decoder); 321 | } 322 | -------------------------------------------------------------------------------- /src/test/java/de/maxhenkel/opus4j/OpusEncoderTest.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.opus4j; 2 | 3 | import de.maxhenkel.nativeutils.UnknownPlatformException; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.IOException; 8 | 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | public class OpusEncoderTest { 12 | 13 | @Test 14 | @DisplayName("Encode") 15 | void encode() throws IOException, UnknownPlatformException { 16 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 17 | byte[] encoded1 = encoder.encode(new short[120]); 18 | assertTrue(encoded1.length > 0); 19 | byte[] encoded2 = encoder.encode(new short[240]); 20 | assertTrue(encoded2.length > 0); 21 | byte[] encoded3 = encoder.encode(new short[480]); 22 | assertTrue(encoded3.length > 0); 23 | byte[] encoded4 = encoder.encode(new short[960]); 24 | assertTrue(encoded4.length > 0); 25 | byte[] encoded5 = encoder.encode(new short[1920]); 26 | assertTrue(encoded5.length > 0); 27 | byte[] encoded6 = encoder.encode(new short[2880]); 28 | assertTrue(encoded6.length > 0); 29 | } 30 | } 31 | 32 | @Test 33 | @DisplayName("Invalid encoding") 34 | void invalidEncoding() throws IOException, UnknownPlatformException { 35 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 36 | IOException e1 = assertThrowsExactly(IOException.class, () -> { 37 | encoder.encode(new short[0]); 38 | }); 39 | assertEquals("Failed to encode: invalid argument", e1.getMessage()); 40 | 41 | IOException e2 = assertThrowsExactly(IOException.class, () -> { 42 | encoder.encode(new short[1]); 43 | }); 44 | assertEquals("Failed to encode: invalid argument", e2.getMessage()); 45 | 46 | IOException e3 = assertThrowsExactly(IOException.class, () -> { 47 | encoder.encode(new short[239]); 48 | }); 49 | assertEquals("Failed to encode: invalid argument", e3.getMessage()); 50 | 51 | IOException e4 = assertThrowsExactly(IOException.class, () -> { 52 | encoder.encode(new short[961]); 53 | }); 54 | assertEquals("Failed to encode: invalid argument", e4.getMessage()); 55 | } 56 | } 57 | 58 | @Test 59 | @DisplayName("Encode with frame size") 60 | void encodeWithFrameSize() throws IOException, UnknownPlatformException { 61 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 62 | encoder.setMaxPayloadSize(1); 63 | byte[] encoded = encoder.encode(new short[960]); 64 | assertEquals(1, encoded.length); 65 | encoder.setMaxPayloadSize(4096); 66 | byte[] encoded2 = encoder.encode(new short[960]); 67 | assertTrue(encoded2.length > 0); 68 | } 69 | } 70 | 71 | @Test 72 | @DisplayName("Reset state") 73 | void resetState() throws IOException, UnknownPlatformException { 74 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 75 | encoder.encode(new short[960]); 76 | encoder.resetState(); 77 | encoder.encode(new short[960]); 78 | } 79 | } 80 | 81 | @Test 82 | @DisplayName("Invalid channel count") 83 | void invalidChannels() { 84 | assertThrowsExactly(IllegalArgumentException.class, () -> { 85 | OpusEncoder encoder = new OpusEncoder(48000, 3, OpusEncoder.Application.VOIP); 86 | encoder.close(); 87 | }); 88 | assertThrowsExactly(IllegalArgumentException.class, () -> { 89 | OpusEncoder encoder = new OpusEncoder(48000, 0, OpusEncoder.Application.VOIP); 90 | encoder.close(); 91 | }); 92 | } 93 | 94 | @Test 95 | @DisplayName("Double close") 96 | void doubleClose() throws IOException, UnknownPlatformException { 97 | OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP); 98 | encoder.encode(new short[960]); 99 | encoder.close(); 100 | encoder.close(); 101 | } 102 | 103 | @Test 104 | @DisplayName("Encode after close") 105 | void encodeAfterClose() throws IOException, UnknownPlatformException { 106 | OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP); 107 | encoder.encode(new short[960]); 108 | encoder.close(); 109 | RuntimeException e = assertThrowsExactly(RuntimeException.class, () -> { 110 | encoder.encode(new short[960]); 111 | }); 112 | assertEquals("Encoder is closed", e.getMessage()); 113 | } 114 | 115 | @Test 116 | @DisplayName("Invalid sample rate") 117 | void invalidSampleRate() { 118 | IOException e = assertThrowsExactly(IOException.class, () -> { 119 | OpusEncoder encoder = new OpusEncoder(48001, 1, OpusEncoder.Application.VOIP); 120 | encoder.close(); 121 | }); 122 | assertEquals("Failed to create encoder: invalid argument", e.getMessage()); 123 | } 124 | 125 | @Test 126 | @DisplayName("Valid sample rates") 127 | void validSampleRates() throws IOException, UnknownPlatformException { 128 | new OpusEncoder(8000, 1, OpusEncoder.Application.VOIP).close(); 129 | new OpusEncoder(12000, 1, OpusEncoder.Application.VOIP).close(); 130 | new OpusEncoder(16000, 1, OpusEncoder.Application.VOIP).close(); 131 | new OpusEncoder(24000, 1, OpusEncoder.Application.VOIP).close(); 132 | new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP).close(); 133 | } 134 | 135 | @Test 136 | @DisplayName("Get Opus version") 137 | void getOpusVersion() throws IOException, UnknownPlatformException { 138 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 139 | assertEquals("libopus", encoder.getOpusVersion().split(" ")[0]); 140 | assertTrue(encoder.getOpusVersion().matches("libopus \\d+\\.\\d+\\.\\d+")); 141 | } 142 | } 143 | 144 | @Test 145 | @DisplayName("Invalid maximum payload size") 146 | void invalidMaximumPayloadSize() throws IOException, UnknownPlatformException { 147 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 148 | IllegalArgumentException e1 = assertThrowsExactly(IllegalArgumentException.class, () -> { 149 | encoder.setMaxPayloadSize(0); 150 | }); 151 | assertEquals("Invalid maximum payload size: 0", e1.getMessage()); 152 | IllegalArgumentException e2 = assertThrowsExactly(IllegalArgumentException.class, () -> { 153 | encoder.setMaxPayloadSize(-1); 154 | }); 155 | assertEquals("Invalid maximum payload size: -1", e2.getMessage()); 156 | IllegalArgumentException e3 = assertThrowsExactly(IllegalArgumentException.class, () -> { 157 | encoder.setMaxPayloadSize(Integer.MIN_VALUE); 158 | }); 159 | assertEquals("Invalid maximum payload size: " + Integer.MIN_VALUE, e3.getMessage()); 160 | IllegalArgumentException e4 = assertThrowsExactly(IllegalArgumentException.class, () -> { 161 | encoder.setMaxPayloadSize(4097); 162 | }); 163 | assertEquals("Maximum payload size too large: 4097", e4.getMessage()); 164 | IllegalArgumentException e5 = assertThrowsExactly(IllegalArgumentException.class, () -> { 165 | encoder.setMaxPayloadSize(Integer.MAX_VALUE); 166 | }); 167 | assertEquals("Maximum payload size too large: " + Integer.MAX_VALUE, e5.getMessage()); 168 | } 169 | } 170 | 171 | @Test 172 | @DisplayName("Get payload size") 173 | void getPayloadSize() throws IOException, UnknownPlatformException { 174 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 175 | encoder.setMaxPayloadSize(1); 176 | assertEquals(1, encoder.getMaxPayloadSize()); 177 | encoder.setMaxPayloadSize(128); 178 | assertEquals(128, encoder.getMaxPayloadSize()); 179 | encoder.setMaxPayloadSize(4096); 180 | assertEquals(4096, encoder.getMaxPayloadSize()); 181 | } 182 | } 183 | 184 | @Test 185 | @DisplayName("Get max packet loss percentage") 186 | void getMaxPacketLossPercentage() throws IOException, UnknownPlatformException { 187 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 188 | // Default is 0 (0% -> off by default) 189 | assertEquals(0F, encoder.getMaxPacketLossPercentage()); 190 | encoder.setMaxPacketLossPercentage(0F); 191 | assertEquals(0F, encoder.getMaxPacketLossPercentage()); 192 | encoder.setMaxPacketLossPercentage(1F); 193 | assertEquals(1F, encoder.getMaxPacketLossPercentage()); 194 | encoder.setMaxPacketLossPercentage(0.25F); 195 | assertEquals(0.25F, encoder.getMaxPacketLossPercentage()); 196 | encoder.setMaxPacketLossPercentage(0.5F); 197 | assertEquals(0.5F, encoder.getMaxPacketLossPercentage()); 198 | encoder.setMaxPacketLossPercentage(0.75F); 199 | assertEquals(0.75F, encoder.getMaxPacketLossPercentage()); 200 | } 201 | } 202 | 203 | @Test 204 | @DisplayName("Set invalid max packet loss percentage") 205 | void setInvalidMaxPacketLossPercentage() throws IOException, UnknownPlatformException { 206 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 207 | IllegalArgumentException e1 = assertThrowsExactly(IllegalArgumentException.class, () -> { 208 | encoder.setMaxPacketLossPercentage(-1F); 209 | }); 210 | assertEquals("Invalid max packet loss percentage: -1", e1.getMessage()); 211 | IllegalArgumentException e2 = assertThrowsExactly(IllegalArgumentException.class, () -> { 212 | encoder.setMaxPacketLossPercentage(2F); 213 | }); 214 | assertEquals("Invalid max packet loss percentage: 2", e2.getMessage()); 215 | IllegalArgumentException e3 = assertThrowsExactly(IllegalArgumentException.class, () -> { 216 | encoder.setMaxPacketLossPercentage(Float.MIN_VALUE); 217 | }); 218 | assertEquals("Invalid max packet loss percentage: 1.4013e-45", e3.getMessage()); 219 | IllegalArgumentException e4 = assertThrowsExactly(IllegalArgumentException.class, () -> { 220 | encoder.setMaxPacketLossPercentage(Float.MAX_VALUE); 221 | }); 222 | assertEquals("Invalid max packet loss percentage: 3.40282e+38", e4.getMessage()); 223 | IllegalArgumentException e5 = assertThrowsExactly(IllegalArgumentException.class, () -> { 224 | encoder.setMaxPacketLossPercentage(Float.NaN); 225 | }); 226 | assertEquals("Invalid max packet loss percentage: nan", e5.getMessage()); 227 | IllegalArgumentException e6 = assertThrowsExactly(IllegalArgumentException.class, () -> { 228 | encoder.setMaxPacketLossPercentage(Float.POSITIVE_INFINITY); 229 | }); 230 | assertEquals("Invalid max packet loss percentage: inf", e6.getMessage()); 231 | IllegalArgumentException e7 = assertThrowsExactly(IllegalArgumentException.class, () -> { 232 | encoder.setMaxPacketLossPercentage(Float.NEGATIVE_INFINITY); 233 | }); 234 | assertEquals("Invalid max packet loss percentage: -inf", e7.getMessage()); 235 | } 236 | } 237 | 238 | @Test 239 | @DisplayName("Is closed") 240 | void isClosed() throws IOException, UnknownPlatformException { 241 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 242 | assertFalse(encoder.isClosed()); 243 | encoder.close(); 244 | assertTrue(encoder.isClosed()); 245 | } 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /src/test/java/de/maxhenkel/opus4j/OpusDecoderTest.java: -------------------------------------------------------------------------------- 1 | package de.maxhenkel.opus4j; 2 | 3 | import de.maxhenkel.nativeutils.UnknownPlatformException; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.IOException; 8 | 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | public class OpusDecoderTest { 12 | 13 | @Test 14 | @DisplayName("Decode") 15 | void decode() throws IOException, UnknownPlatformException { 16 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 17 | byte[] encoded1 = encoder.encode(new short[120]); 18 | byte[] encoded2 = encoder.encode(new short[240]); 19 | byte[] encoded3 = encoder.encode(new short[480]); 20 | byte[] encoded4 = encoder.encode(new short[960]); 21 | byte[] encoded5 = encoder.encode(new short[1920]); 22 | byte[] encoded6 = encoder.encode(new short[2880]); 23 | 24 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 25 | decoder.setFrameSize(2880); 26 | short[] decoded1 = decoder.decode(encoded1); 27 | assertEquals(120, decoded1.length); 28 | short[] decoded2 = decoder.decode(encoded2); 29 | assertEquals(240, decoded2.length); 30 | short[] decoded3 = decoder.decode(encoded3); 31 | assertEquals(480, decoded3.length); 32 | short[] decoded4 = decoder.decode(encoded4); 33 | assertEquals(960, decoded4.length); 34 | short[] decoded5 = decoder.decode(encoded5); 35 | assertEquals(1920, decoded5.length); 36 | short[] decoded6 = decoder.decode(encoded6); 37 | assertEquals(2880, decoded6.length); 38 | } 39 | } 40 | } 41 | 42 | @Test 43 | @DisplayName("Decode stereo") 44 | void decodeStereo() throws IOException, UnknownPlatformException { 45 | try (OpusEncoder encoder = new OpusEncoder(48000, 2, OpusEncoder.Application.VOIP)) { 46 | byte[] encoded1 = encoder.encode(new short[120 * 2]); 47 | byte[] encoded2 = encoder.encode(new short[240 * 2]); 48 | byte[] encoded3 = encoder.encode(new short[480 * 2]); 49 | byte[] encoded4 = encoder.encode(new short[960 * 2]); 50 | byte[] encoded5 = encoder.encode(new short[1920 * 2]); 51 | byte[] encoded6 = encoder.encode(new short[2880 * 2]); 52 | 53 | try (OpusDecoder decoder = new OpusDecoder(48000, 2)) { 54 | decoder.setFrameSize(2880); 55 | short[] decoded1 = decoder.decode(encoded1); 56 | assertEquals(120 * 2, decoded1.length); 57 | short[] decoded2 = decoder.decode(encoded2); 58 | assertEquals(240 * 2, decoded2.length); 59 | short[] decoded3 = decoder.decode(encoded3); 60 | assertEquals(480 * 2, decoded3.length); 61 | short[] decoded4 = decoder.decode(encoded4); 62 | assertEquals(960 * 2, decoded4.length); 63 | short[] decoded5 = decoder.decode(encoded5); 64 | assertEquals(1920 * 2, decoded5.length); 65 | short[] decoded6 = decoder.decode(encoded6); 66 | assertEquals(2880 * 2, decoded6.length); 67 | } 68 | } 69 | } 70 | 71 | @Test 72 | @DisplayName("Decode PLC") 73 | void decodePlc() throws IOException, UnknownPlatformException { 74 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 75 | decoder.setFrameSize(960); 76 | short[] decodedFec1 = decoder.decodeFec(); 77 | assertEquals(960, decodedFec1.length); 78 | short[] decodedFec2 = decoder.decode(null); 79 | assertEquals(960, decodedFec2.length); 80 | short[] decodedFec3 = decoder.decode(new byte[0], true); 81 | assertEquals(960, decodedFec3.length); 82 | } 83 | } 84 | 85 | @Test 86 | @DisplayName("Decode in-band FEC") 87 | void decodeInBandFec() throws IOException, UnknownPlatformException { 88 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 89 | encoder.setMaxPacketLossPercentage(0.4F); 90 | byte[][] encoded = new byte[10][]; 91 | for (int i = 0; i < encoded.length; i++) { 92 | encoded[i] = encoder.encode(new short[960]); 93 | } 94 | 95 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 96 | decoder.setFrameSize(960); 97 | for (int i = 0; i < 5; i++) { 98 | decoder.decode(encoded[i]); 99 | } 100 | short[][] recovered = decoder.decode(encoded[encoded.length - 1], 5); 101 | assertEquals(5, recovered.length); 102 | for (short[] shorts : recovered) { 103 | assertEquals(960, shorts.length); 104 | } 105 | } 106 | } 107 | } 108 | 109 | @Test 110 | @DisplayName("Decode in-band FEC with null") 111 | void decodeInBandFecNull() throws IOException, UnknownPlatformException { 112 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 113 | IllegalArgumentException e1 = assertThrowsExactly(IllegalArgumentException.class, () -> { 114 | decoder.decode(null, 5); 115 | }); 116 | assertEquals("Can't recover without input", e1.getMessage()); 117 | } 118 | } 119 | 120 | @Test 121 | @DisplayName("Decode invalid packet") 122 | void decodeInvalidFrameSize() throws IOException, UnknownPlatformException { 123 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 124 | byte[] encoded = encoder.encode(new short[960]); 125 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 126 | byte[] cutoff = new byte[encoded.length - 4]; 127 | System.arraycopy(encoded, 4, cutoff, 0, cutoff.length); 128 | IOException e1 = assertThrowsExactly(IOException.class, () -> { 129 | decoder.decode(cutoff); 130 | }); 131 | assertEquals("Failed to decode: corrupted stream", e1.getMessage()); 132 | } 133 | } 134 | } 135 | 136 | @Test 137 | @DisplayName("Wrong frame size") 138 | void wrongFrameSize() throws IOException, UnknownPlatformException { 139 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 140 | byte[] encoded1 = encoder.encode(new short[120]); 141 | byte[] encoded2 = encoder.encode(new short[2880]); 142 | 143 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 144 | decoder.setFrameSize(119); 145 | IOException e1 = assertThrowsExactly(IOException.class, () -> { 146 | decoder.decode(encoded1); 147 | }); 148 | assertEquals("Failed to decode: buffer too small", e1.getMessage()); 149 | 150 | decoder.setFrameSize(2879); 151 | IOException e2 = assertThrowsExactly(IOException.class, () -> { 152 | decoder.decode(encoded2); 153 | }); 154 | assertEquals("Failed to decode: buffer too small", e2.getMessage()); 155 | 156 | decoder.setFrameSize(100_000); 157 | decoder.decode(encoded1); 158 | } 159 | } 160 | } 161 | 162 | @Test 163 | @DisplayName("Stereo frame size") 164 | void stereoFrameSize() throws IOException, UnknownPlatformException { 165 | try (OpusEncoder encoder = new OpusEncoder(48000, 2, OpusEncoder.Application.VOIP)) { 166 | byte[] encoded = encoder.encode(new short[2880 * 2]); 167 | try (OpusDecoder decoder = new OpusDecoder(48000, 2)) { 168 | decoder.setFrameSize(2880); 169 | short[] decode = decoder.decode(encoded); 170 | assertEquals(2880 * 2, decode.length); 171 | } 172 | } 173 | } 174 | 175 | @Test 176 | @DisplayName("Get frame size") 177 | void getFrameSize() throws IOException, UnknownPlatformException { 178 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 179 | IllegalArgumentException e1 = assertThrowsExactly(IllegalArgumentException.class, () -> { 180 | decoder.setFrameSize(Integer.MIN_VALUE); 181 | }); 182 | assertEquals("Invalid frame size: " + Integer.MIN_VALUE, e1.getMessage()); 183 | 184 | IllegalArgumentException e2 = assertThrowsExactly(IllegalArgumentException.class, () -> { 185 | decoder.setFrameSize(-1); 186 | }); 187 | assertEquals("Invalid frame size: -1", e2.getMessage()); 188 | 189 | IllegalArgumentException e3 = assertThrowsExactly(IllegalArgumentException.class, () -> { 190 | decoder.setFrameSize(0); 191 | }); 192 | assertEquals("Invalid frame size: 0", e3.getMessage()); 193 | 194 | decoder.setFrameSize(1); 195 | assertEquals(1, decoder.getFrameSize()); 196 | 197 | decoder.setFrameSize(960); 198 | assertEquals(960, decoder.getFrameSize()); 199 | 200 | decoder.setFrameSize(Integer.MAX_VALUE); 201 | assertEquals(Integer.MAX_VALUE, decoder.getFrameSize()); 202 | } 203 | } 204 | 205 | @Test 206 | @DisplayName("Reset state") 207 | void resetState() throws IOException, UnknownPlatformException { 208 | try (OpusEncoder encoder = new OpusEncoder(48000, 1, OpusEncoder.Application.VOIP)) { 209 | byte[] encoded1 = encoder.encode(new short[120]); 210 | byte[] encoded2 = encoder.encode(new short[2880]); 211 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 212 | decoder.setFrameSize(2880); 213 | decoder.decode(encoded1); 214 | decoder.resetState(); 215 | decoder.decode(encoded2); 216 | decoder.resetState(); 217 | } 218 | } 219 | } 220 | 221 | @Test 222 | @DisplayName("Invalid channel count") 223 | void invalidChannels() { 224 | assertThrowsExactly(IllegalArgumentException.class, () -> { 225 | OpusDecoder decoder = new OpusDecoder(48000, 3); 226 | decoder.close(); 227 | }); 228 | assertThrowsExactly(IllegalArgumentException.class, () -> { 229 | OpusDecoder encoder = new OpusDecoder(48000, 0); 230 | encoder.close(); 231 | }); 232 | } 233 | 234 | @Test 235 | @DisplayName("Decode after close") 236 | void encodeAfterClose() throws IOException, UnknownPlatformException { 237 | OpusDecoder decoder = new OpusDecoder(48000, 1); 238 | decoder.decode(null); 239 | decoder.close(); 240 | RuntimeException e = assertThrowsExactly(RuntimeException.class, () -> { 241 | decoder.decode(null); 242 | }); 243 | assertEquals("Decoder is closed", e.getMessage()); 244 | } 245 | 246 | @Test 247 | @DisplayName("Invalid sample rate") 248 | void invalidSampleRate() { 249 | IOException e = assertThrowsExactly(IOException.class, () -> { 250 | OpusDecoder decoder = new OpusDecoder(48001, 1); 251 | decoder.close(); 252 | }); 253 | assertEquals("Failed to create decoder: invalid argument", e.getMessage()); 254 | } 255 | 256 | @Test 257 | @DisplayName("Valid sample rates") 258 | void validSampleRates() throws IOException, UnknownPlatformException { 259 | new OpusDecoder(8000, 1).close(); 260 | new OpusDecoder(12000, 1).close(); 261 | new OpusDecoder(16000, 1).close(); 262 | new OpusDecoder(24000, 1).close(); 263 | new OpusDecoder(48000, 1).close(); 264 | } 265 | 266 | @Test 267 | @DisplayName("Is closed") 268 | void isClosed() throws IOException, UnknownPlatformException { 269 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 270 | assertFalse(decoder.isClosed()); 271 | decoder.close(); 272 | assertTrue(decoder.isClosed()); 273 | } 274 | } 275 | 276 | @Test 277 | @DisplayName("Get Opus version") 278 | void getOpusVersion() throws IOException, UnknownPlatformException { 279 | try (OpusDecoder decoder = new OpusDecoder(48000, 1)) { 280 | assertEquals("libopus", decoder.getOpusVersion().split(" ")[0]); 281 | assertTrue(decoder.getOpusVersion().matches("libopus \\d+\\.\\d+\\.\\d+")); 282 | } 283 | } 284 | 285 | } 286 | --------------------------------------------------------------------------------