Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách xây dựng Speech Recognizer (Nhận diện giọng nói) Android không dùng Google API

Giới thiệu

Bài viết này hướng dẫn xây dựng tính năng nhận diện giọng nói (Speech-to-Text) sử dụng SpeechRecognizer của Android SDK – API hệ thống, không phụ thuộc Google Cloud Speech API. SpeechRecognizer sử dụng engine nhận diện giọng nói cài đặt trên thiết bị (thường là Google Speech Services, nhưng có thể là engine khác của OEM).

Các bước thực hiện

Bước 1: Tạo dự án mới

Mở Android Studio, chọn File → New Project và tạo dự án.

Bước 2: Thiết kế giao diện (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        android:src="@drawable/ic_mic" />

    <ProgressBar
        android:id="@+id/progressBar1"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/toggleButton1"
        android:layout_marginTop="28dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/progressBar1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="47dp" />

    <ToggleButton
        android:id="@+id/toggleButton1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="26dp"
        android:textOn="Đang nghe..."
        android:textOff="Nhấn để nói" />

</RelativeLayout>

Bước 3: Viết code logic (src/MainActivity.java)

package app.com.sample;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements RecognitionListener {
    private static final int REQUEST_RECORD_PERMISSION = 100;
    private static final String LOG_TAG = "VoiceRecognitionActivity";

    private TextView returnedText;
    private ToggleButton toggleButton;
    private ProgressBar progressBar;
    private SpeechRecognizer speechRecognizer;
    private Intent recognizerIntent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        returnedText = findViewById(R.id.textView1);
        progressBar = findViewById(R.id.progressBar1);
        toggleButton = findViewById(R.id.toggleButton1);

        progressBar.setVisibility(View.INVISIBLE);

        // Kiểm tra tính khả dụng
        boolean available = SpeechRecognizer.isRecognitionAvailable(this);
        Log.i(LOG_TAG, "isRecognitionAvailable: " + available);
        if (!available) {
            Toast.makeText(this, "Thiết bị không hỗ trợ nhận diện giọng nói", Toast.LENGTH_LONG).show();
            toggleButton.setEnabled(false);
            return;
        }

        speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this);
        speechRecognizer.setRecognitionListener(this);

        recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, "en-US");
        recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
        recognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3);

        toggleButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked) {
                progressBar.setVisibility(View.VISIBLE);
                progressBar.setIndeterminate(true);
                // Xin quyền RECORD_AUDIO
                ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.RECORD_AUDIO},
                    REQUEST_RECORD_PERMISSION);
            } else {
                progressBar.setIndeterminate(false);
                progressBar.setVisibility(View.INVISIBLE);
                speechRecognizer.stopListening();
            }
        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_RECORD_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                speechRecognizer.startListening(recognizerIntent);
            } else {
                Toast.makeText(this, "Quyền ghi âm bị từ chối!", Toast.LENGTH_SHORT).show();
                toggleButton.setChecked(false);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (speechRecognizer != null) {
            speechRecognizer.destroy();
            Log.i(LOG_TAG, "SpeechRecognizer destroyed");
        }
    }

    // === RecognitionListener callbacks ===
    @Override public void onReadyForSpeech(Bundle params) { Log.i(LOG_TAG, "onReadyForSpeech"); }
    @Override public void onBeginningOfSpeech() {
        Log.i(LOG_TAG, "onBeginningOfSpeech");
        progressBar.setIndeterminate(false);
        progressBar.setMax(10);
    }
    @Override public void onRmsChanged(float rmsdB) {
        Log.i(LOG_TAG, "onRmsChanged: " + rmsdB);
        progressBar.setProgress((int) rmsdB);
    }
    @Override public void onBufferReceived(byte[] buffer) { Log.i(LOG_TAG, "onBufferReceived"); }
    @Override public void onEndOfSpeech() {
        Log.i(LOG_TAG, "onEndOfSpeech");
        progressBar.setIndeterminate(true);
        toggleButton.setChecked(false);
    }
    @Override public void onError(int errorCode) {
        String errorMessage = getErrorText(errorCode);
        Log.d(LOG_TAG, "FAILED: " + errorMessage);
        returnedText.setText(errorMessage);
        toggleButton.setChecked(false);
    }
    @Override public void onResults(Bundle results) {
        Log.i(LOG_TAG, "onResults");
        ArrayList<String> matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
        StringBuilder text = new StringBuilder();
        for (String result : matches) {
            text.append(result).append("\n");
        }
        returnedText.setText(text.toString());
    }
    @Override public void onPartialResults(Bundle partialResults) { Log.i(LOG_TAG, "onPartialResults"); }
    @Override public void onEvent(int eventType, Bundle params) { Log.i(LOG_TAG, "onEvent"); }

    private static String getErrorText(int errorCode) {
        switch (errorCode) {
            case SpeechRecognizer.ERROR_AUDIO: return "Lỗi ghi âm";
            case SpeechRecognizer.ERROR_CLIENT: return "Lỗi client";
            case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: return "Thiếu quyền";
            case SpeechRecognizer.ERROR_NETWORK: return "Lỗi mạng";
            case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: return "Hết thời gian chờ mạng";
            case SpeechRecognizer.ERROR_NO_MATCH: return "Không khớp giọng nói";
            case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: return "Recognizer bận";
            case SpeechRecognizer.ERROR_SERVER: return "Lỗi server";
            case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: return "Không phát hiện giọng nói";
            default: return "Không hiểu, vui lòng thử lại";
        }
    }
}

Bước 4: Cấu hình quyền trong AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="app.com.sample">

    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <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>

Chạy ứng dụng

Kết nối thiết bị thật (cần micro), nhấn Run. Nhấn ToggleButton -> cấp quyền ghi âm -> nói something -> kết quả hiển thị lên TextView, ProgressBar hiển thị âm lượng realtime.

Giải thích các tham số quan trọng

ExtraGiá trịMô tả
EXTRA_LANGUAGE_MODELLANGUAGE_MODEL_FREE_FORMNhận diện tự do (dictation)
EXTRA_LANGUAGE_MODELLANGUAGE_MODEL_WEB_SEARCHTối ưu cho tìm kiếm web (ngắn gọn)
EXTRA_LANGUAGE_PREFERENCE"vi-VN", "en-US"...Ngôn ngữ ưu tiên (BCP-47)
EXTRA_MAX_RESULTS1-10Số kết quả tối đa trả về
EXTRA_PARTIAL_RESULTStrueNhận kết quả trung gian (real-time)

Lưu ý & Best Practices

  • Luôn gọi destroy() trong onDestroy() để tránh memory leak
  • Xử lý lỗi ERROR_NO_MATCH và ERROR_SPEECH_TIMEOUT – phổ biến khi môi trường ồn hoặc nói quá nhanh/chậm
  • Engine nhận diện phụ thuộc thiết bị: Pixel dùng Google, Samsung dùng Bixby/Samsung Voice, Xiaomi dùng Mi AI...
  • Yêu cầu kết nối internet cho hầu hết engine (trừ một số engine offline)
  • Để hỗ trợ tiếng Việt tốt: EXTRA_LANGUAGE_PREFERENCE = "vi-VN"
  • Có thể dùng RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE cho chế độ hands-free

So sánh với Google Cloud Speech API

  • SpeechRecognizer (On-device/Platform): Miễn phí, offline một phần, phụ thuộc engine thiết bị, ít tùy chỉnh
  • Google Cloud Speech-to-Text: Trả phí (free tier 60 phút/tháng), độ chính xác cao, hỗ trợ streaming, custom vocabulary, speaker diarization