夜雨飘零
2023-10-18 de3f9462e020ad2f4c7302ec07fa808d177cec95
change send data size (#1014)

* change send data size

* 增加菜单栏和APK下载地址

* 忽略SSL证书验证
3个文件已修改
10个文件已添加
1个文件已删除
283 ■■■■ 已修改文件
funasr/runtime/android/AndroidClient/app/src/main/AndroidManifest.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/MainActivity.java 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/SSLSocketClient.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/edittext_border.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/layout/dialog_input_hotwords.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/layout/dialog_input_uri.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/res/menu/menu.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/images/QRcode.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/images/demo.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/images/image1.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/images/image2.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/images/image3.png 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/readme.md 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
funasr/runtime/android/AndroidClient/app/src/main/AndroidManifest.xml
@@ -10,9 +10,9 @@
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:icon="@drawable/logo"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:roundIcon="@drawable/logo"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidClient"
        tools:targetApi="31">
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/MainActivity.java
@@ -2,26 +2,31 @@
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
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.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
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;
@@ -32,8 +37,15 @@
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";
    // WebSocket地址
    public String ASR_HOST = "";
    // 发送的JSON数据
    public static final String MODE = "2pass";
    public static final String CHUNK_SIZE = "5, 10, 5";
    public static final int CHUNK_INTERVAL = 10;
    public static final int SEND_SIZE = 1920;
    // 热词
    private String hotWords = "阿里巴巴 达摩院 夜雨飘零";
    // 采样率
    public static final int SAMPLE_RATE = 16000;
    // 声道数
@@ -42,10 +54,10 @@
    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 SharedPreferences sharedPreferences;
    // 控件
    private Button recordBtn;
    private TextView resultText;
@@ -60,8 +72,6 @@
        if (!hasPermission()) {
            requestPermission();
        }
        // 录音参数
        minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, AUDIO_FORMAT);
        // 显示识别结果控件
        resultText = findViewById(R.id.result_text);
        // 显示录音状态控件
@@ -71,22 +81,100 @@
        recordBtn = findViewById(R.id.record_button);
        recordBtn.setOnTouchListener((v, event) -> {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                if (!ASR_HOST.equals("")) {
                isRecording = false;
                stopRecording();
                recordBtn.setText("按下录音");
            } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
                if (webSocket != null){
                    webSocket.cancel();
                    webSocket = null;
                }
            } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
                if (!ASR_HOST.equals("")) {
                allAsrText = "";
                asrText = "";
                isRecording = true;
                startRecording();
                recordBtn.setText("录音中...");
            }
            }
            return true;
        });
        // 读取WebSocket地址
        sharedPreferences = getSharedPreferences("FunASR", MODE_PRIVATE);
        String uri = sharedPreferences.getString("uri", "");
        if (uri.equals("")) {
            showUriInput();
        } else {
            ASR_HOST = uri;
        }
        // 读取热词
        String hotWords = sharedPreferences.getString("hotwords", "");
        if (!hotWords.equals("")) {
            this.hotWords = hotWords;
        }
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.change_uri) {
            showUriInput();
            return true;
        } else if (id == R.id.change_hotwords) {
            showHotWordsInput();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
    // 显示WebSocket地址输入框
    private void showUriInput() {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle("请输入WebSocket地址:");
        View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.dialog_input_uri, null);
        final EditText input = view.findViewById(R.id.uri_edit_text);
        if (!ASR_HOST.equals("")) {
            input.setText(ASR_HOST);
        }
        builder.setView(view);
        builder.setPositiveButton("确定", (dialog, id) -> {
            ASR_HOST = input.getText().toString();
            if (!ASR_HOST.equals("")) {
                Toast.makeText(MainActivity.this, "WebSocket地址:" + ASR_HOST, Toast.LENGTH_SHORT).show();
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString("uri", ASR_HOST);
                editor.apply();
            }
        });
        AlertDialog dialog = builder.create();
        dialog.show();
    }
    // 显示热词输入框
    private void showHotWordsInput() {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle("请输入热词:");
        View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.dialog_input_hotwords, null);
        final EditText input = view.findViewById(R.id.hotwords_edit_text);
        if (!this.hotWords.equals("")) {
            input.setText(this.hotWords);
        }
        builder.setView(view);
        builder.setPositiveButton("确定", (dialog, id) -> {
            String hotwords = input.getText().toString();
            if (!hotwords.equals("")) {
                this.hotWords = hotwords;
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString("hotwords", hotwords);
                editor.apply();
            }
        });
        AlertDialog dialog = builder.create();
        dialog.show();
    }
    // 开始录音
@@ -94,12 +182,12 @@
        // 准备录音器
        try {
            // 确保有权限
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
                requestPermission();
                return;
            }
            // 创建录音器
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL, AUDIO_FORMAT, minBufferSize);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL, AUDIO_FORMAT, SEND_SIZE);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }
@@ -127,14 +215,12 @@
    // 读取录音数据
    private void setAudioData() throws Exception {
        // 如果使用正常的wss,可以去掉这个
        HostnameVerifier hostnameVerifier = (hostname, session) -> {
            // 总是返回true,表示不验证域名
            return true;
        };
        // 建立WebSocket连接
        OkHttpClient client = new OkHttpClient.Builder()
                .hostnameVerifier(hostnameVerifier)
                // 忽略验证证书
                .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
                // 不验证域名
                .hostnameVerifier(SSLSocketClient.getHostnameVerifier())
                .build();
        Request request = new Request.Builder()
                .url(ASR_HOST)
@@ -145,6 +231,7 @@
            public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
                // 连接成功时的处理
                Log.d(TAG, "WebSocket连接成功");
                runOnUiThread(() -> Toast.makeText(MainActivity.this, "WebSocket连接成功", Toast.LENGTH_SHORT).show());
            }
            @Override
