夜雨飘零
2023-10-16 10efb8bcefbebf15721551f2e074c050c2287967
添加Android应用 (#1008)

* Add Android WebSocket Demo

* Add Android WebSocket Demo

* update docs
36个文件已添加
1273 ■■■■■ 已修改文件
funasr/runtime/android/AndroidClient/app/build.gradle 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/proguard-rules.pro 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/androidTest/java/com/yeyupiaoling/androidclient/ExampleInstrumentedTest.kt 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/AndroidManifest.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/AudioView.java 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/MainActivity.java 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/ic_launcher_background.xml 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/ic_launcher_foreground.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/layout/activity_main.xml 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-hdpi/ic_launcher.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-mdpi/ic_launcher.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xhdpi/ic_launcher.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/values/colors.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/values/strings.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/values/themes.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/xml/backup_rules.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/xml/data_extraction_rules.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/test/java/com/yeyupiaoling/androidclient/ExampleUnitTest.kt 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/build.gradle 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/gradle.properties 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/gradle/wrapper/gradle-wrapper.jar 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/gradle/wrapper/gradle-wrapper.properties 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/gradlew 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/gradlew.bat 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/settings.gradle 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/images/demo.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/readme.md 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/build.gradle
New file
@@ -0,0 +1,71 @@
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}
android {
    namespace 'com.yeyupiaoling.androidclient'
    compileSdk 33
    defaultConfig {
        applicationId "com.yeyupiaoling.androidclient"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.4.3'
    }
    packaging {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}
dependencies {
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
    implementation 'androidx.activity:activity-compose:1.7.0'
    implementation platform('androidx.compose:compose-bom:2023.03.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    debugImplementation 'androidx.compose.ui:ui-test-manifest'
    implementation 'com.squareup.okhttp3:okhttp:4.9.1'
}
funasr/runtime/android/AndroidClient/app/proguard-rules.pro
New file
@@ -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
funasr/runtime/android/AndroidClient/app/src/androidTest/java/com/yeyupiaoling/androidclient/ExampleInstrumentedTest.kt
New file
@@ -0,0 +1,24 @@
package com.yeyupiaoling.androidclient
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.yeyupiaoling.androidclient", appContext.packageName)
    }
}
funasr/runtime/android/AndroidClient/app/src/main/AndroidManifest.xml
New file
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidClient"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/AudioView.java
New file
@@ -0,0 +1,216 @@
package com.yeyupiaoling.androidclient;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class AudioView extends View {
    // 频谱数量
    private static final int LUMP_COUNT = 128;
    private static final int LUMP_WIDTH = 6;
    private static final int LUMP_SPACE = 2;
    private static final int LUMP_MIN_HEIGHT = LUMP_WIDTH;
    private static final int LUMP_MAX_HEIGHT = 200;//TODO: HEIGHT
    private static final int LUMP_SIZE = LUMP_WIDTH + LUMP_SPACE;
    private static final int LUMP_COLOR = Color.parseColor("#6de8fd");
    private static final int WAVE_SAMPLING_INTERVAL = 3;
    private static final float SCALE = LUMP_MAX_HEIGHT / LUMP_COUNT;
    private ShowStyle upShowStyle = ShowStyle.STYLE_HOLLOW_LUMP;
    private ShowStyle downShowStyle = ShowStyle.STYLE_WAVE;
    private byte[] waveData;
    List<Point> pointList;
    private Paint lumpPaint;
    Path wavePath = new Path();
    public AudioView(Context context) {
        super(context);
        init();
    }
    public AudioView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public AudioView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        lumpPaint = new Paint();
        lumpPaint.setAntiAlias(true);
        lumpPaint.setColor(LUMP_COLOR);
        lumpPaint.setStrokeWidth(2);
        lumpPaint.setStyle(Paint.Style.STROKE);
    }
    public void setWaveData(byte[] data) {
        this.waveData = readyData(data);
        genSamplingPoint(data);
        invalidate();
    }
    public void setStyle(ShowStyle upShowStyle, ShowStyle downShowStyle) {
        this.upShowStyle = upShowStyle;
        this.downShowStyle = downShowStyle;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        wavePath.reset();
        for (int i = 0; i < LUMP_COUNT; i++) {
            if (waveData == null) {
                canvas.drawRect((LUMP_WIDTH + LUMP_SPACE) * i,
                        LUMP_MAX_HEIGHT - LUMP_MIN_HEIGHT,
                        (LUMP_WIDTH + LUMP_SPACE) * i + LUMP_WIDTH,
                        LUMP_MAX_HEIGHT,
                        lumpPaint);
                continue;
            }
            switch (upShowStyle) {
                case STYLE_HOLLOW_LUMP:
                    drawLump(canvas, i, false);
                    break;
                case STYLE_WAVE:
                    drawWave(canvas, i, false);
                    break;
                default:
                    break;
            }
            switch (downShowStyle) {
                case STYLE_HOLLOW_LUMP:
                    drawLump(canvas, i, true);
                    break;
                case STYLE_WAVE:
                    drawWave(canvas, i, true);
                    break;
                default:
                    break;
            }
        }
    }
    /**
     * 预处理数据
     *
     * @return
     */
    private static byte[] readyData(byte[] fft) {
        byte[] newData = new byte[LUMP_COUNT];
        byte abs;
        for (int i = 0; i < LUMP_COUNT; i++) {
            abs = (byte) Math.abs(fft[i]);
            //描述:Math.abs -128时越界
            newData[i] = abs < 0 ? 127 : abs;
        }
        return newData;
    }
    /**
     * 绘制曲线
     *
     * @param canvas
     * @param i
     * @param reversal
     */
    private void drawWave(Canvas canvas, int i, boolean reversal) {
        if (pointList == null || pointList.size() < 2) {
            return;
        }
        float ratio = SCALE * (reversal ? -1 : 1);
        if (i < pointList.size() - 2) {
            Point point = pointList.get(i);
            Point nextPoint = pointList.get(i + 1);
            int midX = (point.x + nextPoint.x) >> 1;
            if (i == 0) {
                wavePath.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio);
            }
            wavePath.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio,
                    midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio,
                    nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio);
            canvas.drawPath(wavePath, lumpPaint);
        }
    }
    /**
     * 绘制矩形条
     */
    private void drawLump(Canvas canvas, int i, boolean reversal) {
        int minus = reversal ? -1 : 1;
        float top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] * SCALE) * minus);
        canvas.drawRect(LUMP_SIZE * i,
                top,
                LUMP_SIZE * i + LUMP_WIDTH,
                LUMP_MAX_HEIGHT,
                lumpPaint);
    }
    /**
     * 生成波形图的采样数据,减少计算量
     *
     * @param data
     */
    private void genSamplingPoint(byte[] data) {
        if (upShowStyle != ShowStyle.STYLE_WAVE && downShowStyle != ShowStyle.STYLE_WAVE) {
            return;
        }
        if (pointList == null) {
            pointList = new ArrayList<>();
        } else {
            pointList.clear();
        }
        pointList.add(new Point(0, 0));
        for (int i = WAVE_SAMPLING_INTERVAL; i < LUMP_COUNT; i += WAVE_SAMPLING_INTERVAL) {
            pointList.add(new Point(LUMP_SIZE * i, waveData[i]));
        }
        pointList.add(new Point(LUMP_SIZE * LUMP_COUNT, 0));
    }
    /**
     * 可视化样式
     */
    public enum ShowStyle {
        /**
         * 空心的矩形小块
         */
        STYLE_HOLLOW_LUMP,
        /**
         * 曲线
         */
        STYLE_WAVE,
        /**
         * 不显示
         */
        STYLE_NOTHING
    }
}
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/MainActivity.java
New file
@@ -0,0 +1,248 @@
package com.yeyupiaoling.androidclient;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import javax.net.ssl.HostnameVerifier;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class MainActivity extends AppCompatActivity {
    public static final String TAG = MainActivity.class.getSimpleName();
    // WebSocket地址,如果服务端没有使用SSL,请使用ws://
    public static final String ASR_HOST = "wss://192.168.0.1:10095";
    // 采样率
    public static final int SAMPLE_RATE = 16000;
    // 声道数
    public static final int CHANNEL = AudioFormat.CHANNEL_IN_MONO;
    // 返回的音频数据的格式
    public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    private AudioRecord audioRecord;
    private boolean isRecording = false;
    private int minBufferSize;
    private AudioView audioView;
    private String allAsrText = "";
    private String asrText = "";
    // 控件
    private Button recordBtn;
    private TextView resultText;
    private WebSocket webSocket;
    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 请求权限
        if (!hasPermission()) {
            requestPermission();
        }
        // 录音参数
        minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, AUDIO_FORMAT);
        // 显示识别结果控件
        resultText = findViewById(R.id.result_text);
        // 显示录音状态控件
        audioView = findViewById(R.id.audioView);
        audioView.setStyle(AudioView.ShowStyle.STYLE_HOLLOW_LUMP, AudioView.ShowStyle.STYLE_NOTHING);
        // 按下识别按钮
        recordBtn = findViewById(R.id.record_button);
        recordBtn.setOnTouchListener((v, event) -> {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                isRecording = false;
                stopRecording();
                recordBtn.setText("按下录音");
            } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
                if (webSocket != null){
                    webSocket.cancel();
                    webSocket = null;
                }
                allAsrText = "";
                asrText = "";
                isRecording = true;
                startRecording();
                recordBtn.setText("录音中...");
            }
            return true;
        });
    }
    // 开始录音
    private void startRecording() {
        // 准备录音器
        try {
            // 确保有权限
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
                requestPermission();
                return;
            }
            // 创建录音器
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL, AUDIO_FORMAT, minBufferSize);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }
        // 开启一个线程将录音数据写入文件
        Thread recordingAudioThread = new Thread(() -> {
            try {
                setAudioData();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        recordingAudioThread.start();
        // 启动录音器
        audioRecord.startRecording();
        audioView.setVisibility(View.VISIBLE);
    }
    // 停止录音器
    private void stopRecording() {
        audioRecord.stop();
        audioRecord.release();
        audioRecord = null;
        audioView.setVisibility(View.GONE);
    }
    // 读取录音数据
    private void setAudioData() throws Exception {
        // 如果使用正常的wss,可以去掉这个
        HostnameVerifier hostnameVerifier = (hostname, session) -> {
            // 总是返回true,表示不验证域名
            return true;
        };
        // 建立WebSocket连接
        OkHttpClient client = new OkHttpClient.Builder()
                .hostnameVerifier(hostnameVerifier)
                .build();
        Request request = new Request.Builder()
                .url(ASR_HOST)
                .build();
        webSocket = client.newWebSocket(request, new WebSocketListener() {
            @Override
            public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
                // 连接成功时的处理
                Log.d(TAG, "WebSocket连接成功");
            }
            @Override
            public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
                // 接收到消息时的处理
                Log.d(TAG, "WebSocket接收到消息: " + text);
                try {
                    JSONObject jsonObject = new JSONObject(text);
                    String t = jsonObject.getString("text");
                    boolean isFinal = jsonObject.getBoolean("is_final");
                    if (!t.equals("")) {
                        // 拼接识别结果
                        String mode = jsonObject.getString("mode");
                        if (mode.equals("2pass-offline")) {
                            asrText = "";
                            allAsrText = allAsrText + t;
                            // 这里可以做一些自动停止录音识别的程序
                        } else {
                            asrText = asrText + t;
                        }
                    }
                    // 显示语音识别结果消息
                    if (!(allAsrText + asrText).equals("")) {
                        runOnUiThread(() -> resultText.setText(allAsrText + asrText));
                    }
                    // 如果检测的录音停止就关闭WebSocket连接
                    if (isFinal) {
                        webSocket.close(1000, "关闭WebSocket连接");
                    }
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                }
            }
            @Override
            public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
                // 关闭连接时的处理
                Log.d(TAG, "WebSocket关闭连接: " + reason);
            }
            @Override
            public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, Response response) {
                // 连接失败时的处理
                Log.d(TAG, "WebSocket连接失败: " + t + ": " + response);
            }
        });
        String message = getMessage("2pass", "5, 10, 5", 10, true);
        webSocket.send(message);
        audioRecord.startRecording();
        byte[] bytes = new byte[minBufferSize];
        while (isRecording) {
            int readSize = audioRecord.read(bytes, 0, minBufferSize);
            if (readSize > 0) {
                ByteString byteString = ByteString.of(bytes);
                webSocket.send(byteString);
                audioView.post(() -> audioView.setWaveData(bytes));
            }
        }
        JSONObject obj = new JSONObject();
        obj.put("is_speaking", false);
        webSocket.send(obj.toString());
        // webSocket.close(1000, "关闭WebSocket连接");
    }
    // 发送第一步的JSON数据
    public String getMessage(String mode, String strChunkSize, int chunkInterval, boolean isSpeaking) {
        try {
            JSONObject obj = new JSONObject();
            obj.put("mode", mode);
            JSONArray array = new JSONArray();
            String[] chunkList = strChunkSize.split(",");
            for (String s : chunkList) {
                array.put(Integer.valueOf(s.trim()));
            }
            obj.put("chunk_size", array);
            obj.put("chunk_interval", chunkInterval);
            obj.put("wav_name", "default");
            // 热词
            obj.put("hotwords", "阿里巴巴 达摩院");
            obj.put("wav_format", "pcm");
            obj.put("is_speaking", isSpeaking);
            return obj.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
    // 检查权限
    private boolean hasPermission() {
        return checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED &&
                checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    }
    // 请求权限
    private void requestPermission() {
        requestPermissions(new String[]{android.Manifest.permission.RECORD_AUDIO,
                Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
    }
}
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/ic_launcher_background.xml
New file
@@ -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>
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/ic_launcher_foreground.xml
New file
@@ -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>
funasr/runtime/android/AndroidClient/app/src/main/res/layout/activity_main.xml
New file
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <Button
        android:id="@+id/record_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="10dp"
        android:text="按下录音" />
    <com.yeyupiaoling.androidclient.AudioView
        android:id="@+id/audioView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_above="@id/record_button"
        android:layout_marginStart="10dp"
        android:visibility="gone" />
    <TextView
        android:id="@+id/result_text"
        android:layout_above="@id/record_button"
        android:layout_width="match_parent"
        android:hint="显示识别结果"
        android:textSize="22sp"
        android:layout_height="match_parent"/>
</RelativeLayout>
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
New file
@@ -0,0 +1,6 @@
<?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" />
    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
New file
@@ -0,0 +1,6 @@
<?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" />
    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
funasr/runtime/android/AndroidClient/app/src/main/res/values/colors.xml
New file
@@ -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>
funasr/runtime/android/AndroidClient/app/src/main/res/values/strings.xml
New file
@@ -0,0 +1,3 @@
<resources>
    <string name="app_name">FunASR</string>
</resources>
funasr/runtime/android/AndroidClient/app/src/main/res/values/themes.xml
New file
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.AndroidClient" 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">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>
funasr/runtime/android/AndroidClient/app/src/main/res/xml/backup_rules.xml
New file
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
   Sample backup rules file; uncomment and customize as necessary.
   See https://developer.android.com/guide/topics/data/autobackup
   for details.
   Note: This file is ignored for devices older that API 31
   See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
    <!--
   <include domain="sharedpref" path="."/>
   <exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
funasr/runtime/android/AndroidClient/app/src/main/res/xml/data_extraction_rules.xml
New file
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
   Sample data extraction rules file; uncomment and customize as necessary.
   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
   for details.
-->
<data-extraction-rules>
    <cloud-backup>
        <!--
        <include .../>
        <exclude .../>
        -->
    </cloud-backup>
    <!--
    <device-transfer>
        <include .../>
        <exclude .../>
    </device-transfer>
    -->
</data-extraction-rules>
funasr/runtime/android/AndroidClient/app/src/test/java/com/yeyupiaoling/androidclient/ExampleUnitTest.kt
New file
@@ -0,0 +1,17 @@
package com.yeyupiaoling.androidclient
import org.junit.Test
import org.junit.Assert.*
/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}
funasr/runtime/android/AndroidClient/build.gradle
New file
@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
}
funasr/runtime/android/AndroidClient/gradle.properties
New file
@@ -0,0 +1,23 @@
# 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
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
funasr/runtime/android/AndroidClient/gradle/wrapper/gradle-wrapper.jar
Binary files differ
funasr/runtime/android/AndroidClient/gradle/wrapper/gradle-wrapper.properties
New file
@@ -0,0 +1,6 @@
#Fri Oct 13 14:55:29 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
funasr/runtime/android/AndroidClient/gradlew
New file
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
##  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='"-Xmx64m" "-Xms64m"'
# 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 or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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"
exec "$JAVACMD" "$@"
funasr/runtime/android/AndroidClient/gradlew.bat
New file
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@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 Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@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="-Xmx64m" "-Xms64m"
@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 execute
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 execute
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
: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 %*
: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
funasr/runtime/android/AndroidClient/settings.gradle
New file
@@ -0,0 +1,17 @@
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "AndroidClient"
include ':app'
funasr/runtime/android/images/demo.png
funasr/runtime/android/readme.md
New file
@@ -0,0 +1,13 @@
# AndroidClient
先说明,本项目是使用WebSocket连接服务器的语音识别服务,并不是将FunASR部署到Android里,服务启动方式请查看文档[SDK_advanced_guide_online_zh.md](https://github.com/alibaba-damo-academy/FunASR/blob/main/funasr/runtime/docs/SDK_advanced_guide_online_zh.md)。
使用最新的 Android Studio 打开`AndroidClient`项目,运行即可,在运行之前还需要修改`ASR_HOST`参数,该参数是语音识别服务的WebSocket接口地址,需要修复为开发者自己的服务地址。
应用只有一个功能,按钮下开始识别,松开按钮结束识别。
应用效果图:
<div align="center">
  <img src="./images/demo.png" alt="应用效果图" width="300">
</div>