Android音频采集

在 Android 开发领域,音频采集是一项非常重要且有趣的功能。它为各种应用程序,如语音聊天、音频录制、多媒体内容创作等提供了基础支持。今天我们就来深入探讨一下 Android 音频采集的两大类型:Mic 音频采集和系统音频采集。

1. Mic音频采集

在 Android 中,我们通常使用 AudioRecord 类来实现 Mic 音频采集。

1.1 AudioRecord介绍

1.1.1 参数介绍

java 复制代码
@RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
        int bufferSizeInBytes)
  1. 音频源(audioSource)

    • 这个参数指定了音频数据的来源。例如,MediaRecorder.AudioSource.MIC表示从设备的麦克风获取音频。除此之外,还有其他可选的音频源,如VOICE_RECOGNITION(用于语音识别)、VOICE_COMMUNICATION(用于语音通信,例如 VoIP 应用)等。不同的音频源在音频采集的特性上可能会有所不同,例如针对语音通信的音频源可能会对音频进行一些预处理,以优化语音传输的质量。
  2. 采样率(sampleRateInHz)

    • 采样率决定了每秒从音频信号中采集的样本数量。常见的采样率有 44100Hz(44.1kHz)和 16000Hz(16kHz)等。较高的采样率可以提供更高质量的音频,但同时也会产生更大的数据量。例如,44.1kHz 的采样率常用于音乐录制等对音质要求较高的场景,而 16kHz 的采样率在语音通信等场景中较为常见,因为它在保证一定语音清晰度的前提下,能够减少数据传输和处理的负担。
  3. 声道配置(channelConfig)

    • 声道配置参数用于指定音频是单声道(AudioFormat.CHANNEL_IN_MONO)还是立体声(AudioFormat.CHANNEL_IN_STEREO)。单声道音频只有一个音频通道,而立体声有两个通道,分别对应左右声道。在移动设备上,考虑到性能和存储空间等因素,单声道采集较为常用,并且可以在后期通过算法将单声道转换为立体声,以满足不同的应用需求。
  4. 音频格式(audioFormat)

    • 音频格式参数确定了音频数据的编码格式。常见的有AudioFormat.ENCODING_PCM_16BIT(16 位)和AudioFormat.ENCODING_PCM_8BIT(8 位)等。16 位格式能够提供更丰富的音频动态范围和更好的音质,但数据量相对较大。在 Android 手机等设备上,16 位 PCM 格式具有较好的兼容性,是比较常用的音频格式。
  5. 缓冲区大小(bufferSizeInBytes)

    • 缓冲区大小是AudioRecord中一个非常关键的参数。它决定了在音频采集过程中用于存储音频数据的缓冲区的大小。合适的缓冲区大小可以确保音频采集的流畅性,避免出现数据丢失或音频卡顿等问题。可以通过AudioRecord.getMinBufferSize方法来获取满足指定音频参数(采样率、声道配置和音频格式)的最小缓冲区大小。这个方法会根据设备的硬件性能和音频参数计算出一个合适的值,开发者通常可以根据这个最小值来合理设置缓冲区大小,例如可以适当增大缓冲区大小以应对一些复杂的音频处理场景,但过大的缓冲区可能会导致音频采集的延迟增加。

1.1.2 工作流程

  1. 初始化

    • 首先,需要使用合适的音频参数来创建AudioRecord对象。通过调用AudioRecord的构造函数,传入音频源、采样率、声道配置、音频格式和缓冲区大小等参数来完成初始化。例如:
java 复制代码
int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat);

if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
    Log.e(TAG, "AudioRecord.getMinBufferSize failed: " + bufferSize);
    return;
}

mBuffer = new byte[bufferSize];
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mSampleRate, channelConfig, mAudioFormat, bufferSize);
  • 如果audioRecord.getState() == AudioRecord.STATE_INITIALIZED,则表示AudioRecord对象成功初始化,可以进行下一步操作。
  1. 开始采集

    • 调用audioRecord.startRecording()方法来启动音频采集。一旦开始采集,音频数据就会开始填充到缓冲区中。
  2. 读取数据

    • 通常会在一个单独的线程中读取缓冲区中的音频数据。可以使用audioRecord.read()方法来读取数据。例如,将读取的数据存储到一个byte类型的数组中:
java 复制代码
int bytesRead = mAudioRecord.read(mBuffer, 0, mBuffer.length);
  • 这个readSize表示实际读取到的音频数据的大小。需要注意的是,在读取数据的过程中,要及时处理数据,避免缓冲区溢出。
  1. 停止采集和释放资源

    • 当音频采集完成后,需要调用audioRecord.stop()方法来停止采集,然后调用audioRecord.release()方法来释放AudioRecord对象占用的资源。这一步非常重要,因为如果不释放资源,可能会导致内存泄漏等问题。

