From ef93dcd733b26e0a0874cc0ef0e0cf62e4904a91 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 22 Feb 2023 21:14:57 +0800 Subject: [PATCH] Add JNI (#57) --- .github/scripts/.gitignore | 2 + .github/scripts/AssetManager.kt | 4 + .github/scripts/Main.kt | 45 +++++ .github/scripts/SherpaOnnx.kt | 89 +++++++++ .github/scripts/WaveReader.kt | 17 ++ .github/scripts/test-jni.sh | 33 ++++ .github/workflows/jni.yaml | 59 ++++++ CMakeLists.txt | 7 + sherpa-onnx/CMakeLists.txt | 4 + sherpa-onnx/jni/CMakeLists.txt | 14 ++ sherpa-onnx/jni/jni.cc | 315 ++++++++++++++++++++++++++++++++ 11 files changed, 589 insertions(+) create mode 100644 .github/scripts/.gitignore create mode 100644 .github/scripts/AssetManager.kt create mode 100644 .github/scripts/Main.kt create mode 100644 .github/scripts/SherpaOnnx.kt create mode 100644 .github/scripts/WaveReader.kt create mode 100755 .github/scripts/test-jni.sh create mode 100644 .github/workflows/jni.yaml create mode 100644 sherpa-onnx/jni/CMakeLists.txt create mode 100644 sherpa-onnx/jni/jni.cc diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore new file mode 100644 index 00000000..06a90796 --- /dev/null +++ b/.github/scripts/.gitignore @@ -0,0 +1,2 @@ +Makefile +*.jar diff --git a/.github/scripts/AssetManager.kt b/.github/scripts/AssetManager.kt new file mode 100644 index 00000000..309a9b7c --- /dev/null +++ b/.github/scripts/AssetManager.kt @@ -0,0 +1,4 @@ +package android.content.res + +// a dummy class for testing only +class AssetManager diff --git a/.github/scripts/Main.kt b/.github/scripts/Main.kt new file mode 100644 index 00000000..8ed0698a --- /dev/null +++ b/.github/scripts/Main.kt @@ -0,0 +1,45 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager + +fun main() { + var featConfig = FeatureConfig( + sampleRate=16000.0f, + featureDim=80, + ) + + var modelConfig = OnlineTransducerModelConfig( + encoder="./sherpa-onnx-streaming-zipformer-en-2023-02-21/encoder-epoch-99-avg-1.onnx", + decoder="./sherpa-onnx-streaming-zipformer-en-2023-02-21/decoder-epoch-99-avg-1.onnx", + joiner="./sherpa-onnx-streaming-zipformer-en-2023-02-21/joiner-epoch-99-avg-1.onnx", + numThreads=4, + debug=false, + ) + + var endpointConfig = EndpointConfig() + + var config = OnlineRecognizerConfig( + modelConfig=modelConfig, + featConfig=featConfig, + endpointConfig=endpointConfig, + tokens="./sherpa-onnx-streaming-zipformer-en-2023-02-21/tokens.txt", + enableEndpoint=true, + ) + + var model = SherpaOnnx( + assetManager = AssetManager(), + config = config, + ) + var samples = WaveReader.readWave( + assetManager = AssetManager(), + filename = "./sherpa-onnx-streaming-zipformer-en-2023-02-21/test_wavs/1089-134686-0001.wav", + ) + + model.decodeSamples(samples!!) + + var tail_paddings = FloatArray(8000) // 0.5 seconds + model.decodeSamples(tail_paddings) + + model.inputFinished() + println(model.text) +} diff --git a/.github/scripts/SherpaOnnx.kt b/.github/scripts/SherpaOnnx.kt new file mode 100644 index 00000000..e18250bf --- /dev/null +++ b/.github/scripts/SherpaOnnx.kt @@ -0,0 +1,89 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager + +data class EndpointRule( + var mustContainNonSilence: Boolean, + var minTrailingSilence: Float, + var minUtteranceLength: Float, +) + +data class EndpointConfig( + var rule1: EndpointRule = EndpointRule(false, 2.4f, 0.0f), + var rule2: EndpointRule = EndpointRule(true, 1.4f, 0.0f), + var rule3: EndpointRule = EndpointRule(false, 0.0f, 20.0f) +) + +data class OnlineTransducerModelConfig( + var encoder: String, + var decoder: String, + var joiner: String, + var numThreads: Int = 4, + var debug: Boolean = false, +) + +data class FeatureConfig( + var sampleRate: Float = 16000.0f, + var featureDim: Int = 80, +) + +data class OnlineRecognizerConfig( + var featConfig: FeatureConfig = FeatureConfig(), + var modelConfig: OnlineTransducerModelConfig, + var tokens: String, + var endpointConfig: EndpointConfig = EndpointConfig(), + var enableEndpoint: Boolean, +) + +class SherpaOnnx( + assetManager: AssetManager, + var config: OnlineRecognizerConfig +) { + private val ptr: Long + + init { + ptr = new(assetManager, config) + } + + protected fun finalize() { + delete(ptr) + } + + + fun decodeSamples(samples: FloatArray) = + decodeSamples(ptr, samples, sampleRate = config.featConfig.sampleRate) + + fun inputFinished() = inputFinished(ptr) + fun reset() = reset(ptr) + fun isEndpoint(): Boolean = isEndpoint(ptr) + + val text: String + get() = getText(ptr) + + private external fun delete(ptr: Long) + + private external fun new( + assetManager: AssetManager, + config: OnlineRecognizerConfig, + ): Long + + private external fun decodeSamples(ptr: Long, samples: FloatArray, sampleRate: Float) + private external fun inputFinished(ptr: Long) + private external fun getText(ptr: Long): String + private external fun reset(ptr: Long) + private external fun isEndpoint(ptr: Long): Boolean + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} + +fun getFeatureConfig(): FeatureConfig { + val featConfig = FeatureConfig() + featConfig.sampleRate = 16000.0f + featConfig.featureDim = 80 + + return featConfig +} diff --git a/.github/scripts/WaveReader.kt b/.github/scripts/WaveReader.kt new file mode 100644 index 00000000..4444ab93 --- /dev/null +++ b/.github/scripts/WaveReader.kt @@ -0,0 +1,17 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager + +class WaveReader { + companion object { + // Read a mono wave file. + // No resampling is made. + external fun readWave( + assetManager: AssetManager, filename: String, expected_sample_rate: Float = 16000.0f + ): FloatArray? + + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} diff --git a/.github/scripts/test-jni.sh b/.github/scripts/test-jni.sh new file mode 100755 index 00000000..0f13e3ee --- /dev/null +++ b/.github/scripts/test-jni.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -e + +mkdir -p build +cd build + +cmake \ + -DSHERPA_ONNX_ENABLE_PYTHON=OFF \ + -DSHERPA_ONNX_ENABLE_TESTS=OFF \ + -DSHERPA_ONNX_ENABLE_CHECK=OFF \ + -DBUILD_SHARED_LIBS=ON \ + -DSHERPA_ONNX_ENABLE_PORTAUDIO=OFF \ + -DSHERPA_ONNX_ENABLE_JNI=ON \ + .. + +make -j4 +ls -lh lib + +cd .. + +export LD_LIBRARY_PATH=$PWD/build/lib:$LD_LIBRARY_PATH + +cd .github/scripts/ + +git lfs install +git clone https://huggingface.co/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-02-21 + +kotlinc-jvm -include-runtime -d main.jar Main.kt WaveReader.kt SherpaOnnx.kt AssetManager.kt + +ls -lh main.jar + +java -Djava.library.path=../../build/lib -jar main.jar diff --git a/.github/workflows/jni.yaml b/.github/workflows/jni.yaml new file mode 100644 index 00000000..36b76e54 --- /dev/null +++ b/.github/workflows/jni.yaml @@ -0,0 +1,59 @@ +name: jni + +on: + push: + branches: + - master + paths: + - '.github/workflows/jni.yaml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'sherpa-onnx/csrc/*' + - 'sherpa-onnx/jni/*' + - '.github/scripts/test-jni.sh' + pull_request: + branches: + - master + paths: + - '.github/workflows/jni.yaml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'sherpa-onnx/csrc/*' + - 'sherpa-onnx/jni/*' + - '.github/scripts/test-jni.sh' + +concurrency: + group: jni-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + jni: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Display kotlin version + shell: bash + run: | + kotlinc -version + + - name: Display java version + shell: bash + run: | + java -version + echo "JAVA_HOME is: ${JAVA_HOME}" + + - name: Run JNI test + shell: bash + run: | + .github/scripts/test-jni.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 6736731d..4bd853f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ option(SHERPA_ONNX_ENABLE_TESTS "Whether to build tests" OFF) option(SHERPA_ONNX_ENABLE_CHECK "Whether to build with assert" ON) option(BUILD_SHARED_LIBS "Whether to build shared libraries" OFF) option(SHERPA_ONNX_ENABLE_PORTAUDIO "Whether to build with portaudio" ON) +option(SHERPA_ONNX_ENABLE_JNI "Whether to build JNI internface" OFF) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") @@ -44,6 +45,11 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() +if(DEFINED ANDROID_ABI) + message(STATUS "Set SHERPA_ONNX_ENABLE_JNI to ON for Android") + set(SHERPA_ONNX_ENABLE_JNI ON CACHE BOOL "" FORCE) +endif() + message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") message(STATUS "BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS}") @@ -51,6 +57,7 @@ message(STATUS "SHERPA_ONNX_ENABLE_PYTHON ${SHERPA_ONNX_ENABLE_PYTHON}") message(STATUS "SHERPA_ONNX_ENABLE_TESTS ${SHERPA_ONNX_ENABLE_TESTS}") message(STATUS "SHERPA_ONNX_ENABLE_CHECK ${SHERPA_ONNX_ENABLE_CHECK}") message(STATUS "SHERPA_ONNX_ENABLE_PORTAUDIO ${SHERPA_ONNX_ENABLE_PORTAUDIO}") +message(STATUS "SHERPA_ONNX_ENABLE_JNI ${SHERPA_ONNX_ENABLE_JNI}") set(CMAKE_CXX_STANDARD 14 CACHE STRING "The C++ version to be used.") set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/sherpa-onnx/CMakeLists.txt b/sherpa-onnx/CMakeLists.txt index 257eaba1..d641811a 100644 --- a/sherpa-onnx/CMakeLists.txt +++ b/sherpa-onnx/CMakeLists.txt @@ -2,3 +2,7 @@ add_subdirectory(csrc) if(SHERPA_ONNX_ENABLE_PYTHON) add_subdirectory(python) endif() + +if(SHERPA_ONNX_ENABLE_JNI) + add_subdirectory(jni) +endif() diff --git a/sherpa-onnx/jni/CMakeLists.txt b/sherpa-onnx/jni/CMakeLists.txt new file mode 100644 index 00000000..29e98211 --- /dev/null +++ b/sherpa-onnx/jni/CMakeLists.txt @@ -0,0 +1,14 @@ +include_directories(${CMAKE_SOURCE_DIR}) + +if(NOT DEFINED ANDROID_ABI) + if(NOT DEFINED ENV{JAVA_HOME}) + message(FATAL_ERROR "Please set the environment variable JAVA_HOME") + endif() + include_directories($ENV{JAVA_HOME}/include) + include_directories($ENV{JAVA_HOME}/include/linux) + include_directories($ENV{JAVA_HOME}/include/darwin) +endif() + +add_library(sherpa-onnx-jni jni.cc) +target_link_libraries(sherpa-onnx-jni sherpa-onnx-core) +install(TARGETS sherpa-onnx-jni DESTINATION lib) diff --git a/sherpa-onnx/jni/jni.cc b/sherpa-onnx/jni/jni.cc new file mode 100644 index 00000000..bc3cee7b --- /dev/null +++ b/sherpa-onnx/jni/jni.cc @@ -0,0 +1,315 @@ +// sherpa-onnx/jni/jni.cc +// +// Copyright (c) 2022-2023 Xiaomi Corporation +// 2022 Pingfeng Luo + +// TODO(fangjun): Add documentation to functions/methods in this file +// and also show how to use them with kotlin, possibly with java. + +// If you use ndk, you can find "jni.h" inside +// android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include +#include "jni.h" // NOLINT + +#include + +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#else +#include +#endif + +#if __ANDROID_API__ >= 8 +#include +#define SHERPA_ONNX_LOGE(...) \ + do { \ + fprintf(stderr, ##__VA_ARGS__); \ + fprintf(stderr, "\n"); \ + __android_log_print(ANDROID_LOG_WARN, "sherpa-onnx", ##__VA_ARGS__); \ + } while (0) +#else +#define SHERPA_ONNX_LOGE(...) \ + do { \ + fprintf(stderr, ##__VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while (0) +#endif + +#include "sherpa-onnx/csrc/online-recognizer.h" +#include "sherpa-onnx/csrc/wave-reader.h" + +#define SHERPA_ONNX_EXTERN_C extern "C" + +namespace sherpa_onnx { + +class SherpaOnnx { + public: + SherpaOnnx( +#if __ANDROID_API__ >= 9 + AAssetManager *mgr, +#endif + const sherpa_onnx::OnlineRecognizerConfig &config) + : recognizer_( +#if __ANDROID_API__ >= 9 + mgr, +#endif + config), + stream_(recognizer_.CreateStream()), + tail_padding_(16000 * 0.32, 0) { + } + + void DecodeSamples(float sample_rate, const float *samples, int32_t n) const { + stream_->AcceptWaveform(sample_rate, samples, n); + Decode(); + } + + void InputFinished() const { + stream_->AcceptWaveform(16000, tail_padding_.data(), tail_padding_.size()); + stream_->InputFinished(); + Decode(); + } + + const std::string GetText() const { + auto result = recognizer_.GetResult(stream_.get()); + return result.text; + } + + bool IsEndpoint() const { return recognizer_.IsEndpoint(stream_.get()); } + + void Reset() const { return recognizer_.Reset(stream_.get()); } + + private: + void Decode() const { + while (recognizer_.IsReady(stream_.get())) { + recognizer_.DecodeStream(stream_.get()); + } + } + + private: + sherpa_onnx::OnlineRecognizer recognizer_; + std::unique_ptr stream_; + std::vector tail_padding_; +}; + +static OnlineRecognizerConfig GetConfig(JNIEnv *env, jobject config) { + OnlineRecognizerConfig ans; + + jclass cls = env->GetObjectClass(config); + jfieldID fid; + + // https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html + // https://courses.cs.washington.edu/courses/cse341/99wi/java/tutorial/native1.1/implementing/field.html + + //---------- feat config ---------- + fid = env->GetFieldID(cls, "featConfig", + "Lcom/k2fsa/sherpa/onnx/FeatureConfig;"); + jobject feat_config = env->GetObjectField(config, fid); + jclass feat_config_cls = env->GetObjectClass(feat_config); + + fid = env->GetFieldID(feat_config_cls, "sampleRate", "F"); + ans.feat_config.sampling_rate = env->GetFloatField(feat_config, fid); + + fid = env->GetFieldID(feat_config_cls, "featureDim", "I"); + ans.feat_config.feature_dim = env->GetIntField(feat_config, fid); + + //---------- enable endpoint ---------- + fid = env->GetFieldID(cls, "enableEndpoint", "Z"); + ans.enable_endpoint = env->GetBooleanField(config, fid); + + //---------- endpoint_config ---------- + + fid = env->GetFieldID(cls, "endpointConfig", + "Lcom/k2fsa/sherpa/onnx/EndpointConfig;"); + jobject endpoint_config = env->GetObjectField(config, fid); + jclass endpoint_config_cls = env->GetObjectClass(endpoint_config); + + fid = env->GetFieldID(endpoint_config_cls, "rule1", + "Lcom/k2fsa/sherpa/onnx/EndpointRule;"); + jobject rule1 = env->GetObjectField(endpoint_config, fid); + jclass rule_class = env->GetObjectClass(rule1); + + fid = env->GetFieldID(endpoint_config_cls, "rule2", + "Lcom/k2fsa/sherpa/onnx/EndpointRule;"); + jobject rule2 = env->GetObjectField(endpoint_config, fid); + + fid = env->GetFieldID(endpoint_config_cls, "rule3", + "Lcom/k2fsa/sherpa/onnx/EndpointRule;"); + jobject rule3 = env->GetObjectField(endpoint_config, fid); + + fid = env->GetFieldID(rule_class, "mustContainNonSilence", "Z"); + ans.endpoint_config.rule1.must_contain_nonsilence = + env->GetBooleanField(rule1, fid); + ans.endpoint_config.rule2.must_contain_nonsilence = + env->GetBooleanField(rule2, fid); + ans.endpoint_config.rule3.must_contain_nonsilence = + env->GetBooleanField(rule3, fid); + + fid = env->GetFieldID(rule_class, "minTrailingSilence", "F"); + ans.endpoint_config.rule1.min_trailing_silence = + env->GetFloatField(rule1, fid); + ans.endpoint_config.rule2.min_trailing_silence = + env->GetFloatField(rule2, fid); + ans.endpoint_config.rule3.min_trailing_silence = + env->GetFloatField(rule3, fid); + + fid = env->GetFieldID(rule_class, "minUtteranceLength", "F"); + ans.endpoint_config.rule1.min_utterance_length = + env->GetFloatField(rule1, fid); + ans.endpoint_config.rule2.min_utterance_length = + env->GetFloatField(rule2, fid); + ans.endpoint_config.rule3.min_utterance_length = + env->GetFloatField(rule3, fid); + + //---------- tokens ---------- + + fid = env->GetFieldID(cls, "tokens", "Ljava/lang/String;"); + jstring s = (jstring)env->GetObjectField(config, fid); + const char *p = env->GetStringUTFChars(s, nullptr); + ans.tokens = p; + env->ReleaseStringUTFChars(s, p); + + //---------- model config ---------- + fid = env->GetFieldID(cls, "modelConfig", + "Lcom/k2fsa/sherpa/onnx/OnlineTransducerModelConfig;"); + jobject model_config = env->GetObjectField(config, fid); + jclass model_config_cls = env->GetObjectClass(model_config); + + fid = env->GetFieldID(model_config_cls, "encoder", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(model_config, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.model_config.encoder_filename = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(model_config_cls, "decoder", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(model_config, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.model_config.decoder_filename = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(model_config_cls, "joiner", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(model_config, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.model_config.joiner_filename = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(model_config_cls, "numThreads", "I"); + ans.model_config.num_threads = env->GetIntField(model_config, fid); + + fid = env->GetFieldID(model_config_cls, "debug", "Z"); + ans.model_config.debug = env->GetBooleanField(model_config, fid); + + return ans; +} + +} // namespace sherpa_onnx + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jlong JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_new( + JNIEnv *env, jobject /*obj*/, jobject asset_manager, jobject _config) { +#if __ANDROID_API__ >= 9 + AAssetManager *mgr = AAssetManager_fromJava(env, asset_manager); + if (!mgr) { + SHERPA_ONNX_LOGE("Failed to get asset manager: %p", mgr); + } +#endif + + auto config = sherpa_onnx::GetConfig(env, _config); + auto model = new sherpa_onnx::SherpaOnnx( +#if __ANDROID_API__ >= 9 + mgr, +#endif + config); + + return (jlong)model; +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_delete( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + SHERPA_ONNX_LOGE("freed!"); + delete reinterpret_cast(ptr); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_reset( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + auto model = reinterpret_cast(ptr); + model->Reset(); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT bool JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_isEndpoint( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + auto model = reinterpret_cast(ptr); + return model->IsEndpoint(); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_decodeSamples( + JNIEnv *env, jobject /*obj*/, jlong ptr, jfloatArray samples, + jfloat sample_rate) { + auto model = reinterpret_cast(ptr); + + jfloat *p = env->GetFloatArrayElements(samples, nullptr); + jsize n = env->GetArrayLength(samples); + + model->DecodeSamples(sample_rate, p, n); + + env->ReleaseFloatArrayElements(samples, p, JNI_ABORT); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_inputFinished( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + reinterpret_cast(ptr)->InputFinished(); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jstring JNICALL Java_com_k2fsa_sherpa_onnx_SherpaOnnx_getText( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + // see + // https://stackoverflow.com/questions/11621449/send-c-string-to-java-via-jni + auto text = reinterpret_cast(ptr)->GetText(); + return env->NewStringUTF(text.c_str()); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jfloatArray JNICALL +Java_com_k2fsa_sherpa_onnx_WaveReader_00024Companion_readWave( + JNIEnv *env, jclass /*cls*/, jobject asset_manager, jstring filename, + jfloat expected_sample_rate) { + const char *p_filename = env->GetStringUTFChars(filename, nullptr); +#if __ANDROID_API__ >= 9 + AAssetManager *mgr = AAssetManager_fromJava(env, asset_manager); + if (!mgr) { + SHERPA_ONNX_LOGE("Failed to get asset manager: %p", mgr); + return nullptr; + } + + AAsset *asset = AAssetManager_open(mgr, p_filename, AASSET_MODE_BUFFER); + size_t asset_length = AAsset_getLength(asset); + std::vector buffer(asset_length); + AAsset_read(asset, buffer.data(), asset_length); + + std::istrstream is(buffer.data(), asset_length); +#else + std::ifstream is(p_filename, std::ios::binary); +#endif + + bool is_ok = false; + std::vector samples = + sherpa_onnx::ReadWave(is, expected_sample_rate, &is_ok); + +#if __ANDROID_API__ >= 9 + AAsset_close(asset); +#endif + env->ReleaseStringUTFChars(filename, p_filename); + + if (!is_ok) { + return nullptr; + } + + jfloatArray ans = env->NewFloatArray(samples.size()); + env->SetFloatArrayRegion(ans, 0, samples.size(), samples.data()); + return ans; +}