@@ -189,15 +276,16 @@
            public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, Response response) {
                // 连接失败时的处理
                Log.d(TAG, "WebSocket连接失败: " + t + ": " + response);
                runOnUiThread(() -> Toast.makeText(MainActivity.this, "WebSocket连接失败:" + t, Toast.LENGTH_SHORT).show());
            }
        });
        String message = getMessage("2pass", "5, 10, 5", 10, true);
        String message = getMessage(true);
        webSocket.send(message);
        audioRecord.startRecording();
        byte[] bytes = new byte[minBufferSize];
        byte[] bytes = new byte[SEND_SIZE];
        while (isRecording) {
            int readSize = audioRecord.read(bytes, 0, minBufferSize);
            int readSize = audioRecord.read(bytes, 0, SEND_SIZE);
            if (readSize > 0) {
                ByteString byteString = ByteString.of(bytes);
                webSocket.send(byteString);
@@ -211,20 +299,19 @@
    }
    // 发送第一步的JSON数据
    public String getMessage(String mode, String strChunkSize, int chunkInterval, boolean isSpeaking) {
    public String getMessage(boolean isSpeaking) {
        try {
            JSONObject obj = new JSONObject();
            obj.put("mode", mode);
            obj.put("mode", MODE);
            JSONArray array = new JSONArray();
            String[] chunkList = strChunkSize.split(",");
            String[] chunkList = CHUNK_SIZE.split(",");
            for (String s : chunkList) {
                array.put(Integer.valueOf(s.trim()));
            }
            obj.put("chunk_size", array);
            obj.put("chunk_interval", chunkInterval);
            obj.put("chunk_interval", CHUNK_INTERVAL);
            obj.put("wav_name", "default");
            // 热词
            obj.put("hotwords", "阿里巴巴 达摩院");
            obj.put("hotwords", hotWords);
            obj.put("wav_format", "pcm");
            obj.put("is_speaking", isSpeaking);
            return obj.toString();
@@ -236,13 +323,13 @@
    // 检查权限
    private boolean hasPermission() {
        return checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED &&
                checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
        return checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED &&
                checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    }
    // 请求权限
    private void requestPermission() {
        requestPermissions(new String[]{android.Manifest.permission.RECORD_AUDIO,
        requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO,
                Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
    }
}
funasr/runtime/android/AndroidClient/app/src/main/java/com/yeyupiaoling/androidclient/SSLSocketClient.java
New file
@@ -0,0 +1,71 @@
package com.yeyupiaoling.androidclient;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class SSLSocketClient {
    //获取SSLSocketFactory
    public static SSLSocketFactory getSSLSocketFactory() {
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, getTrustManager(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    //获取X509TrustManager
    public static X509TrustManager getX509TrustManager() {
        X509TrustManager x509TrustManager = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
        return x509TrustManager;
    }
    //获取TrustManager
    private static TrustManager[] getTrustManager() {
        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }
                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    }
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }
        };
        return trustAllCerts;
    }
    //获取HostnameVerifier
    public static HostnameVerifier getHostnameVerifier() {
        HostnameVerifier hostnameVerifier = (s, sslSession) -> true;
        return hostnameVerifier;
    }
}
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/edittext_border.xml
New file
@@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke android:width="1dp" android:color="#000000" />
    <padding android:left="5dp" android:top="5dp" android:right="5dp" android:bottom="5dp" />
</shape>
funasr/runtime/android/AndroidClient/app/src/main/res/drawable/logo.png
funasr/runtime/android/AndroidClient/app/src/main/res/layout/dialog_input_hotwords.xml
New file
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <EditText
        android:id="@+id/hotwords_edit_text"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_margin="10dp"
        android:inputType="textMultiLine"
        android:background="@drawable/edittext_border"
        android:minLines="3"
        android:gravity="top"
        android:hint="每个热词用空格隔开" />
</LinearLayout>
funasr/runtime/android/AndroidClient/app/src/main/res/layout/dialog_input_uri.xml
New file
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <EditText
        android:id="@+id/uri_edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@drawable/edittext_border"
        android:hint="wss://" />
</LinearLayout>
funasr/runtime/android/AndroidClient/app/src/main/res/menu/menu.xml
New file
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/change_uri"
        android:title="服务地址" />
    <item
        android:id="@+id/change_hotwords"
        android:title="热词" />
</menu>
funasr/runtime/android/images/QRcode.png
funasr/runtime/android/images/demo.png
Binary files differ
funasr/runtime/android/images/image1.png
funasr/runtime/android/images/image2.png
funasr/runtime/android/images/image3.png
funasr/runtime/android/readme.md
@@ -2,12 +2,19 @@
先说明,本项目是使用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接口地址,需要修复为开发者自己的服务地址。
使用最新的 Android Studio 打开`AndroidClient`项目,运行即可。也可以直接下载[APK安装包](https://yeyupiaoling.cn/AndroidClient.apk)安装使用,或者使用手机扫码下载。
应用只有一个功能,按钮下开始识别,松开按钮结束识别。
<div align="center">
  <img src="./images/QRcode.png" alt="APK安装包" width="300">
</div>
应用只有一个功能,按钮下开始识别,松开按钮结束识别。第一次打开应用需要设置WebSocket的地址,也可以在菜单栏修改,同时也可以在菜单栏修改热词。
应用效果图:
<div align="center">
  <img src="./images/demo.png" alt="应用效果图" width="300">
  <img src="./images/image1.png" alt="应用效果图" width="300">
  <img src="./images/image2.png" alt="应用效果图" width="300">
  <img src="./images/image3.png" alt="应用效果图" width="300">
</div>