Home | 简体中文 | 繁体中文 | 杂文 | Github | 知乎专栏 | Facebook | Linkedin | Youtube | 打赏(Donations) | About
知乎专栏

13.8. 麦克风

13.8.1. AudioRecord

		
    private AudioRecord getAudioRecord() {
        final int SAMPLE_RATE = 16000;
        final int WAVE_FRAM_SIZE = 20 * 2 * 16000 / 1000; //20ms audio for 16k/16bit/mono
        try {
            int bufferSize = AudioRecord.getMinBufferSize(16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
//            AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);

            AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, SAMPLE_RATE,
                    AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, WAVE_FRAM_SIZE * 4);

            if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
                return recorder;
            }
        } catch (Exception e) {
            Log.e(TAG, "Error in Audio Record");
        }
        return null;
    }		
		
			

13.8.2. 远端录音

AudioDeviceInfo.TYPE_REMOTE_SUBMIX 用于投屏,电视等设备录音

			
AudioRecord record = new AudioRecord(AudioSource.REMOTE_SUBMIX, 44100,
        AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

			
			

13.8.3. 选择麦克风

当设备中有多个麦克风时,我们希望切换到另一个麦克风,可以采用此方法。

		
AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 16000 * 4);

        AudioManager audioManager = (AudioManager) MainApplication.getContext().getSystemService(Context.AUDIO_SERVICE);
        AudioDeviceInfo[] audioDeviceInfos = new AudioDeviceInfo[]{};
        audioDeviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
        for (AudioDeviceInfo device : audioDeviceInfos) {
            if (device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE && device.getProductName().equals("USB-Audio - USB PnP Sound Device")) {
                audioRecord.setPreferredDevice(device);
                Log.d(TAG, "Set microphone: " + device.getProductName());
            }

            Log.d(TAG, device.getId() + " | " + device.getProductName() + " | " + device.getType());
        }
        audioRecord.startRecording();
        Log.d(TAG, "AudioRecord state: " + audioRecord.getState());

        if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.e(TAG, "audio recorder not init");
        } else {
            byte[] audioData = new byte[1024];
            for (int i = 0; i < 100; i++) {
                int ret = audioRecord.read(audioData, 0, 1024);
                Log.d(TAG, String.valueOf(audioData));
            }
        }
		
			

13.8.4. 设置蓝牙麦克风为默认麦克风

			
		if (ActivityCompat.checkSelfPermission(MainApplication.getContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
			// https://www.netkiller.cn/android/
            if (mAudioRecorder == null) {

                AudioManager audioManager = (AudioManager) MainApplication.getContext().getSystemService(Context.AUDIO_SERVICE);
                // 启动蓝牙SCO连接
                audioManager.startBluetoothSco();
                // 启用蓝牙SCO音频路由
                audioManager.setBluetoothScoOn(true);
                // 设置音频模式为通信模式(这会优先使用蓝牙麦克风)
                audioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT);

                // 获取所有音频输入设备
                AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);

                // 查找蓝牙设备
                AudioDeviceInfo bluetoothDevice = null;
                for (AudioDeviceInfo device : devices) {
                    if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
                            device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
                        bluetoothDevice = device;
                        break;
                    }
                }

                mAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        WAVE_FRAM_SIZE * 4);

                if (bluetoothDevice != null) {
                    if (mAudioRecorder.setPreferredDevice(bluetoothDevice)) {
                        Log.i(TAG, "成功设置蓝牙麦克风为首选设备 " + bluetoothDevice.getProductName());
                    }

                    AudioDeviceInfo currentDevice = mAudioRecorder.getRoutedDevice();
                    if (currentDevice != null) {
                        Log.d(TAG, "当前音频输入设备: " + currentDevice.getProductName());
                    }
                }
            } else {
                Log.w(TAG, "AudioRecord has been new ...");
            }
        } else {
            Log.e(TAG, "未获得录音权限 RECORD_AUDIO permission!");
        }			
			
			

13.8.5. 录音例子

			
package cn.netkiller.conference.test;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import java.nio.ByteBuffer;

import cn.netkiller.conference.MainApplication;
import cn.netkiller.conference.R;

public class TestActivity extends AppCompatActivity {

    private static final String TAG = TestActivity.class.getSimpleName();
    public String meetingJoinUrl;
    public String taskId;
    private AudioRecord audioRecord;


