]> git.djapps.eu Git - pkg/ggml/sources/whisper.cpp/commitdiff
examples : add whisper.android.java for compatibility with older Android versions...
authorTong Li <redacted>
Sun, 12 Nov 2023 16:31:58 +0000 (06:31 -1000)
committerGitHub <redacted>
Sun, 12 Nov 2023 16:31:58 +0000 (18:31 +0200)
* save the recorded audio to a file

* Alignment -help

* Save the correct audio

* chage to a consistent coding style

* Correct typo

* Update examples/stream/stream.cpp

* Update examples/stream/stream.cpp

* Correct variable misuse

* Update examples/stream/stream.cpp

* Update examples/stream/stream.cpp

* Update examples/stream/stream.cpp

* Update examples/stream/stream.cpp

* add *.bin .cxx/ .gradle/ cmake-build-debug/ to gitignore

* add whisper.android.java

* Added support for older versions of Android of Java

* add examples for android java

* add README.md for android java

* add fullTranscribeWithTime

* 增加 toString()方法和测试

* change return type to void

* update to v1.4.1

* add WhisperService

* chage to whisper_full_get_segment_t1

* add method transcribeDataWithTime

* modified toString
```
return "[" + start + " --> " + end + "]:" + sentence;
```

* Optimize code logic

* update text view on handle

* set max lines

* change Chinese to English

* Update bindings/java/build.gradle

* Update .gitignore

* add android.java to github action

* chage android.java to   android_java in build.yml

* remove gradle

* chage jdk to temurin in android_java of CI

* chage jdk to temurin 11 in android_java of CI

* add x to gradlew

* set api-level for android_java of CI

* Update examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt

* add ndk version in build.gradle

* remove local.properties

* add testFullTranscribeWithTime

---------

