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
| Extra | Giá trị | Mô tả |
|---|---|---|
| EXTRA_LANGUAGE_MODEL | LANGUAGE_MODEL_FREE_FORM | Nhận diện tự do (dictation) |
| EXTRA_LANGUAGE_MODEL | LANGUAGE_MODEL_WEB_SEARCH | Tố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_RESULTS | 1-10 | Số kết quả tối đa trả về |
| EXTRA_PARTIAL_RESULTS | true | Nhậ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_MATCHvà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_FREEcho 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