1.2 具体实现

1.2.1 MainActivity

申请权限并打开CaptureActivity

java 复制代码
package com.skystack.mediaexporation;

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

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;

import com.skystack.mediaexporation.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'mediaexporation' library on application startup.
    static {
        System.loadLibrary("mediaexporation");
    }

    private ActivityMainBinding binding;

    private final static String TAG = MainActivity.class.getName();
    static private final String[] PERMISSION = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO};
    private final static int RequestCodePermissions = 1;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());


        if (!CheckPermission()) {
            ActivityCompat.requestPermissions(this, PERMISSION, RequestCodePermissions);
        }else{
            Init();
        }

    }

    private boolean CheckPermission(){
        boolean ret = true;
        for (String str : PERMISSION) {
            ret = ret && (ActivityCompat.checkSelfPermission(this, str) == PackageManager.PERMISSION_GRANTED);
        }
        return ret;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == RequestCodePermissions) {
            if (!CheckPermission()) {
                Log.e(TAG, "request permissions denied");
                Toast.makeText(this, "request permissions denied", Toast.LENGTH_SHORT).show();
                finish();
            }else{
                Init();
            }
        }
    }

    private void Init(){

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.capture:
                Toast.makeText(this, "capture", Toast.LENGTH_SHORT).show();
                CaptureActivity.IntentTo(this);
                break;
            case R.id.setting:
                Toast.makeText(this, "setting", Toast.LENGTH_SHORT).show();
                break;
        }
        return super.onOptionsItemSelected(item);
    }
}

1.2.2 CaptureActivity

采集控制及回调