    @SuppressLint("MissingInflatedId")
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            return;
        }
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.UNPROCESSED, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 16000 * 4);

        AudioManager audioManager = (AudioManager) MainApplication.getContext().getSystemService(Context.AUDIO_SERVICE);
        AudioDeviceInfo[] audioDeviceInfos = new AudioDeviceInfo[]{};
        audioDeviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
        for (AudioDeviceInfo device : audioDeviceInfos) {
            Log.d(TAG, device.getId() + " | " + device.getProductName() + " | " + device.getType());

            if ((device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE || device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET) && device.getProductName().toString().contains("USB-Audio")) {
                audioRecord.setPreferredDevice(device);
                Log.d(TAG, "Set microphone: " + device.getProductName());
            }
        }
        audioRecord.startRecording();
        Log.d(TAG, "AudioRecord state: " + audioRecord.getState());

        if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.e(TAG, "audio recorder not init");
        } else {
            new Thread(new AudioRecordRunnable()).start();
        }


    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (audioRecord != null) {
            audioRecord.stop();
            audioRecord.release();
        }
    }

    private class AudioRecordRunnable implements Runnable {
        public void run() {
            Log.d(TAG, "Thread buffer start");
            ByteBuffer audioBuffer = ByteBuffer.allocate(1024);
            while (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                int read = audioRecord.read(audioBuffer, audioBuffer.capacity());
                if (read > 0) {
                    Log.d(TAG, audioBuffer.toString());
                }
//                Log.d(TAG, String.valueOf(read));
            }

//            try (FileOutputStream fos = new FileOutputStream("output.pcm")) {
//                while (isRecording) {
//                    int read = audioRecord.read(audioBuffer, audioBuffer.capacity());
//                    if (read > 0) {
//                        fos.write(audioBuffer.array(), 0, read);
//                    }
//                }
//            } catch (Exception e) {
//                Log.e(TAG, "Error writing audio", e);
//            }
        }
    }

}			
			
			

13.8.6. Microphone 录音,蓝牙,增益

			
package com.ideasprite.conference.ai.media.audio;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.AutomaticGainControl;
import android.media.audiofx.NoiseSuppressor;
import android.os.Environment;
import android.util.Log;

import androidx.core.app.ActivityCompat;

