Thu âm và phát âm thanh thời gian thực với AudioRecord và AudioTrack trên Android

Để triển khai các ứng dụng đàm thoại hoặc xử lý âm thanh trực tiếp qua mạng, việc nắm vững cách sử dụng các API cấp thấp như AudioRecord và AudioTrack là rất quan trọng. Bài viết này hướng dẫn cách đóng gói và vận hành hai lớp này trong môi trường đa luồng.

1. Xây dựng lớp thu âm MicRecorder

Lớp MicRecorder đóng vai trò là một trình bao bọc (wrapper) cho AudioRecord, giúp quản lý cấu hình lấy mẫu và thu thập dữ liệu thô từ microphone.

package com.example.mediademo;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;

public class MicRecorder {
    private static final String LOG_TAG = "MicRecorder";
    private static final int FREQUENCY = 44100;
    private static final int CHANNEL_IN = AudioFormat.CHANNEL_IN_STEREO;
    private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;

    private AudioRecord recorder;
    public static final int MIN_BUFFER_SIZE = AudioRecord.getMinBufferSize(FREQUENCY, CHANNEL_IN, ENCODING);

    public void prepare() {
        recorder = new AudioRecord(
                MediaRecorder.AudioSource.MIC,
                FREQUENCY,
                CHANNEL_IN,
                ENCODING,
                MIN_BUFFER_SIZE);

        if (recorder.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.e(LOG_TAG, "Không thể khởi tạo bộ thu âm.");
        }
    }

    public void start() {
        if (recorder != null) recorder.startRecording();
    }

    public void stop() {
        if (recorder != null) {
            recorder.stop();
            recorder.release();
            recorder = null;
        }
    }

    public int pullData(byte[] dataArray, int length) {
        return recorder.read(dataArray, 0, length);
    }
}

2. Xây dựng lớp phát âm thanh StreamPlayer

Tương tự, StreamPlayer sẽ đóng gói AudioTrack để xử lý việc tái tạo âm thanh từ luồng dữ liệu PCM (Pulse-Code Modulation).

package com.example.mediademo;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;

public class StreamPlayer {
    private final int RATE = 44100;
    private final int CHANNEL_OUT = AudioFormat.CHANNEL_OUT_STEREO;
    private final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    
    private AudioTrack track;
    private int bufferCapacity;

    public void setup() {
        bufferCapacity = AudioTrack.getMinBufferSize(RATE, CHANNEL_OUT, FORMAT);
        track = new AudioTrack(
                AudioManager.STREAM_MUSIC,
                RATE,
                CHANNEL_OUT,
                FORMAT,
                bufferCapacity,
                AudioTrack.MODE_STREAM
        );
    }

    public void play() {
        if (track != null && track.getState() != AudioTrack.STATE_UNINITIALIZED) {
            track.play();
        }
    }

    public void feed(byte[] pcmData, int size) {
        if (size > 0 && track != null) {
            track.write(pcmData, 0, size);
        }
    }

    public void terminate() {
        if (track != null) {
            try {
                if (track.getState() != AudioTrack.STATE_UNINITIALIZED) {
                    track.stop();
                    track.release();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3. Điều phối luồng dữ liệu giữa thu và phát

Để thực hiện việc thu âm và phát lại đồng thời (loopback), chúng ta sử dụng hai luồng riêng biệt cùng một hàng đợi (Queue) làm bộ đệm trung gian để tránh hiện tượng nghẽn luồng giao diện người dùng (UI thread).

package com.example.mediademo;

import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;

public class AudioActivity extends AppCompatActivity {
    private MicRecorder mic;
    private StreamPlayer speaker;
    private volatile boolean isProcessing = false;
    
    private final Queue<byte[]> audioQueue = new LinkedList<>();
    private final ReentrantLock syncLock = new ReentrantLock();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio);
        
        mic = new MicRecorder();
        speaker = new StreamPlayer();
    }

    public void onStartClick(View v) {
        if (isProcessing) return;

        mic.prepare();
        speaker.setup();
        mic.start();
        speaker.play();

        isProcessing = true;
        
        // Luồng thu âm
        new Thread(() -> {
            int size = MicRecorder.MIN_BUFFER_SIZE;
            byte[] tempBuffer = new byte[size];
            while (isProcessing) {
                int read = mic.pullData(tempBuffer, size);
                if (read > 0) {
                    enqueueData(tempBuffer, read);
                }
            }
        }).start();

        // Luồng phát âm
        new Thread(() -> {
            while (isProcessing) {
                byte[] data = dequeueData();
                if (data != null) {
                    speaker.feed(data, data.length);
                }
            }
        }).start();
    }

    public void onStopClick(View v) {
        isProcessing = false;
        speaker.terminate();
        mic.stop();
    }

    private void enqueueData(byte[] raw, int len) {
        byte[] copy = new byte[len];
        System.arraycopy(raw, 0, copy, 0, len);
        syncLock.lock();
        try {
            audioQueue.offer(copy);
        } finally {
            syncLock.unlock();
        }
    }

    private byte[] dequeueData() {
        syncLock.lock();
        try {
            return audioQueue.poll();
        } finally {
            syncLock.unlock();
        }
    }
}

Trong kiến trúc trên, dữ liệu âm thanh được thu từ microphone dưới dạng PCM thô, sau đó được đưa vào một hàng đợi an toàn luồng (thread-safe). Luồng phát âm thanh liên tục kiểm tra hàng đợi này để lấy dữ liệu và đẩy vào AudioTrack, tạo nên một chu trình xử lý âm thanh thời gian thực mượt mà.

Thẻ: android-audio audiorecord audiotrack multithreading pcm-audio

Đăng vào ngày 26 tháng 7 lúc 09:43