Co-authored-by: litongmacos <redacted>
Co-authored-by: bobqianic <redacted>
59 files changed:
.github/workflows/build.yml
.gitignore
bindings/java/build.gradle
bindings/java/src/main/java/io/github/ggerganov/whispercpp/WhisperCpp.java
bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java [new file with mode: 0644]
bindings/java/src/test/java/io/github/ggerganov/whispercpp/WhisperCppTest.java
examples/whisper.android.java/.gitignore [new file with mode: 0644]
examples/whisper.android.java/README.md [new file with mode: 0644]
examples/whisper.android.java/README_files/1.jpg [new file with mode: 0644]
examples/whisper.android.java/app/.gitignore [new file with mode: 0644]
examples/whisper.android.java/app/build.gradle [new file with mode: 0644]
examples/whisper.android.java/app/proguard-rules.pro [new file with mode: 0644]
examples/whisper.android.java/app/src/androidTest/java/com/litongjava/whisper/android/java/ExampleInstrumentedTest.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/AndroidManifest.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/assets/logback.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java [new file with mode: 0644]
examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt [new file with mode: 0644]
examples/whisper.android.java/app/src/main/jni/whisper/jni.c [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/layout/activity_main.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher_round.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher_round.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/values-night/themes.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/values/colors.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/values/strings.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/main/res/values/themes.xml [new file with mode: 0644]
examples/whisper.android.java/app/src/test/java/com/litongjava/whisper/android/java/ExampleUnitTest.java [new file with mode: 0644]
examples/whisper.android.java/build.gradle [new file with mode: 0644]
examples/whisper.android.java/gradle.properties [new file with mode: 0644]
examples/whisper.android.java/gradle/wrapper/gradle-wrapper.jar [new file with mode: 0644]
examples/whisper.android.java/gradle/wrapper/gradle-wrapper.properties [new file with mode: 0644]
examples/whisper.android.java/gradlew [new file with mode: 0644]
examples/whisper.android.java/gradlew.bat [new file with mode: 0644]
examples/whisper.android.java/settings.gradle [new file with mode: 0644]
examples/whisper.android/.idea/gradle.xml

index 38e476b98e8b9e8789b2ab09cb4ea85b1797da1f..974ecda51739603855e55ef2db81d0ddf5f8412a 100644 (file)
@@ -396,6 +396,32 @@ jobs:
           cd examples/whisper.android
           ./gradlew assembleRelease --no-daemon
 
+  android_java:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Clone
+        uses: actions/checkout@v3
+
+      - name: set up JDK 11
+        uses: actions/setup-java@v3
+        with:
+          java-version: '11'
+          distribution: 'temurin'
+          cache: gradle
+
+      - name: Setup Android SDK
+        uses: android-actions/setup-android@v2
+        with:
+          api-level: 30
+          build-tools-version: 30.0.3
+
+      - name: Build
+        run: |
+          cd examples/whisper.android.java
+          chmod +x ./gradlew 
+          ./gradlew assembleRelease
+
   java:
     needs: [ 'windows' ]
     runs-on: windows-latest
index 9ff35d00b6ac5378eed4bb55aee5d000cc74231a..00325823f978d9ceb5e8151ecd9c5b712c63b3db 100644 (file)
@@ -54,3 +54,7 @@ bindings/java/.idea/
 .idea/
 
 benchmark_results.csv
+cmake-build-debug/
+.cxx/
+.gradle/
+local.properties
\ No newline at end of file
index 8f7a5fd928f5305f1714bf0891f04ac06ad5653b..75f3a9cd9018d684966bdf7b68cb6905935e3731 100644 (file)
@@ -9,6 +9,7 @@ archivesBaseName = 'whispercpp'
 group = 'io.github.ggerganov'\r
 version = '1.4.0'\r
 \r
+\r
 sourceCompatibility = 1.8\r
 targetCompatibility = 1.8\r
 \r
index 4a25040377ceccc71b9322f57b89d5aa81223579..4c1594d5d5533cc1496e84ec7233a2bdc3387a80 100644 (file)
@@ -2,6 +2,7 @@ package io.github.ggerganov.whispercpp;
 \r
 import com.sun.jna.Native;\r
 import com.sun.jna.Pointer;\r
+import io.github.ggerganov.whispercpp.bean.WhisperSegment;\r
 import io.github.ggerganov.whispercpp.params.WhisperContextParams;\r
 import io.github.ggerganov.whispercpp.params.WhisperFullParams;\r
 import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy;\r
@@ -9,6 +10,8 @@ import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy;
 import java.io.File;\r
 import java.io.FileNotFoundException;\r
 import java.io.IOException;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
 \r
 /**\r
  * Before calling most methods, you must call `initContext(modelPath)` to initialise the `ctx` Pointer.\r
@@ -160,6 +163,28 @@ public class WhisperCpp implements AutoCloseable {
 \r
         return str.toString().trim();\r
     }\r
+    public List<WhisperSegment> fullTranscribeWithTime(WhisperFullParams whisperParams, float[] audioData) throws IOException {\r
+        if (ctx == null) {\r
+            throw new IllegalStateException("Model not initialised");\r
+        }\r
+\r
+        if (lib.whisper_full(ctx, whisperParams, audioData, audioData.length) != 0) {\r
+            throw new IOException("Failed to process audio");\r
+        }\r
+\r
+        int nSegments = lib.whisper_full_n_segments(ctx);\r
+        List<WhisperSegment> segments= new ArrayList<>(nSegments);\r
+\r
+\r
+        for (int i = 0; i < nSegments; i++) {\r
+            long t0 = lib.whisper_full_get_segment_t0(ctx, i);\r
+            String text = lib.whisper_full_get_segment_text(ctx, i);\r
+            long t1 = lib.whisper_full_get_segment_t1(ctx, i);\r
+            segments.add(new WhisperSegment(t0,t1,text));\r
+        }\r
+\r
+        return segments;\r
+    }\r
 \r
 //    public int getTextSegmentCount(Pointer ctx) {\r
 //        return lib.whisper_full_n_segments(ctx);\r
diff --git a/bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java b/bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java
new file mode 100644 (file)
index 0000000..da970b5
--- /dev/null
@@ -0,0 +1,47 @@
+package io.github.ggerganov.whispercpp.bean;
+
+/**
+ * Created by litonglinux@qq.com on 10/21/2023_7:48 AM
+ */
+public class WhisperSegment {
+  private long start, end;
+  private String sentence;
+
+  public WhisperSegment() {
+  }
+
+  public WhisperSegment(long start, long end, String sentence) {
+    this.start = start;
+    this.end = end;
+    this.sentence = sentence;
+  }
+
+  public long getStart() {
+    return start;
+  }
+
+  public long getEnd() {
+    return end;
+  }
+
+  public String getSentence() {
+    return sentence;
+  }
+
+  public void setStart(long start) {
+    this.start = start;
+  }
+
+  public void setEnd(long end) {
+    this.end = end;
+  }
+
+  public void setSentence(String sentence) {
+    this.sentence = sentence;
+  }
+
+  @Override
+  public String toString() {
+    return "[" + start + " --> " + end + "]:" + sentence;
+  }
+}
index 66e18f9a93647a36543fe37cebdaa2a07a47680c..ccc3be8961ea3e3e512f9c68462fc60db9a83cd9 100644 (file)
@@ -2,6 +2,7 @@ package io.github.ggerganov.whispercpp;
 \r
 import static org.junit.jupiter.api.Assertions.*;\r
 \r
+import io.github.ggerganov.whispercpp.bean.WhisperSegment;\r
 import io.github.ggerganov.whispercpp.params.CBool;\r
 import io.github.ggerganov.whispercpp.params.WhisperFullParams;\r
 import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy;\r
@@ -11,6 +12,7 @@ import javax.sound.sampled.AudioInputStream;
 import javax.sound.sampled.AudioSystem;\r
 import java.io.File;\r
 import java.io.FileNotFoundException;\r
+import java.util.List;\r
 \r
 class WhisperCppTest {\r
     private static WhisperCpp whisper = new WhisperCpp();\r
@@ -20,7 +22,8 @@ class WhisperCppTest {
     static void init() throws FileNotFoundException {\r
         // By default, models are loaded from ~/.cache/whisper/ and are usually named "ggml-${name}.bin"\r
         // or you can provide the absolute path to the model file.\r
-        String modelName = "../../models/ggml-tiny.en.bin";\r
+        String modelName = "../../models/ggml-tiny.bin";\r
+//        String modelName = "../../models/ggml-tiny.en.bin";\r
         try {\r
             whisper.initContext(modelName);\r
 //            whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_GREEDY);\r
@@ -99,4 +102,44 @@ class WhisperCppTest {
             audioInputStream.close();\r
         }\r
     }\r
+\r
+    @Test\r
+    void testFullTranscribeWithTime() throws Exception {\r
+        if (!modelInitialised) {\r
+            System.out.println("Model not initialised, skipping test");\r
+            return;\r
+        }\r
+\r
+        // Given\r
+        File file = new File(System.getProperty("user.dir"), "../../samples/jfk.wav");\r
+        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);\r
+\r
+        byte[] b = new byte[audioInputStream.available()];\r
+        float[] floats = new float[b.length / 2];\r
+\r
+//        WhisperFullParams params = whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_GREEDY);\r
+        WhisperFullParams params = whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_BEAM_SEARCH);\r
+        params.setProgressCallback((ctx, state, progress, user_data) -> System.out.println("progress: " + progress));\r
+        params.print_progress = CBool.FALSE;\r
+//        params.initial_prompt = "and so my fellow Americans um, like";\r
+\r
+\r
+        try {\r
+            audioInputStream.read(b);\r
+\r
+            for (int i = 0, j = 0; i < b.length; i += 2, j++) {\r
+                int intSample = (int) (b[i + 1]) << 8 | (int) (b[i]) & 0xFF;\r
+                floats[j] = intSample / 32767.0f;\r
+            }\r
+\r
+            List<WhisperSegment> segments = whisper.fullTranscribeWithTime(params, floats);\r
+            assertTrue(segments.size() > 0, "The size of segments should be greater than 0");\r
+            for (WhisperSegment segment : segments) {\r
+                System.out.println(segment);\r
+            }\r
+        } finally {\r
+            audioInputStream.close();\r
+        }\r
+    }\r
+\r
 }\r
diff --git a/examples/whisper.android.java/.gitignore b/examples/whisper.android.java/.gitignore
new file mode 100644 (file)
index 0000000..aa724b7
--- /dev/null
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/examples/whisper.android.java/README.md b/examples/whisper.android.java/README.md
new file mode 100644 (file)
index 0000000..44675ab
--- /dev/null
@@ -0,0 +1,20 @@
+A sample Android app using java code and [whisper.cpp](https://github.com/ggerganov/whisper.cpp/) to do voice-to-text transcriptions.
+
+To use:
+
+1. Select a model from the [whisper.cpp repository](https://github.com/ggerganov/whisper.cpp/tree/master/models).[^1]
+2. Copy the model to the "app/src/main/assets/models" folder.
+3. Select a sample audio file (for example, [jfk.wav](https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav)).
+4. Copy the sample to the "app/src/main/assets/samples" folder.
+5. Modify the modelFilePath in the WhisperService.java
+6. Modify the sampleFilePath in the WhisperService.java
+7. Select the "release" active build variant, and use Android Studio to run and deploy to your device.
+[^1]: I recommend the tiny or base models for running on an Android device.
+
+PS:  
+1. Do not move this android project folder individually to other folders, because this android project folder depends on the files of the whole project.  
+2. The cpp code is compiled during the build process  
+3. If you want to import a compiled cpp project in your Android project, please refer to the https://github.com/litongjava/whisper.cpp.android.java.demo  
+
+![](README_files/1.jpg)
+
diff --git a/examples/whisper.android.java/README_files/1.jpg b/examples/whisper.android.java/README_files/1.jpg
new file mode 100644 (file)
index 0000000..035cc10
Binary files /dev/null and b/examples/whisper.android.java/README_files/1.jpg differ
diff --git a/examples/whisper.android.java/app/.gitignore b/examples/whisper.android.java/app/.gitignore
new file mode 100644 (file)
index 0000000..42afabf
--- /dev/null
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/build.gradle b/examples/whisper.android.java/app/build.gradle
new file mode 100644 (file)
index 0000000..532d780
--- /dev/null
@@ -0,0 +1,58 @@
+plugins {
+  id 'com.android.application'
+}
+
+android {
+  compileSdkVersion 30
+  buildToolsVersion '30.0.3'
+
+  defaultConfig {
+    applicationId "com.litongjava.whisper.android.java"
+    minSdkVersion 21
+    targetSdkVersion 30
+    versionCode 1
+    versionName "1.0"
+
+    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    externalNativeBuild {
+      cmake {
+        cppFlags ""
+      }
+    }
+    ndk {
+      abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
+    }
+  }
+
+  buildTypes {
+    release {
+      signingConfig signingConfigs.debug
+      minifyEnabled true
+      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+    }
+  }
+  externalNativeBuild {
+    cmake {
+      path "src/main/jni/whisper/CMakeLists.txt"
+    }
+  }
+  ndkVersion "25.2.9519653"
+  compileOptions {
+    sourceCompatibility JavaVersion.VERSION_1_8
+    targetCompatibility JavaVersion.VERSION_1_8
+  }
+}
+
+dependencies {
+  implementation 'androidx.appcompat:appcompat:1.1.0'
+  implementation 'com.google.android.material:material:1.1.0'
+  implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+  testImplementation 'junit:junit:4.+'
+  androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+  androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+
+  //litongjava
+  implementation 'com.litongjava:android-view-inject:1.0'
+  implementation 'com.litongjava:jfinal-aop:1.0.1'
+  implementation 'com.litongjava:litongjava-android-utils:1.0.0'
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/proguard-rules.pro b/examples/whisper.android.java/app/proguard-rules.pro
new file mode 100644 (file)
index 0000000..481bb43
--- /dev/null
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/androidTest/java/com/litongjava/whisper/android/java/ExampleInstrumentedTest.java b/examples/whisper.android.java/app/src/androidTest/java/com/litongjava/whisper/android/java/ExampleInstrumentedTest.java
new file mode 100644 (file)
index 0000000..7962acc
--- /dev/null
@@ -0,0 +1,26 @@
+package com.litongjava.whisper.android.java;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+  @Test
+  public void useAppContext() {
+    // Context of the app under test.
+    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    assertEquals("com.litongjava.whisper.android.java", appContext.getPackageName());
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/AndroidManifest.xml b/examples/whisper.android.java/app/src/main/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..f4980ad
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.litongjava.whisper.android.java">
+
+  <application
+    android:allowBackup="true"
+    android:name=".app.App"
+    android:icon="@mipmap/ic_launcher"
+    android:label="@string/app_name"
+    android:roundIcon="@mipmap/ic_launcher_round"
+    android:supportsRtl="true"
+    android:theme="@style/Theme.Whisperandroidjava">
+    <activity android:name=".MainActivity">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+  </application>
+
+</manifest>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/assets/logback.xml b/examples/whisper.android.java/app/src/main/assets/logback.xml
new file mode 100644 (file)
index 0000000..1bd6d92
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false" xmlns="http://ch.qos.logback/xml/ns/logback"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd
+http://ch.qos.logback/xml/ns/logback ">
+  <!--Define the storage address of the log file Do not use relative paths in the LogBack configuration. -->
+  <property name="LOG_HOME" value="logs" />
+  <!--Formatted output: %d means the date, %-6level: log level from the left display 6 characters wide, %m: log message, %n is a newline character -->
+  <property name="CONSOLE_LOG_PATTERN"
+    value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-6level%logger{0}.%M:%L - %m%n" />
+
+  <!-- console output -->
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+      <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+    </encoder>
+  </appender>
+
+  <!-- Generate log files on a daily basis -->
+  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+      <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+    </encoder>
+    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+      <!--File name for log file output -->
+      <fileNamePattern>${LOG_HOME}/project-name-%d{yyyy-MM-dd}.log</fileNamePattern>
+      <!--Maximum size of log file -->
+      <maxHistory>180</maxHistory>
+    </rollingPolicy>
+    <!--日志文件最大的大小 -->
+    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+      <maxFileSize>10MB</maxFileSize>
+    </triggeringPolicy>
+  </appender>
+  <!-- Log output level and source-->
+  <root level="info">
+    <appender-ref ref="STDOUT" />
+    <appender-ref ref="FILE" />
+  </root>
+</configuration>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java
new file mode 100644 (file)
index 0000000..b85d550
--- /dev/null
@@ -0,0 +1,107 @@
+package com.litongjava.whisper.android.java;
+
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.widget.TextView;
+
+import com.blankj.utilcode.util.ThreadUtils;
+import com.litongjava.android.view.inject.annotation.FindViewById;
+import com.litongjava.android.view.inject.annotation.FindViewByIdLayout;
+import com.litongjava.android.view.inject.annotation.OnClick;
+import com.litongjava.android.view.inject.utils.ViewInjectUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.jfinal.aop.AopManager;
+import com.litongjava.whisper.android.java.services.WhisperService;
+import com.litongjava.whisper.android.java.task.LoadModelTask;
+import com.litongjava.whisper.android.java.task.TranscriptionTask;
+import com.litongjava.whisper.android.java.utils.AssetUtils;
+import com.whispercpp.java.whisper.WhisperLib;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+
+@FindViewByIdLayout(R.layout.activity_main)
+public class MainActivity extends AppCompatActivity {
+
+  @FindViewById(R.id.sample_text)
+  private TextView tv;
+
+  Logger log = LoggerFactory.getLogger(this.getClass());
+  private WhisperService whisperService = Aop.get(WhisperService.class);
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    //setContentView(R.layout.activity_main);
+    ViewInjectUtils.injectActivity(this, this);
+    initAopBean();
+    showSystemInfo();
+  }
+
+  private void initAopBean() {
+    Handler mainHandler = new Handler(Looper.getMainLooper());
+    AopManager.me().addSingletonObject(mainHandler);
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  @OnClick(R.id.loadModelBtn)
+  public void loadModelBtn_OnClick(View v) {
+    Context context = getBaseContext();
+    ThreadUtils.executeByIo(new LoadModelTask(tv));
+  }
+
+  @OnClick(R.id.transcriptSampleBtn)
+  public void transcriptSampleBtn_OnClick(View v) {
+    Context context = getBaseContext();
+
+    long start = System.currentTimeMillis();
+    String sampleFilePath = "samples/jfk.wav";
+    File filesDir = context.getFilesDir();
+    File sampleFile = AssetUtils.copyFileIfNotExists(context, filesDir, sampleFilePath);
+    long end = System.currentTimeMillis();
+    String msg = "copy file:" + (end - start) + "ms";
+    outputMsg(tv, msg);
+    ThreadUtils.executeByIo(new TranscriptionTask(tv, sampleFile));
+  }
+
+  private void outputMsg(TextView tv, String msg) {
+    tv.append(msg + "\n");
+    log.info(msg);
+  }
+
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  @OnClick(R.id.systemInfoBtn)
+  public void systemInfoBtn_OnClick(View v) {
+    showSystemInfo();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public void showSystemInfo() {
+    String systemInfo = WhisperLib.getSystemInfo();
+    tv.append(systemInfo + "\n");
+  }
+
+  @OnClick(R.id.clearBtn)
+  public void clearBtn_OnClick(View v) {
+    tv.setText("");
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+    whisperService.release();
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java
new file mode 100644 (file)
index 0000000..afa3452
--- /dev/null
@@ -0,0 +1,13 @@
+package com.litongjava.whisper.android.java.app;
+
+import android.app.Application;
+
+import com.blankj.utilcode.util.Utils;
+
+public class App extends Application {
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    Utils.init(this);
+  }
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java
new file mode 100644 (file)
index 0000000..e529fed
--- /dev/null
@@ -0,0 +1,47 @@
+package com.litongjava.whisper.android.java.bean;
+
+/**
+ * Created by litonglinux@qq.com on 10/21/2023_7:48 AM
+ */
+public class WhisperSegment {
+  private long start, end;
+  private String sentence;
+
+  public WhisperSegment() {
+  }
+
+  public WhisperSegment(long start, long end, String sentence) {
+    this.start = start;
+    this.end = end;
+    this.sentence = sentence;
+  }
+
+  public long getStart() {
+    return start;
+  }
+
+  public long getEnd() {
+    return end;
+  }
+
+  public String getSentence() {
+    return sentence;
+  }
+
+  public void setStart(long start) {
+    this.start = start;
+  }
+
+  public void setEnd(long end) {
+    this.end = end;
+  }
+
+  public void setSentence(String sentence) {
+    this.sentence = sentence;
+  }
+
+  @Override
+  public String toString() {
+    return "["+start+" --> "+end+"]:"+sentence;
+  }
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java
new file mode 100644 (file)
index 0000000..7b97d3b
--- /dev/null
@@ -0,0 +1,101 @@
+package com.litongjava.whisper.android.java.services;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.RequiresApi;
+
+import com.blankj.utilcode.util.ToastUtils;
+import com.blankj.utilcode.util.Utils;
+import com.litongjava.android.utils.dialog.AlertDialogUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.bean.WhisperSegment;
+import com.litongjava.whisper.android.java.single.LocalWhisper;
+import com.litongjava.whisper.android.java.utils.WaveEncoder;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class WhisperService {
+  private Logger log = LoggerFactory.getLogger(this.getClass());
+
+  private final Object lock = new Object();
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public void loadModel(TextView tv) {
+    String modelFilePath = LocalWhisper.modelFilePath;
+    String msg = "load model from :" + modelFilePath + "\n";
+    outputMsg(tv, msg);
+
+    long start = System.currentTimeMillis();
+    LocalWhisper.INSTANCE.init();
+    long end = System.currentTimeMillis();
+    msg = "model load successful:" + (end - start) + "ms";
+    outputMsg(tv, msg);
+    ToastUtils.showLong(msg);
+
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public void transcribeSample(TextView tv, File sampleFile) {
+    String msg = "";
+    msg = "transcribe file from :" + sampleFile.getAbsolutePath();
+    outputMsg(tv, msg);
+
+    Long start = System.currentTimeMillis();
+    float[] audioData = new float[0];  // 读取音频样本
+    try {
+      audioData = WaveEncoder.decodeWaveFile(sampleFile);
+    } catch (IOException e) {
+      e.printStackTrace();
+      return;
+    }
+    long end = System.currentTimeMillis();
+    msg = "decode wave file:" + (end - start) + "ms";
+    outputMsg(tv, msg);
+
+    start = System.currentTimeMillis();
+    List<WhisperSegment> transcription = null;
+    try {
+      //transcription = LocalWhisper.INSTANCE.transcribeData(audioData);
+      transcription = LocalWhisper.INSTANCE.transcribeDataWithTime(audioData);
+    } catch (ExecutionException e) {
+      e.printStackTrace();
+    } catch (InterruptedException e) {
+      e.printStackTrace();
+    }
+    end = System.currentTimeMillis();
+    if(transcription!=null){
+      ToastUtils.showLong(transcription.toString());
+      msg = "Transcript successful:" + (end - start) + "ms";
+      outputMsg(tv, msg);
+
+      outputMsg(tv, transcription.toString());
+
+    }else{
+      msg = "Transcript failed:" + (end - start) + "ms";
+      outputMsg(tv, msg);
+    }
+
+  }
+
+  private void outputMsg(TextView tv, String msg) {
+    log.info(msg);
+    if(tv!=null){
+      Aop.get(Handler.class).post(()->{ tv.append(msg + "\n");});
+    }
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public void release() {
+    //noting to do
+  }
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java
new file mode 100644 (file)
index 0000000..bbf628c
--- /dev/null
@@ -0,0 +1,66 @@
+package com.litongjava.whisper.android.java.single;
+
+import android.app.Application;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.annotation.RequiresApi;
+
+import com.blankj.utilcode.util.ToastUtils;
+import com.blankj.utilcode.util.Utils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.bean.WhisperSegment;
+import com.litongjava.whisper.android.java.utils.AssetUtils;
+import com.whispercpp.java.whisper.WhisperContext;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+public enum LocalWhisper {
+  INSTANCE;
+
+  public static final String modelFilePath = "models/ggml-tiny.bin";
+  private WhisperContext whisperContext;
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  LocalWhisper() {
+    Application context = Utils.getApp();
+    File filesDir = context.getFilesDir();
+    File modelFile = AssetUtils.copyFileIfNotExists(context, filesDir, modelFilePath);
+    String realModelFilePath = modelFile.getAbsolutePath();
+    whisperContext = WhisperContext.createContextFromFile(realModelFilePath);
+  }
+
+  public synchronized String transcribeData(float[] data) throws ExecutionException, InterruptedException {
+    if(whisperContext==null){
+        toastModelLoading();
+        return null;
+    }else{
+      return whisperContext.transcribeData(data);
+    }
+  }
+
+    private static void toastModelLoading() {
+        Aop.get(Handler.class).post(()->{
+          ToastUtils.showShort("please wait for model loading");
+        });
+    }
+
+    public List<WhisperSegment> transcribeDataWithTime(float[] audioData) throws ExecutionException, InterruptedException {
+    if(whisperContext==null){
+        toastModelLoading();
+      return null;
+    }else{
+      return whisperContext.transcribeDataWithTime(audioData);
+    }
+  }
+
+  public void init() {
+    //noting to do.but init
+  }
+
+
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java
new file mode 100644 (file)
index 0000000..23fe448
--- /dev/null
@@ -0,0 +1,44 @@
+package com.litongjava.whisper.android.java.task;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.widget.TextView;
+
+import com.blankj.utilcode.util.ThreadUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.services.WhisperService;
+
+import java.io.File;
+
+public class LoadModelTask extends ThreadUtils.Task<Object> {
+  private final TextView tv;
+  public LoadModelTask(TextView tv) {
+    this.tv = tv;
+  }
+
+  @Override
+  public Object doInBackground() {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+      Aop.get(WhisperService.class).loadModel(tv);
+    }else{
+      Aop.get(Handler.class).post(()->{
+        tv.append("not supported android devices");
+      });
+
+    }
+    return null;
+  }
+
+  @Override
+  public void onSuccess(Object result) {
+  }
+
+  @Override
+  public void onCancel() {
+  }
+
+  @Override
+  public void onFail(Throwable t) {
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java
new file mode 100644 (file)
index 0000000..7477f8e
--- /dev/null
@@ -0,0 +1,44 @@
+package com.litongjava.whisper.android.java.task;
+
+import android.content.Context;
+import android.os.Build;
+import android.widget.TextView;
+
+import com.blankj.utilcode.util.ThreadUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.services.WhisperService;
+
+import java.io.File;
+
+public class TranscriptionTask extends ThreadUtils.Task<Object> {
+  private final TextView tv;
+  private final File sampleFile;
+
+  public TranscriptionTask(TextView tv, File sampleFile) {
+    this.tv = tv;
+    this.sampleFile = sampleFile;
+
+  }
+
+  @Override
+  public Object doInBackground() {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+      Aop.get(WhisperService.class).transcribeSample(tv, sampleFile);
+    }else{
+      tv.append("not supported android devices");
+    }
+    return null;
+  }
+
+  @Override
+  public void onSuccess(Object result) {
+  }
+
+  @Override
+  public void onCancel() {
+  }
+
+  @Override
+  public void onFail(Throwable t) {
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java
new file mode 100644 (file)
index 0000000..d5ac5bc
--- /dev/null
@@ -0,0 +1,91 @@
+package com.litongjava.whisper.android.java.utils;
+
+import android.content.Context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class AssetUtils {
+  private static Logger log = LoggerFactory.getLogger(AssetUtils.class);
+
+  public static File copyFileIfNotExists(Context context, File distDir, String filename) {
+    File dstFile = new File(distDir, filename);
+    if (dstFile.exists()) {
+      return dstFile;
+    } else {
+      File parentFile = dstFile.getParentFile();
+      log.info("parentFile:{}", parentFile);
+      if (!parentFile.exists()) {
+        parentFile.mkdirs();
+      }
+      AssetUtils.copyFileFromAssets(context, filename, dstFile);
+    }
+    return dstFile;
+  }
+
+  public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) {
+    if (srcDir.isEmpty() || dstDir.isEmpty()) {
+      return;
+    }
+    try {
+      if (!new File(dstDir).exists()) {
+        new File(dstDir).mkdirs();
+      }
+      for (String fileName : appCtx.getAssets().list(srcDir)) {
+        String srcSubPath = srcDir + File.separator + fileName;
+        String dstSubPath = dstDir + File.separator + fileName;
+        if (new File(srcSubPath).isDirectory()) {
+          copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath);
+        } else {
+          copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
+        }
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) {
+    File dstFile = new File(dstPath);
+    copyFileFromAssets(appCtx, srcPath, dstFile);
+  }
+
+  public static void copyFileFromAssets(Context appCtx, String srcPath, File dstFile) {
+    if (srcPath.isEmpty()) {
+      return;
+    }
+    InputStream is = null;
+    OutputStream os = null;
+    try {
+      is = new BufferedInputStream(appCtx.getAssets().open(srcPath));
+
+      os = new BufferedOutputStream(new FileOutputStream(dstFile));
+      byte[] buffer = new byte[1024];
+      int length = 0;
+      while ((length = is.read(buffer)) != -1) {
+        os.write(buffer, 0, length);
+      }
+    } catch (FileNotFoundException e) {
+      e.printStackTrace();
+    } catch (IOException e) {
+      e.printStackTrace();
+    } finally {
+      try {
+        os.close();
+        is.close();
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java
new file mode 100644 (file)
index 0000000..fbe57d4
--- /dev/null
@@ -0,0 +1,105 @@
+package com.litongjava.whisper.android.java.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+
+public class WaveEncoder {
+
+  public static float[] decodeWaveFile(File file) throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (FileInputStream fis = new FileInputStream(file)) {
+      byte[] buffer = new byte[1024];
+      int bytesRead;
+      while ((bytesRead = fis.read(buffer)) != -1) {
+        baos.write(buffer, 0, bytesRead);
+      }
+    }
+    ByteBuffer byteBuffer = ByteBuffer.wrap(baos.toByteArray());
+    byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+    int channel = byteBuffer.getShort(22);
+    byteBuffer.position(44);
+
+    ShortBuffer shortBuffer = byteBuffer.asShortBuffer();
+    short[] shortArray = new short[shortBuffer.limit()];
+    shortBuffer.get(shortArray);
+
+    float[] output = new float[shortArray.length / channel];
+
+    for (int index = 0; index < output.length; index++) {
+      if (channel == 1) {
+        output[index] = Math.max(-1f, Math.min(1f, shortArray[index] / 32767.0f));
+      } else {
+        output[index] = Math.max(-1f, Math.min(1f, (shortArray[2 * index] + shortArray[2 * index + 1]) / 32767.0f / 2.0f));
+      }
+    }
+    return output;
+  }
+
+  public static void encodeWaveFile(File file, short[] data) throws IOException {
+    try (FileOutputStream fos = new FileOutputStream(file)) {
+      fos.write(headerBytes(data.length * 2));
+
+      ByteBuffer buffer = ByteBuffer.allocate(data.length * 2);
+      buffer.order(ByteOrder.LITTLE_ENDIAN);
+      buffer.asShortBuffer().put(data);
+
+      byte[] bytes = new byte[buffer.limit()];
+      buffer.get(bytes);
+
+      fos.write(bytes);
+    }
+  }
+
+  private static byte[] headerBytes(int totalLength) {
+    if (totalLength < 44)
+      throw new IllegalArgumentException("Total length must be at least 44 bytes");
+
+    ByteBuffer buffer = ByteBuffer.allocate(44);
+    buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+    buffer.put((byte) 'R');
+    buffer.put((byte) 'I');
+    buffer.put((byte) 'F');
+    buffer.put((byte) 'F');
+
+    buffer.putInt(totalLength - 8);
+
+    buffer.put((byte) 'W');
+    buffer.put((byte) 'A');
+    buffer.put((byte) 'V');
+    buffer.put((byte) 'E');
+
+    buffer.put((byte) 'f');
+    buffer.put((byte) 'm');
+    buffer.put((byte) 't');
+    buffer.put((byte) ' ');
+
+    buffer.putInt(16);
+    buffer.putShort((short) 1);
+    buffer.putShort((short) 1);
+    buffer.putInt(16000);
+    buffer.putInt(32000);
+    buffer.putShort((short) 2);
+    buffer.putShort((short) 16);
+
+    buffer.put((byte) 'd');
+    buffer.put((byte) 'a');
+    buffer.put((byte) 't');
+    buffer.put((byte) 'a');
+
+    buffer.putInt(totalLength - 44);
+    buffer.position(0);
+
+    byte[] bytes = new byte[buffer.limit()];
+    buffer.get(bytes);
+
+    return bytes;
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java
new file mode 100644 (file)
index 0000000..733ca35
--- /dev/null
@@ -0,0 +1,121 @@
+package com.whispercpp.java.whisper;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CpuInfo {
+  private static final String LOG_TAG = "WhisperCpuConfig";
+
+  private List<String> lines;
+
+  public CpuInfo(List<String> lines) {
+    this.lines = lines;
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  public int getHighPerfCpuCount0() {
+    try {
+      return getHighPerfCpuCountByFrequencies();
+    } catch (Exception e) {
+      Log.d(LOG_TAG, "Couldn't read CPU frequencies", e);
+      return getHighPerfCpuCountByVariant();
+    }
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  private int getHighPerfCpuCountByFrequencies() {
+    List<Integer> frequencies = getCpuValues("processor", line -> {
+        try {
+          return getMaxCpuFrequency(Integer.parseInt(line.trim()));
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+        return 0;
+      }
+    );
+    Log.d(LOG_TAG, "Binned cpu frequencies (frequency, count): " + binnedValues(frequencies));
+    return countDroppingMin(frequencies);
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  private int getHighPerfCpuCountByVariant() {
+    List<Integer> variants = getCpuValues("CPU variant", line -> Integer.parseInt(line.trim().substring(line.indexOf("0x") + 2), 16));
+    Log.d(LOG_TAG, "Binned cpu variants (variant, count): " + binnedValues(variants));
+    return countKeepingMin(variants);
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  private Map<Integer, Integer> binnedValues(List<Integer> values) {
+    Map<Integer, Integer> countMap = new HashMap<>();
+    for (int value : values) {
+      countMap.put(value, countMap.getOrDefault(value, 0) + 1);
+    }
+    return countMap;
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  private List<Integer> getCpuValues(String property, Mapper mapper) {
+    List<Integer> values = new ArrayList<>();
+    for (String line : lines) {
+      if (line.startsWith(property)) {
+        values.add(mapper.map(line.substring(line.indexOf(':') + 1)));
+      }
+    }
+    values.sort(Integer::compareTo);
+    return values;
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  private int countDroppingMin(List<Integer> values) {
+    int min = values.stream().mapToInt(i -> i).min().orElse(Integer.MAX_VALUE);
+    return (int) values.stream().filter(value -> value > min).count();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  private int countKeepingMin(List<Integer> values) {
+    int min = values.stream().mapToInt(i -> i).min().orElse(Integer.MAX_VALUE);
+    return (int) values.stream().filter(value -> value.equals(min)).count();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  public static int getHighPerfCpuCount() {
+    try {
+      return readCpuInfo().getHighPerfCpuCount0();
+    } catch (Exception e) {
+      Log.d(LOG_TAG, "Couldn't read CPU info", e);
+      return Math.max(Runtime.getRuntime().availableProcessors() - 4, 0);
+    }
+  }
+
+  private static CpuInfo readCpuInfo() throws IOException {
+    try (BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo"))) {
+      List<String> lines = new ArrayList<>();
+      String line;
+      while ((line = reader.readLine()) != null) {
+        lines.add(line);
+      }
+      return new CpuInfo(lines);
+    }
+  }
+
+  private static int getMaxCpuFrequency(int cpuIndex) throws IOException {
+    String path = "/sys/devices/system/cpu/cpu" + cpuIndex + "/cpufreq/cpuinfo_max_freq";
+    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
+      return Integer.parseInt(reader.readLine());
+    }
+  }
+
+  private interface Mapper {
+    int map(String line);
+  }
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java
new file mode 100644 (file)
index 0000000..0e52ec1
--- /dev/null
@@ -0,0 +1,138 @@
+package com.whispercpp.java.whisper;
+
+import android.content.res.AssetManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.litongjava.whisper.android.java.bean.WhisperSegment;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class WhisperContext {
+
+  private static final String LOG_TAG = "LibWhisper";
+  private long ptr;
+  private final ExecutorService executorService;
+
+  private WhisperContext(long ptr) {
+    this.ptr = ptr;
+    this.executorService = Executors.newSingleThreadExecutor();
+  }
+
+  public String transcribeData(float[] data) throws ExecutionException, InterruptedException {
+    return executorService.submit(new Callable<String>() {
+      @RequiresApi(api = Build.VERSION_CODES.O)
+      @Override
+      public String call() throws Exception {
+        if (ptr == 0L) {
+          throw new IllegalStateException();
+        }
+        int numThreads = WhisperCpuConfig.getPreferredThreadCount();
+        Log.d(LOG_TAG, "Selecting " + numThreads + " threads");
+
+        StringBuilder result = new StringBuilder();
+        synchronized (this) {
+
+          WhisperLib.fullTranscribe(ptr, numThreads, data);
+          int textCount = WhisperLib.getTextSegmentCount(ptr);
+          for (int i = 0; i < textCount; i++) {
+            String sentence = WhisperLib.getTextSegment(ptr, i);
+            result.append(sentence);
+          }
+        }
+        return result.toString();
+      }
+    }).get();
+  }
+
+  public List<WhisperSegment> transcribeDataWithTime(float[] data) throws ExecutionException, InterruptedException {
+    return executorService.submit(new Callable<List<WhisperSegment>>() {
+      @RequiresApi(api = Build.VERSION_CODES.O)
+      @Override
+      public List<WhisperSegment> call() throws Exception {
+        if (ptr == 0L) {
+          throw new IllegalStateException();
+        }
+        int numThreads = WhisperCpuConfig.getPreferredThreadCount();
+        Log.d(LOG_TAG, "Selecting " + numThreads + " threads");
+
+        List<WhisperSegment> segments = new ArrayList<>();
+        synchronized (this) {
+//          StringBuilder result = new StringBuilder();
+          WhisperLib.fullTranscribe(ptr, numThreads, data);
+          int textCount = WhisperLib.getTextSegmentCount(ptr);
+          for (int i = 0; i < textCount; i++) {
+            long start = WhisperLib.getTextSegmentT0(ptr, i);
+            String sentence = WhisperLib.getTextSegment(ptr, i);
+            long end = WhisperLib.getTextSegmentT1(ptr, i);
+//            result.append();
+            segments.add(new WhisperSegment(start, end, sentence));
+
+          }
+//          return result.toString();
+        }
+        return segments;
+      }
+    }).get();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public String benchMemory(int nthreads) throws ExecutionException, InterruptedException {
+    return executorService.submit(() -> WhisperLib.benchMemcpy(nthreads)).get();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public String benchGgmlMulMat(int nthreads) throws ExecutionException, InterruptedException {
+    return executorService.submit(() -> WhisperLib.benchGgmlMulMat(nthreads)).get();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public void release() throws ExecutionException, InterruptedException {
+    executorService.submit(() -> {
+      if (ptr != 0L) {
+        WhisperLib.freeContext(ptr);
+        ptr = 0;
+      }
+    }).get();
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public static WhisperContext createContextFromFile(String filePath) {
+    long ptr = WhisperLib.initContext(filePath);
+    if (ptr == 0L) {
+      throw new RuntimeException("Couldn't create context with path " + filePath);
+    }
+    return new WhisperContext(ptr);
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public static WhisperContext createContextFromInputStream(InputStream stream) {
+    long ptr = WhisperLib.initContextFromInputStream(stream);
+    if (ptr == 0L) {
+      throw new RuntimeException("Couldn't create context from input stream");
+    }
+    return new WhisperContext(ptr);
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public static WhisperContext createContextFromAsset(AssetManager assetManager, String assetPath) {
+    long ptr = WhisperLib.initContextFromAsset(assetManager, assetPath);
+    if (ptr == 0L) {
+      throw new RuntimeException("Couldn't create context from asset " + assetPath);
+    }
+    return new WhisperContext(ptr);
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public static String getSystemInfo() {
+    return WhisperLib.getSystemInfo();
+  }
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java
new file mode 100644 (file)
index 0000000..8cd2b88
--- /dev/null
@@ -0,0 +1,12 @@
+package com.whispercpp.java.whisper;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+public class WhisperCpuConfig {
+  @RequiresApi(api = Build.VERSION_CODES.N)
+  public static int getPreferredThreadCount() {
+    return Math.max(CpuInfo.getHighPerfCpuCount(), 2);
+  }
+}
diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java
new file mode 100644 (file)
index 0000000..38dd47a
--- /dev/null
@@ -0,0 +1,75 @@
+package com.whispercpp.java.whisper;
+
+import android.content.res.AssetManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.InputStream;
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+public class WhisperLib {
+  private static final String LOG_TAG = "LibWhisper";
+
+  static {
+
+    Log.d(LOG_TAG, "Primary ABI: " + Build.SUPPORTED_ABIS[0]);
+    boolean loadVfpv4 = false;
+    boolean loadV8fp16 = false;
+    if (WhisperUtils.isArmEabiV7a()) {
+      String cpuInfo = WhisperUtils.cpuInfo();
+      if (cpuInfo != null) {
+        Log.d(LOG_TAG, "CPU info: " + cpuInfo);
+        if (cpuInfo.contains("vfpv4")) {
+          Log.d(LOG_TAG, "CPU supports vfpv4");
+          loadVfpv4 = true;
+        }
+      }
+    } else if (WhisperUtils.isArmEabiV8a()) {
+      String cpuInfo = WhisperUtils.cpuInfo();
+      if (cpuInfo != null) {
+        Log.d(LOG_TAG, "CPU info: " + cpuInfo);
+        if (cpuInfo.contains("fphp")) {
+          Log.d(LOG_TAG, "CPU supports fp16 arithmetic");
+          loadV8fp16 = true;
+        }
+      }
+    }
+
+    if (loadVfpv4) {
+      Log.d(LOG_TAG, "Loading libwhisper_vfpv4.so");
+      System.loadLibrary("whisper_vfpv4");
+    } else if (loadV8fp16) {
+      Log.d(LOG_TAG, "Loading libwhisper_v8fp16_va.so");
+      System.loadLibrary("whisper_v8fp16_va");
+    } else {
+      Log.d(LOG_TAG, "Loading libwhisper.so");
+      System.loadLibrary("whisper");
+    }
+  }
+
+  public static native long initContextFromInputStream(InputStream inputStream);
+
+  public static native long initContextFromAsset(AssetManager assetManager, String assetPath);
+
+  public static native long initContext(String modelPath);
+
+  public static native void freeContext(long contextPtr);
+
+  public static native void fullTranscribe(long contextPtr, int numThreads, float[] audioData);
+
+  public static native int getTextSegmentCount(long contextPtr);
+
+  public static native String getTextSegment(long contextPtr, int index);
+
+  public static native long getTextSegmentT0(long contextPtr, int index);
+
+  public static native long getTextSegmentT1(long contextPtr, int index);
+
+  public static native String getSystemInfo();
+
+  public static native String benchMemcpy(int nthread);
+
+  public static native String benchGgmlMulMat(int nthread);
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java
new file mode 100644 (file)
index 0000000..8e803b7
--- /dev/null
@@ -0,0 +1,34 @@
+package com.whispercpp.java.whisper;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.File;
+import java.nio.file.Path;
+
+public class WhisperUtils {
+  private static final String LOG_TAG = "LibWhisper";
+
+
+  public static boolean isArmEabiV7a() {
+    return Build.SUPPORTED_ABIS[0].equals("armeabi-v7a");
+  }
+
+  public static boolean isArmEabiV8a() {
+    return Build.SUPPORTED_ABIS[0].equals("arm64-v8a");
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.O)
+  public static String cpuInfo() {
+    try {
+      Path path = new File("/proc/cpuinfo").toPath();
+      return new String(java.nio.file.Files.readAllBytes(path));
+    } catch (Exception e) {
+      Log.w(LOG_TAG, "Couldn't read /proc/cpuinfo", e);
+      return null;
+    }
+
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt b/examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt
new file mode 100644 (file)
index 0000000..668cd4a
--- /dev/null
@@ -0,0 +1,56 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(whisper.cpp)
+
+set(CMAKE_CXX_STANDARD 11)
+set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../../../../)
+
+set(
+        SOURCE_FILES
+        ${WHISPER_LIB_DIR}/ggml.c
+        ${WHISPER_LIB_DIR}/ggml-alloc.c
+        ${WHISPER_LIB_DIR}/ggml-backend.c
+        ${WHISPER_LIB_DIR}/ggml-quants.c
+        ${WHISPER_LIB_DIR}/whisper.cpp
+        ${CMAKE_SOURCE_DIR}/jni.c
+)
+
+find_library(LOG_LIB log)
+
+function(build_library target_name)
+    add_library(
+        ${target_name}
+        SHARED
+        ${SOURCE_FILES}
+    )
+
+    target_link_libraries(${target_name} ${LOG_LIB} android)
+
+    if (${target_name} STREQUAL "whisper_v8fp16_va")
+        target_compile_options(${target_name} PRIVATE -march=armv8.2-a+fp16)
+    elseif (${target_name} STREQUAL "whisper_vfpv4")
+        target_compile_options(${target_name} PRIVATE -mfpu=neon-vfpv4)
+    endif ()
+
+    if (NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug")
+
+        target_compile_options(${target_name} PRIVATE -O3)
+        target_compile_options(${target_name} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden)
+        target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections)
+
+        #target_link_options(${target_name} PRIVATE -Wl,--gc-sections)
+        #target_link_options(${target_name} PRIVATE -Wl,--exclude-libs,ALL)
+        #target_link_options(${target_name} PRIVATE -flto)
+
+    endif ()
+endfunction()
+
+build_library("whisper") # Default target
+
+if (${ANDROID_ABI} STREQUAL "arm64-v8a")
+    build_library("whisper_v8fp16_va")
+elseif (${ANDROID_ABI} STREQUAL "armeabi-v7a")
+    build_library("whisper_vfpv4")
+endif ()
+
+include_directories(${WHISPER_LIB_DIR})
diff --git a/examples/whisper.android.java/app/src/main/jni/whisper/jni.c b/examples/whisper.android.java/app/src/main/jni/whisper/jni.c
new file mode 100644 (file)
index 0000000..f8e7eff
--- /dev/null
@@ -0,0 +1,257 @@
+#include <jni.h>
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/log.h>
+#include <stdlib.h>
+#include <sys/sysinfo.h>
+#include <string.h>
+#include "whisper.h"
+#include "ggml.h"
+
+#define UNUSED(x) (void)(x)
+#define TAG "JNI"
+
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,     TAG, __VA_ARGS__)
+#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,     TAG, __VA_ARGS__)
+
+static inline int min(int a, int b) {
+    return (a < b) ? a : b;
+}
+
+static inline int max(int a, int b) {
+    return (a > b) ? a : b;
+}
+
+struct input_stream_context {
+    size_t offset;
+    JNIEnv * env;
+    jobject thiz;
+    jobject input_stream;
+
+    jmethodID mid_available;
+    jmethodID mid_read;
+};
+
+size_t inputStreamRead(void * ctx, void * output, size_t read_size) {
+    struct input_stream_context* is = (struct input_stream_context*)ctx;
+
+    jint avail_size = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available);
+    jint size_to_copy = read_size < avail_size ? (jint)read_size : avail_size;
+
+    jbyteArray byte_array = (*is->env)->NewByteArray(is->env, size_to_copy);
+
+    jint n_read = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_read, byte_array, 0, size_to_copy);
+
+    if (size_to_copy != read_size || size_to_copy != n_read) {
+        LOGI("Insufficient Read: Req=%zu, ToCopy=%d, Available=%d", read_size, size_to_copy, n_read);
+    }
+
+    jbyte* byte_array_elements = (*is->env)->GetByteArrayElements(is->env, byte_array, NULL);
+    memcpy(output, byte_array_elements, size_to_copy);
+    (*is->env)->ReleaseByteArrayElements(is->env, byte_array, byte_array_elements, JNI_ABORT);
+
+    (*is->env)->DeleteLocalRef(is->env, byte_array);
+
+    is->offset += size_to_copy;
+
+    return size_to_copy;
+}
+bool inputStreamEof(void * ctx) {
+    struct input_stream_context* is = (struct input_stream_context*)ctx;
+
+    jint result = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available);
+    return result <= 0;
+}
+void inputStreamClose(void * ctx) {
+
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_initContextFromInputStream(
+        JNIEnv *env, jobject thiz, jobject input_stream) {
+    UNUSED(thiz);
+
+    struct whisper_context *context = NULL;
+    struct whisper_model_loader loader = {};
+    struct input_stream_context inp_ctx = {};
+
+    inp_ctx.offset = 0;
+    inp_ctx.env = env;
+    inp_ctx.thiz = thiz;
+    inp_ctx.input_stream = input_stream;
+
+    jclass cls = (*env)->GetObjectClass(env, input_stream);
+    inp_ctx.mid_available = (*env)->GetMethodID(env, cls, "available", "()I");
+    inp_ctx.mid_read = (*env)->GetMethodID(env, cls, "read", "([BII)I");
+
+    loader.context = &inp_ctx;
+    loader.read = inputStreamRead;
+    loader.eof = inputStreamEof;
+    loader.close = inputStreamClose;
+
+    loader.eof(loader.context);
+
+    context = whisper_init(&loader);
+    return (jlong) context;
+}
+
+static size_t asset_read(void *ctx, void *output, size_t read_size) {
+    return AAsset_read((AAsset *) ctx, output, read_size);
+}
+
+static bool asset_is_eof(void *ctx) {
+    return AAsset_getRemainingLength64((AAsset *) ctx) <= 0;
+}
+
+static void asset_close(void *ctx) {
+    AAsset_close((AAsset *) ctx);
+}
+
+static struct whisper_context *whisper_init_from_asset(
+        JNIEnv *env,
+        jobject assetManager,
+        const char *asset_path
+) {
+    LOGI("Loading model from asset '%s'\n", asset_path);
+    AAssetManager *asset_manager = AAssetManager_fromJava(env, assetManager);
+    AAsset *asset = AAssetManager_open(asset_manager, asset_path, AASSET_MODE_STREAMING);
+    if (!asset) {
+        LOGW("Failed to open '%s'\n", asset_path);
+        return NULL;
+    }
+
+    whisper_model_loader loader = {
+            .context = asset,
+            .read = &asset_read,
+            .eof = &asset_is_eof,
+            .close = &asset_close
+    };
+
+    return whisper_init(&loader);
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_initContextFromAsset(
+        JNIEnv *env, jobject thiz, jobject assetManager, jstring asset_path_str) {
+    UNUSED(thiz);
+    struct whisper_context *context = NULL;
+    const char *asset_path_chars = (*env)->GetStringUTFChars(env, asset_path_str, NULL);
+    context = whisper_init_from_asset(env, assetManager, asset_path_chars);
+    (*env)->ReleaseStringUTFChars(env, asset_path_str, asset_path_chars);
+    return (jlong) context;
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_initContext(
+        JNIEnv *env, jobject thiz, jstring model_path_str) {
+    UNUSED(thiz);
+    struct whisper_context *context = NULL;
+    const char *model_path_chars = (*env)->GetStringUTFChars(env, model_path_str, NULL);
+    context = whisper_init_from_file(model_path_chars);
+    (*env)->ReleaseStringUTFChars(env, model_path_str, model_path_chars);
+    return (jlong) context;
+}
+
+JNIEXPORT void JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_freeContext(
+        JNIEnv *env, jobject thiz, jlong context_ptr) {
+    UNUSED(env);
+    UNUSED(thiz);
+    struct whisper_context *context = (struct whisper_context *) context_ptr;
+    whisper_free(context);
+}
+
+JNIEXPORT void JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_fullTranscribe(
+        JNIEnv *env, jobject thiz, jlong context_ptr, jint num_threads, jfloatArray audio_data) {
+    UNUSED(thiz);
+    struct whisper_context *context = (struct whisper_context *) context_ptr;
+    jfloat *audio_data_arr = (*env)->GetFloatArrayElements(env, audio_data, NULL);
+    const jsize audio_data_length = (*env)->GetArrayLength(env, audio_data);
+
+    // The below adapted from the Objective-C iOS sample
+    struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
+    params.print_realtime = true;
+    params.print_progress = false;
+    params.print_timestamps = true;
+    params.print_special = false;
+    params.translate = false;
+    params.language = "en";
+    params.n_threads = num_threads;
+    params.offset_ms = 0;
+    params.no_context = true;
+    params.single_segment = false;
+
+    whisper_reset_timings(context);
+
+    LOGI("About to run whisper_full");
+    if (whisper_full(context, params, audio_data_arr, audio_data_length) != 0) {
+        LOGI("Failed to run the model");
+    } else {
+        whisper_print_timings(context);
+    }
+    (*env)->ReleaseFloatArrayElements(env, audio_data, audio_data_arr, JNI_ABORT);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentCount(
+        JNIEnv *env, jobject thiz, jlong context_ptr) {
+    UNUSED(env);
+    UNUSED(thiz);
+    struct whisper_context *context = (struct whisper_context *) context_ptr;
+    return whisper_full_n_segments(context);
+}
+
+
+JNIEXPORT jstring JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getTextSegment(
+        JNIEnv *env, jobject thiz, jlong context_ptr, jint index) {
+    UNUSED(thiz);
+    struct whisper_context *context = (struct whisper_context *) context_ptr;
+    const char *text = whisper_full_get_segment_text(context, index);
+    jstring string = (*env)->NewStringUTF(env, text);
+    return string;
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentT0(JNIEnv *env, jobject thiz,jlong context_ptr, jint index) {
+    UNUSED(thiz);
+    struct whisper_context *context = (struct whisper_context *) context_ptr;
+    const int64_t t0 = whisper_full_get_segment_t0(context, index);
+    return (jlong)t0;
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentT1(JNIEnv *env, jobject thiz,jlong context_ptr, jint index) {
+    UNUSED(thiz);
+    struct whisper_context *context = (struct whisper_context *) context_ptr;
+    const int64_t t1 = whisper_full_get_segment_t1(context, index);
+    return (jlong)t1;
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getSystemInfo(
+        JNIEnv *env, jobject thiz
+) {
+    UNUSED(thiz);
+    const char *sysinfo = whisper_print_system_info();
+    jstring string = (*env)->NewStringUTF(env, sysinfo);
+    return string;
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_benchMemcpy(JNIEnv *env, jobject thiz,
+                                                                      jint n_threads) {
+    UNUSED(thiz);
+    const char *bench_ggml_memcpy = whisper_bench_memcpy_str(n_threads);
+    jstring string = (*env)->NewStringUTF(env, bench_ggml_memcpy);
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_benchGgmlMulMat(JNIEnv *env, jobject thiz,
+                                                                          jint n_threads) {
+    UNUSED(thiz);
+    const char *bench_ggml_mul_mat = whisper_bench_ggml_mul_mat_str(n_threads);
+    jstring string = (*env)->NewStringUTF(env, bench_ggml_mul_mat);
+}
+
diff --git a/examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644 (file)
index 0000000..5c3bfcd
--- /dev/null
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:aapt="http://schemas.android.com/aapt"
+  android:width="108dp"
+  android:height="108dp"
+  android:viewportWidth="108"
+  android:viewportHeight="108">
+  <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+    <aapt:attr name="android:fillColor">
+      <gradient
+        android:endX="85.84757"
+        android:endY="92.4963"
+        android:startX="42.9492"
+        android:startY="49.59793"
+        android:type="linear">
+        <item
+          android:color="#44000000"
+          android:offset="0.0" />
+        <item
+          android:color="#00000000"
+          android:offset="1.0" />
+      </gradient>
+    </aapt:attr>
+  </path>
+  <path
+    android:fillColor="#FFFFFF"
+    android:fillType="nonZero"
+    android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+    android:strokeWidth="1"
+    android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml b/examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644 (file)
index 0000000..140f829
--- /dev/null
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="108dp"
+  android:height="108dp"
+  android:viewportWidth="108"
+  android:viewportHeight="108">
+  <path
+    android:fillColor="#3DDC84"
+    android:pathData="M0,0h108v108h-108z" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M9,0L9,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,0L19,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M29,0L29,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M39,0L39,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M49,0L49,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M59,0L59,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M69,0L69,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M79,0L79,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M89,0L89,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M99,0L99,108"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,9L108,9"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,19L108,19"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,29L108,29"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,39L108,39"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,49L108,49"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,59L108,59"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,69L108,69"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,79L108,79"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,89L108,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M0,99L108,99"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,29L89,29"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,39L89,39"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,49L89,49"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,59L89,59"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,69L89,69"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M19,79L89,79"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M29,19L29,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M39,19L39,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M49,19L49,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M59,19L59,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M69,19L69,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+  <path
+    android:fillColor="#00000000"
+    android:pathData="M79,19L79,89"
+    android:strokeWidth="0.8"
+    android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/examples/whisper.android.java/app/src/main/res/layout/activity_main.xml b/examples/whisper.android.java/app/src/main/res/layout/activity_main.xml
new file mode 100644 (file)
index 0000000..f78b4ce
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  tools:context=".MainActivity">
+
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Button
+      android:id="@+id/systemInfoBtn"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="System Info" />
+
+    <Button
+      android:id="@+id/loadModelBtn"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="Load model" />
+
+  </LinearLayout>
+
+  <LinearLayout
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <Button
+      android:id="@+id/transcriptSampleBtn"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="Transcribe sample" />
+
+    <Button
+      android:id="@+id/clearBtn"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="Clear" />
+  </LinearLayout>
+
+  <TextView
+    android:id="@+id/sample_text"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:text="Hello World!"
+    app:layout_constraintBottom_toBottomOf="parent"
+    app:layout_constraintLeft_toLeftOf="parent"
+    app:layout_constraintRight_toRightOf="parent"
+    app:layout_constraintTop_toTopOf="parent"
+    android:scrollbarAlwaysDrawHorizontalTrack="true"
+    android:maxLines="999"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644 (file)
index 0000000..03eed25
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background" />
+  <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644 (file)
index 0000000..03eed25
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background" />
+  <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..a571e60
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644 (file)
index 0000000..61da551
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..c41dd28
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644 (file)
index 0000000..db5080a
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..6dba46d
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644 (file)
index 0000000..da31a87
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..15ac681
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644 (file)
index 0000000..b216f2d
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..f25a419
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644 (file)
index 0000000..e96783c
Binary files /dev/null and b/examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/examples/whisper.android.java/app/src/main/res/values-night/themes.xml b/examples/whisper.android.java/app/src/main/res/values-night/themes.xml
new file mode 100644 (file)
index 0000000..f713808
--- /dev/null
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+  <!-- Base application theme. -->
+  <style name="Theme.Whisperandroidjava" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+    <!-- Primary brand color. -->
+    <item name="colorPrimary">@color/purple_200</item>
+    <item name="colorPrimaryVariant">@color/purple_700</item>
+    <item name="colorOnPrimary">@color/black</item>
+    <!-- Secondary brand color. -->
+    <item name="colorSecondary">@color/teal_200</item>
+    <item name="colorSecondaryVariant">@color/teal_200</item>
+    <item name="colorOnSecondary">@color/black</item>
+    <!-- Status bar color. -->
+    <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+    <!-- Customize your theme here. -->
+  </style>
+</resources>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/values/colors.xml b/examples/whisper.android.java/app/src/main/res/values/colors.xml
new file mode 100644 (file)
index 0000000..09837df
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="purple_200">#FFBB86FC</color>
+  <color name="purple_500">#FF6200EE</color>
+  <color name="purple_700">#FF3700B3</color>
+  <color name="teal_200">#FF03DAC5</color>
+  <color name="teal_700">#FF018786</color>
+  <color name="black">#FF000000</color>
+  <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/values/strings.xml b/examples/whisper.android.java/app/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..1747e4e
--- /dev/null
@@ -0,0 +1,3 @@
+<resources>
+  <string name="app_name">whisper.android.java</string>
+</resources>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/main/res/values/themes.xml b/examples/whisper.android.java/app/src/main/res/values/themes.xml
new file mode 100644 (file)
index 0000000..482c9fc
--- /dev/null
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+  <!-- Base application theme. -->
+  <style name="Theme.Whisperandroidjava" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+    <!-- Primary brand color. -->
+    <item name="colorPrimary">@color/purple_500</item>
+    <item name="colorPrimaryVariant">@color/purple_700</item>
+    <item name="colorOnPrimary">@color/white</item>
+    <!-- Secondary brand color. -->
+    <item name="colorSecondary">@color/teal_200</item>
+    <item name="colorSecondaryVariant">@color/teal_700</item>
+    <item name="colorOnSecondary">@color/black</item>
+    <!-- Status bar color. -->
+    <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+    <!-- Customize your theme here. -->
+  </style>
+</resources>
\ No newline at end of file
diff --git a/examples/whisper.android.java/app/src/test/java/com/litongjava/whisper/android/java/ExampleUnitTest.java b/examples/whisper.android.java/app/src/test/java/com/litongjava/whisper/android/java/ExampleUnitTest.java
new file mode 100644 (file)
index 0000000..c864c62
--- /dev/null
@@ -0,0 +1,17 @@
+package com.litongjava.whisper.android.java;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+  @Test
+  public void addition_isCorrect() {
+    assertEquals(4, 2 + 2);
+  }
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/build.gradle b/examples/whisper.android.java/build.gradle
new file mode 100644 (file)
index 0000000..450441a
--- /dev/null
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+  repositories {
+    google()
+    jcenter()
+  }
+  dependencies {
+    classpath "com.android.tools.build:gradle:4.1.3"
+
+    // NOTE: Do not place your application dependencies here; they belong
+    // in the individual module build.gradle files
+  }
+}
+
+allprojects {
+  repositories {
+    google()
+    jcenter()
+  }
+}
+
+task clean(type: Delete) {
+  delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/examples/whisper.android.java/gradle.properties b/examples/whisper.android.java/gradle.properties
new file mode 100644 (file)
index 0000000..52f5917
--- /dev/null
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/examples/whisper.android.java/gradle/wrapper/gradle-wrapper.jar b/examples/whisper.android.java/gradle/wrapper/gradle-wrapper.jar
new file mode 100644 (file)
index 0000000..f6b961f
Binary files /dev/null and b/examples/whisper.android.java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/whisper.android.java/gradle/wrapper/gradle-wrapper.properties b/examples/whisper.android.java/gradle/wrapper/gradle-wrapper.properties
new file mode 100644 (file)
index 0000000..9f4db5d
--- /dev/null
@@ -0,0 +1,6 @@
+#Fri Oct 20 11:07:15 HST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
diff --git a/examples/whisper.android.java/gradlew b/examples/whisper.android.java/gradlew
new file mode 100644 (file)
index 0000000..cccdd3d
--- /dev/null
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/examples/whisper.android.java/gradlew.bat b/examples/whisper.android.java/gradlew.bat
new file mode 100644 (file)
index 0000000..f955316
--- /dev/null
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/whisper.android.java/settings.gradle b/examples/whisper.android.java/settings.gradle
new file mode 100644 (file)
index 0000000..87e0161
--- /dev/null
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "whisper.android.java"
\ No newline at end of file
index 0897082f7512e48e89310db81b5455d997417505..4a09ccc1215d7bbfea0a0bb98d3ebe1014ae6bae 100644 (file)
@@ -4,6 +4,7 @@
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
+        <option name="testRunner" value="GRADLE" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
         <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
         <option name="modules">
@@ -13,6 +14,7 @@
           </set>
         </option>
         <option name="resolveExternalAnnotations" value="false" />
+        <option name="resolveModulePerSourceSet" value="false" />
       </GradleProjectSettings>
     </option>
   </component>