import com.ideasprite.conference.MainApplication;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Microphone {
    private static final String TAG = Microphone.class.getSimpleName();

    // 音频配置参数
    private static final int SAMPLE_RATE = 16000; // 44.1kHz
    private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    private static final int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
    // 单例模式
    private static Microphone instance;
    //    private volatile boolean isRecording = false;
    private static boolean isRecording = false;
    // 音频数据队列
//    private final BlockingQueue<byte[]> audioQueue = new LinkedBlockingQueue<>();
    // AudioRecord 实例
    private AudioRecord audioRecord;
    private FileOutputStream fileOutputStream;
//    private volatile boolean isProcessing = false;

    public Microphone() {
        if (ActivityCompat.checkSelfPermission(
                MainApplication.getContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
            // 初始化AudioRecord
            if (audioRecord == null) {
                audioRecord = new AudioRecord(
                        MediaRecorder.AudioSource.VOICE_RECOGNITION,
                        SAMPLE_RATE,
                        CHANNEL_CONFIG,
                        AUDIO_FORMAT,
                        minBufferSize * 2); // 使用两倍最小缓冲区
            }

            if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                Log.d(TAG, "AudioRecord 初始化成功");
                if (AutomaticGainControl.isAvailable()) {
                    AutomaticGainControl automaticGainControl = AutomaticGainControl.create(audioRecord.getAudioSessionId());
                    int status = automaticGainControl.setEnabled(true);
                    if (status == AutomaticGainControl.SUCCESS) {
                        Log.d(TAG, "AGC 启用成功");
                    } else {
                        Log.e(TAG, "AGC 启用失败,状态码: " + status);
                    }
                } else {
                    Log.d(TAG, "AutomaticGainControl 失败");
                }

                if (NoiseSuppressor.isAvailable()) {
                    NoiseSuppressor noiseSuppressor = NoiseSuppressor.create(audioRecord.getAudioSessionId());
//                    if (ns != null) {
                    int status = noiseSuppressor.setEnabled(true);
                    if (status == NoiseSuppressor.SUCCESS) {
                        Log.d(TAG, "NoiseSuppressor 启用成功");
                    } else {
                        Log.e(TAG, "NoiseSuppressor 启用失败,状态码: " + status);
                    }
//                    }
                } else {
                    Log.d(TAG, "NoiseSuppressor 失败");
                }
                if (AcousticEchoCanceler.isAvailable()) {
                    AcousticEchoCanceler acousticEchoCanceler = AcousticEchoCanceler.create(audioRecord.getAudioSessionId());
                    if (acousticEchoCanceler != null) {
                        int status = acousticEchoCanceler.setEnabled(true);
                        if (status == AcousticEchoCanceler.SUCCESS) {
                            Log.d(TAG, "AcousticEchoCanceler AEC 启用成功");
                        } else {
                            Log.e(TAG, "AcousticEchoCanceler AEC 启用失败,状态码: " + status);
                        }
                    }
                } else {
                    Log.d(TAG, "AudioRecord AcousticEchoCanceler 失败");
                }

            } else {
                Log.d(TAG, "AudioRecord state: " + audioRecord.getState());
            }

        }


    }

    public static synchronized Microphone getInstance() {
        if (instance == null) {
            instance = new Microphone();
        }
        Log.d(TAG, "Microphone getInstance()");
        return instance;
    }

    /**
     * 将 PCM 文件转换为 WAV 文件(单方法实现)
     *
     * @param pcmFilePath PCM 文件路径
     * @param wavFilePath WAV 文件路径
     * @param sampleRate  采样率(如 44100)
     * @param channels    声道数(1=单声道,2=立体声)
     * @param bitDepth    位深度(如 16 位)
     * @return 成功返回 true,失败返回 false
     */
    public static boolean convertPcmToWav(String pcmFilePath, String wavFilePath, int sampleRate, int channels, int bitDepth) {
        FileInputStream fis = null;
        FileOutputStream fos = null;

        try {
            File pcmFile = new File(pcmFilePath);
            File wavFile = new File(wavFilePath);

            // 创建 WAV 文件输出流
            fos = new FileOutputStream(wavFile);

            // 写入 WAV 文件头
            long fileSize = 36 + pcmFile.length();

            // RIFF 头
            fos.write("RIFF".getBytes());
            fos.write(intToLittleEndian((int) (fileSize - 8)));
            fos.write("WAVE".getBytes());

            // fmt 块
            fos.write("fmt ".getBytes());
            fos.write(intToLittleEndian(16)); // fmt 块大小
            fos.write(shortToLittleEndian((short) 1)); // 音频格式:PCM
            fos.write(shortToLittleEndian((short) channels)); // 声道数
            fos.write(intToLittleEndian(sampleRate)); // 采样率
            fos.write(intToLittleEndian(sampleRate * channels * bitDepth / 8)); // 字节率
            fos.write(shortToLittleEndian((short) (channels * bitDepth / 8))); // 块对齐
            fos.write(shortToLittleEndian((short) bitDepth)); // 位深度

            // data 块
            fos.write("data".getBytes());
            fos.write(intToLittleEndian((int) pcmFile.length())); // 数据大小

            // 复制 PCM 数据到 WAV 文件
            fis = new FileInputStream(pcmFile);
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }

            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            // 关闭流
            try {
                if (fis != null) fis.close();
                if (fos != null) fos.close();

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // int 转小端字节数组
    private static byte[] intToLittleEndian(int value) {
        return new byte[]{
                (byte) (value & 0xFF),
                (byte) ((value >> 8) & 0xFF),
                (byte) ((value >> 16) & 0xFF),
                (byte) ((value >> 24) & 0xFF)
        };
    }

    // short 转小端字节数组
    private static byte[] shortToLittleEndian(short value) {
        return new byte[]{
                (byte) (value & 0xFF),
                (byte) ((value >> 8) & 0xFF)
        };
    }

    public static void applyGainInPlace(byte[] pcmData, float gainFactor) {
        for (int i = 0; i < pcmData.length; i += 2) {
            // 从字节中解析 short(小端序)
            short sample = (short) ((pcmData[i + 1] << 8) | (pcmData[i] & 0xFF));

            // 应用增益
            float amplified = sample * gainFactor;

            // 限制范围
            amplified = Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, amplified));
            short limited = (short) amplified;

            // 写回字节数组
            pcmData[i] = (byte) (limited & 0xFF);
            pcmData[i + 1] = (byte) ((limited >> 8) & 0xFF);
        }
    }

    public void bluetooth() {
        AudioManager audioManager = (AudioManager) MainApplication.getContext().getSystemService(Context.AUDIO_SERVICE);
        // 启动蓝牙SCO连接
        audioManager.startBluetoothSco();
        // 启用蓝牙SCO音频路由
        audioManager.setBluetoothScoOn(true);
        // 设置音频模式为通信模式(这会优先使用蓝牙麦克风)
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);

        // 获取所有音频输入设备
        AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);