java 复制代码
package com.skystack.mediaexporation;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.skystack.mediaexporation.databinding.ActivityCaptureBinding;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class CaptureActivity extends AppCompatActivity implements AudioCapture.AudioCaptureCallback{
    private static final String TAG = CaptureActivity.class.getName();
    private ActivityCaptureBinding binding;

    private AudioCapture mAudioCapture;
    private File mAudioFile;
    private FileOutputStream mAudioOutputStream;

    public static Intent NewIntent(Context context){
        Intent intent = new Intent(context, CaptureActivity.class);
        return intent;
    }
    public static void IntentTo(Context context){
        context.startActivity(NewIntent(context));
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityCaptureBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        mAudioCapture = new AudioCapture(this, 44100, 2);

        binding.buttonStart.setOnClickListener(new View.OnClickListener() {
            boolean mIsCapture = false;
            @Override
            public void onClick(View v) {
                mIsCapture = !mIsCapture;

                if(mIsCapture){
                    if(mAudioFile == null) {
                        mAudioFile = new File(Environment.getExternalStorageDirectory(), "audio.pcm");
                    }
                    if(mAudioOutputStream == null){
                        try {
                            mAudioOutputStream = new FileOutputStream(mAudioFile);
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                    mAudioCapture.StartRecord();
                    binding.buttonStart.setText("停止采集");
                }else{
                    mAudioCapture.StopRecording();
                    binding.buttonStart.setText("采集音频");

                    if(mAudioFile != null){
                        try {
                            mAudioOutputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                }

            }
        });

    }


    @Override
    public void OnAudioDataAvailable(byte[] data) {
        Log.i(TAG, "OnAudioDataAvailable: " + data.length);
        if(mAudioOutputStream != null){
            try {
                mAudioOutputStream.write(data);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

1.2.3 AudioCapture

音频采集类

java 复制代码
package com.skystack.mediaexporation;

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

public class AudioCapture {
    private static final String TAG = AudioCapture.class.getName();
    private final int mSampleRate;
    private final int mChannels;
    private static int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
    private AudioRecord mAudioRecord;
    private AudioCaptureCallback mCallback;

    private AudioRecordThread mRecordThread = null;
    private byte[] mBuffer = null;

    public AudioCapture(AudioCaptureCallback callback, int mSampleRate, int mChannels) {
        this.mCallback = callback;
        this.mSampleRate = mSampleRate;
        this.mChannels = mChannels;

        InitCapture();
    }

    private int ChannelCountToConfiguration(int channels) {
        return (channels == 1 ? android.media.AudioFormat.CHANNEL_IN_MONO : android.media.AudioFormat.CHANNEL_IN_STEREO);
    }

    @SuppressLint("MissingPermission")
    public void InitCapture() {
        int channelConfig = ChannelCountToConfiguration(mChannels);

        int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat);

        if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
            Log.e(TAG, "AudioRecord.getMinBufferSize failed: " + bufferSize);
            return;
        }

        mBuffer = new byte[bufferSize];
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mSampleRate, channelConfig, mAudioFormat, bufferSize);

        if (mAudioRecord == null || mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.e(TAG,"Failed to create a new AudioRecord instance");
            ReleaseAudioResources();
            return;
        }

    }

    public boolean StartRecord(){
        if(mAudioRecord == null) return false;

        if(mRecordThread != null) return false;

        try {
            mAudioRecord.startRecording();
        } catch (IllegalStateException e) {
            Log.e(TAG,"AudioRecord.startRecording failed: " + e.getMessage());
            return false;
        }
        if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
            Log.e(TAG, "AudioRecord.startRecording failed - incorrect state :"
                    + mAudioRecord.getRecordingState());
            return false;
        }
        mRecordThread = new AudioRecordThread();
        mRecordThread.start();

        return true;
    }

    public boolean StopRecording() {
        Log.d(TAG, "stopRecording");
        if(mRecordThread == null)
            return true;
        mRecordThread.StopThread();
        try {
            mRecordThread.join(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mRecordThread = null;
        Log.d(TAG, "stopRecording done");
        return true;
    }

    public boolean DestroyRecording(){
        StopRecording();
        ReleaseAudioResources();
        return true;
    }

    private void ReleaseAudioResources() {
        Log.d(TAG, "releaseAudioResources");
        if (mAudioRecord != null) {
            mAudioRecord.release();
            mAudioRecord = null;
        }
        if(mBuffer != null){
            mBuffer = null;
        }
    }


    private class AudioRecordThread extends Thread {
        private volatile boolean mKeepAlive = true;

        @Override
        public void run() {
            while (mKeepAlive){
                int bytesRead = mAudioRecord.read(mBuffer, 0, mBuffer.length);
                if(bytesRead == mBuffer.length){
                    if(mCallback != null){
                        mCallback.OnAudioDataAvailable(mBuffer);
                    }
                } else {
                    String errorMessage = "AudioRecord.read failed: " + bytesRead;
                    Log.e(TAG, errorMessage);
                    if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
                        mKeepAlive = false;
                        Log.e(TAG, errorMessage);
                    }
                }
            }

            try {
                if(mAudioRecord != null){
                    mAudioRecord.stop();
                }
            }catch (IllegalStateException e){
                Log.e(TAG, "AudioRecord.stop failed: " + e.getMessage());
            }
        }

        public void StopThread() {
            Log.d(TAG, "stopThread");
            mKeepAlive = false;
        }
    }

    public interface AudioCaptureCallback{
        void OnAudioDataAvailable(byte[] data);
    }

}

2. 系统音频采集

系统音频采集有两种方法,但都有局限性。

2.1 REMOTE_SUBMIX

将AudioRecord的source设置为REMOTE_SUBMIXREMOTE_SUBMIX会截断麦克风和耳机的声音,通过AudioRecord采集输出。

但是REMOTE_SUBMIX需要有system权限。适用于自己编的系统中采集音频,一般用在云手机等场景。

获取系统权限步骤:

  1. 在AndroidManifest.xml中声明系统权限,同时申请CAPTURE_AUDIO_OUTPUT权限。
xml 复制代码
android:sharedUserId="android.uid.system"
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
  1. 生成APK时,用与Android系统源码编译时一致的签名文件。

2.2 AudioPlaybackCapture

AudioPlaybackCapture API 是在 Android 10 中引入的。应用可以借助此 API 复制其他应用正在播放的音频。此功能类似于屏幕截图,但针对的是音频。主要用例是视频在线播放应用,这些应用希望捕获游戏正在播放的音频。

2.2.1 构建捕获应用

出于安全和隐私考虑,捕获播放的音频会施加一些限制。为了能够捕获音频,应用必须满足以下要求:

  • 应用必须具有 RECORD_AUDIO权限。
  • 应用必须调出 MediaProjectionManager.createScreenCaptureIntent() 显示的提示,并且用户必须批准此提示。
  • 捕获和播放音频的应用必须使用同一份用户个人资料。

如要从其他应用中捕获音频,您的应用必须构建AudioRecord 对象,并向其添AudioPlaybackCaptureConfiguration。请按以下步骤操作:

  1. 调用 AudioPlaybackCaptureConfiguration.Builder.build()以构建AudioPlaybackCaptureConfiguration
  2. 通过调用 setAudioPlaybackCaptureConfig将配置传递给 AudioRecord
java 复制代码
AudioFormat audioFormat = new AudioFormat.Builder()
        .setChannelMask(channelConfig)
        .setSampleRate(mSampleRate)
        .setEncoding(mAudioFormat)
        .build();

AudioPlaybackCaptureConfiguration configuration =
        new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
                .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
                .addMatchingUsage(AudioAttributes.USAGE_GAME)
                .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
                .build();

mAudioRecord = new AudioRecord.Builder()
        .setAudioPlaybackCaptureConfig(configuration)
        .setAudioFormat(audioFormat)
        .setBufferSizeInBytes(bufferSize)
        .build();

2.2.2 控制音频捕获

您的应用可以控制它可以录制的内容类型,以及哪些其他类型的应用可以录制自己的播放。

应用可以使用以下方法限制其可以捕获的音频:

  • AUDIO_USAGE 传递给AudioPlaybackCaptureConfiguration.addMatchingUsage()可允许捕获特定用法。多次调用该方法可指定多个用法。
  • AUDIO_USAGE 传递给 AudioPlaybackCaptureConfiguration.excludeUsage() 可禁止捕获相应用法。多次调用该方法可指定多个用法。
  • 将 UID 传递到 AudioPlaybackCaptureConfiguration.addMatchingUid()可仅捕获具有特定 UID 的应用。多次调用该方法可指定多个 UID。
  • 将 UID 传递到 AudioPlaybackCaptureConfiguration.excludeUid()可禁止捕获相应 UID。多次调用该方法可指定多个 UID。

请注意,您不能同时使用 addMatchingUsage()excludeUsage() 方法。您必须选择其中之一。同样,您也不能同时使用 addMatchingUid()excludeUid()

2.2.3 获取MediaProjection

首先注册一个ActivityResultLauncher

java 复制代码
mLauncher = registerForActivityResult(
        new ActivityResultContracts.StartActivityForResult(),
        new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                if(result.getResultCode() == RESULT_OK){
                    if(result.getData() != null){
                        mAudioCaptureIntent = new Intent(captureActivity, AudioCaptureService.class);
                        mAudioCaptureIntent.putExtra("resultCode", result.getResultCode());
                        mAudioCaptureIntent.putExtra("data", result.getData());
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                            startForegroundService(mAudioCaptureIntent);
                        }

                        Log.i(TAG, "获取屏幕录制权限成功");
                        binding.buttonMedia.setText("停止采集");

                    }
                }else{
                    Log.e(TAG, "获取屏幕录制权限失败");
                    binding.buttonMedia.setText("采集媒体");
                }
            }
        }
);

在开始采集时启动createScreenCaptureIntent

java 复制代码
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent screenCaptureIntent = mediaProjectionManager.createScreenCaptureIntent();
mLauncher.launch(screenCaptureIntent);

获取MediaProjection

注意:

  1. mediaProjection需要一个前台服务,所以获取MediaProjection需要在一个前台服务中运行,我们创建了一个前台service AudioCaptureService
  2. AndroidManifest.xml中的service中声明AudioCaptureService为类型为mediaProjection的前台服务。
xml 复制代码
<service
    android:name=".Capture.AudioCaptureService"
    android:foregroundServiceType="mediaProjection"
    android:enabled="true"
    android:exported="true"></service>
  1. 获取MediaProjection必须在startForeground之后。

3. Audacity播放

  1. 先从Andorid设备中导出保存的audio.pcm文件到Windows上。
  2. 打开Audacity。
  3. 文件->导入->原始数据,并选择audio.pcm文件。
  4. 按照采集时设置的格式,设置播放格式。
  5. 导入并播放。

4 完整代码

github

相关推荐
EasyDSS2 小时前
视频监控从安装到优化的技术指南,视频汇聚系统EasyCVR智能安防系统构建之道
大数据·网络·网络协议·音视频
似霰2 小时前
安卓adb shell串口基础指令
android·adb
fatiaozhang95274 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO5 小时前
Android APP 热修复原理
android·app·hotfix
阿酷tony6 小时前
将视频生成视频二维码步骤
音视频·视频格式·视频二维码·视频生成二维码
鸿蒙布道师6 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师6 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
9527华安6 小时前
国产紫光同创FPGA视频采集转SDI编码输出,基于HSSTHP高速接口,提供2套工程源码和技术支持
fpga开发·音视频·紫光同创·sdi·高速接口·hssthp
大耳猫6 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白6 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度