//        Log.d(TAG, "Bluetooth: start setup");

        // 查找蓝牙设备
        for (AudioDeviceInfo device : devices) {
            Log.d(TAG, "Bluetooth: device: " + device.getProductName());
            if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
                    device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
                Log.d(TAG, "Bluetooth: " + String.format("Product name: %s, Type: %s, Id: %s", device.getProductName(), device.getType(), device.getId()));
                if (device.getProductName().toString().contains("DELI")) {
                    if (audioRecord.setPreferredDevice(device)) {
                        Log.d(TAG, "Setting bluetooth: " + String.format("Product name: %s, Type: %s, Id: %s", device.getProductName(), device.getType(), device.getId()));
                        break;
                    }

                }

            }
        }
        AudioDeviceInfo currentDevice = audioRecord.getRoutedDevice();
        if (currentDevice != null) {
            Log.d(TAG, "AudioDeviceInfo: " + currentDevice.getProductName());
        }

    }

    /**
     * 开始音频采集和处理
     */
    public void start() {
        if (isRecording) {
            Log.w(TAG, "Already running");
            return;
        }

        isRecording = true;

        String dir = String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
//        String dir = Environment.getExternalStorageDirectory().getPath();
        String date = new SimpleDateFormat("yyyy-MM-dd.hhmmss").format(new Date());
        String filePath = String.format("%s/%s.pcm", dir, date);
        Log.i(TAG, "Start recording " + filePath);

        try {
            fileOutputStream = new FileOutputStream(filePath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        audioRecord.startRecording();

        this.bluetooth();

        Log.d(TAG, "AudioRecord startRecording()");

        if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.e(TAG, "audio recorder not init");
        } else {
            new Thread(() -> {
                byte[] audioData = new byte[minBufferSize]; // 1KB缓冲区
//                short[] audioData = new short[minBufferSize]; // 1KB缓冲区
                try {
                    while (isRecording) {

                        int read = audioRecord.read(audioData, 0, audioData.length);
                        if (read > 0) {
//                            applyGainInPlace(audioData, 3.0f);
                            fileOutputStream.write(audioData, 0, read);

//                            for (short s : audioData) {
//                                fileOutputStream.write((s >> 8) & 0xFF); // 高位字节
//                                fileOutputStream.write(s & 0xFF);         // 低位字节
//                            }
                        }
//                        Log.d(TAG, "Thread " + Thread.currentThread().getName() + " audioRecord.read " + audioBuffer.length);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        fileOutputStream.flush();
                        fileOutputStream.close();

                        boolean success = convertPcmToWav(
                                filePath,
                                filePath.replace(".pcm", ".wav"),
                                16000,                    // 采样率 44.1kHz
                                1,                        // 单声道
                                16                        // 16位深度
                        );

                        if (success) {
                            Log.d("AudioUtil", "PCM 转 WAV 成功");
                        } else {
                            Log.e("AudioUtil", "PCM 转 WAV 失败");
                        }

                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

            }).start();
        }


    }

    /**
     * 停止音频采集和处理
     */
    public void stop() {

        if (audioRecord != null) {
            audioRecord.stop();
        }
        isRecording = false;
        Log.i(TAG, "Stop recording ");
    }

    /**
     * 释放资源
     */
    public void release() {
        stop();
        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }
        Log.i(TAG, "release");
    }

    public void rmsAudioData(byte[] audioData) {
        // 这里只是简单打印日志,实际应用中可以进行音频分析、编码等操作
        Log.d(TAG, "Processing audio data, length: " + audioData.length);

        // 示例:计算音频数据的RMS值(粗略表示音量)
        long sum = 0;
        for (byte b : audioData) {
            sum += b * b;
        }
        double rms = Math.sqrt(sum / (double) audioData.length);
        Log.d(TAG, "Audio RMS: " + rms);
    }

    public void save(String filename) {
        if (!isRecording) {
            Log.i(TAG, "录音存储失败");
            return;
        }
        String filepath = MainApplication.getContext().getExternalFilesDir("debug") + "/" + filename + ".pcm";
        Log.i(TAG, "录音存储到 " + filepath);
        try (FileOutputStream fileOutputStream = new FileOutputStream(filepath)) {

            for (int i = 1; i < 100; i++) {
//                byte[] audioData = read();
//                if (audioData != null) {
//                    fileOutputStream.write(audioData);
//                    Log.i(TAG, "Thread " + Thread.currentThread().getName() + " 存储 " + filename + " 录音 " + audioData.length);
//                }
            }
//            stop();

        } catch (IOException e) {
            e.printStackTrace();
        }
//        finally {
//            fileOutputStream.flush();
//            fileOutputStream.close();
//        }

    }
}