google网站打不开,施工企业分录,搭建网站的空间哪里买,拍卖网站模板在 Android 开发领域#xff0c;音频采集是一项非常重要且有趣的功能。它为各种应用程序#xff0c;如语音聊天、音频录制、多媒体内容创作等提供了基础支持。今天我们就来深入探讨一下 Android 音频采集的两大类型#xff1a;Mic 音频采集和系统音频采集。
1. Mic音频采集…在 Android 开发领域音频采集是一项非常重要且有趣的功能。它为各种应用程序如语音聊天、音频录制、多媒体内容创作等提供了基础支持。今天我们就来深入探讨一下 Android 音频采集的两大类型Mic 音频采集和系统音频采集。
1. Mic音频采集
在 Android 中我们通常使用 AudioRecord 类来实现 Mic 音频采集。
1.1 AudioRecord介绍
1.1.1 参数介绍
RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)音频源audioSource 这个参数指定了音频数据的来源。例如MediaRecorder.AudioSource.MIC表示从设备的麦克风获取音频。除此之外还有其他可选的音频源如VOICE_RECOGNITION用于语音识别、VOICE_COMMUNICATION用于语音通信例如 VoIP 应用等。不同的音频源在音频采集的特性上可能会有所不同例如针对语音通信的音频源可能会对音频进行一些预处理以优化语音传输的质量。 采样率sampleRateInHz 采样率决定了每秒从音频信号中采集的样本数量。常见的采样率有 44100Hz44.1kHz和 16000Hz16kHz等。较高的采样率可以提供更高质量的音频但同时也会产生更大的数据量。例如44.1kHz 的采样率常用于音乐录制等对音质要求较高的场景而 16kHz 的采样率在语音通信等场景中较为常见因为它在保证一定语音清晰度的前提下能够减少数据传输和处理的负担。 声道配置channelConfig 声道配置参数用于指定音频是单声道AudioFormat.CHANNEL_IN_MONO还是立体声AudioFormat.CHANNEL_IN_STEREO。单声道音频只有一个音频通道而立体声有两个通道分别对应左右声道。在移动设备上考虑到性能和存储空间等因素单声道采集较为常用并且可以在后期通过算法将单声道转换为立体声以满足不同的应用需求。 音频格式audioFormat 音频格式参数确定了音频数据的编码格式。常见的有AudioFormat.ENCODING_PCM_16BIT16 位和AudioFormat.ENCODING_PCM_8BIT8 位等。16 位格式能够提供更丰富的音频动态范围和更好的音质但数据量相对较大。在 Android 手机等设备上16 位 PCM 格式具有较好的兼容性是比较常用的音频格式。 缓冲区大小bufferSizeInBytes 缓冲区大小是AudioRecord中一个非常关键的参数。它决定了在音频采集过程中用于存储音频数据的缓冲区的大小。合适的缓冲区大小可以确保音频采集的流畅性避免出现数据丢失或音频卡顿等问题。可以通过AudioRecord.getMinBufferSize方法来获取满足指定音频参数采样率、声道配置和音频格式的最小缓冲区大小。这个方法会根据设备的硬件性能和音频参数计算出一个合适的值开发者通常可以根据这个最小值来合理设置缓冲区大小例如可以适当增大缓冲区大小以应对一些复杂的音频处理场景但过大的缓冲区可能会导致音频采集的延迟增加。
1.1.2 工作流程 初始化 首先需要使用合适的音频参数来创建AudioRecord对象。通过调用AudioRecord的构造函数传入音频源、采样率、声道配置、音频格式和缓冲区大小等参数来完成初始化。例如
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对象成功初始化可以进行下一步操作。 开始采集 调用audioRecord.startRecording()方法来启动音频采集。一旦开始采集音频数据就会开始填充到缓冲区中。 读取数据 通常会在一个单独的线程中读取缓冲区中的音频数据。可以使用audioRecord.read()方法来读取数据。例如将读取的数据存储到一个byte类型的数组中
int bytesRead mAudioRecord.read(mBuffer, 0, mBuffer.length);这个readSize表示实际读取到的音频数据的大小。需要注意的是在读取数据的过程中要及时处理数据避免缓冲区溢出。 停止采集和释放资源 当音频采集完成后需要调用audioRecord.stop()方法来停止采集然后调用audioRecord.release()方法来释放AudioRecord对象占用的资源。这一步非常重要因为如果不释放资源可能会导致内存泄漏等问题。
1.2 具体实现
1.2.1 MainActivity
申请权限并打开CaptureActivity。
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;Overrideprotected 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;}Overridepublic 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(){}Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.main, menu);return true;}Overridepublic 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
采集控制及回调
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));}Overrideprotected 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;Overridepublic 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();}}}}});}Overridepublic 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
音频采集类
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;Overridepublic 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权限。适用于自己编的系统中采集音频一般用在云手机等场景。
获取系统权限步骤
在AndroidManifest.xml中声明系统权限同时申请CAPTURE_AUDIO_OUTPUT权限。
android:sharedUserIdandroid.uid.system
uses-permission android:nameandroid.permission.CAPTURE_AUDIO_OUTPUT /生成APK时用与Android系统源码编译时一致的签名文件。
2.2 AudioPlaybackCapture
AudioPlaybackCapture API 是在 Android 10 中引入的。应用可以借助此 API 复制其他应用正在播放的音频。此功能类似于屏幕截图但针对的是音频。主要用例是视频在线播放应用这些应用希望捕获游戏正在播放的音频。
2.2.1 构建捕获应用
出于安全和隐私考虑捕获播放的音频会施加一些限制。为了能够捕获音频应用必须满足以下要求
应用必须具有 RECORD_AUDIO权限。应用必须调出 MediaProjectionManager.createScreenCaptureIntent() 显示的提示并且用户必须批准此提示。捕获和播放音频的应用必须使用同一份用户个人资料。
如要从其他应用中捕获音频您的应用必须构建AudioRecord 对象并向其添AudioPlaybackCaptureConfiguration。请按以下步骤操作
调用 AudioPlaybackCaptureConfiguration.Builder.build()以构建AudioPlaybackCaptureConfiguration通过调用 setAudioPlaybackCaptureConfig将配置传递给 AudioRecord。
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
mLauncher registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),new ActivityResultCallbackActivityResult() {Overridepublic 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
MediaProjectionManager mediaProjectionManager (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent screenCaptureIntent mediaProjectionManager.createScreenCaptureIntent();
mLauncher.launch(screenCaptureIntent);获取MediaProjection
注意
mediaProjection需要一个前台服务所以获取MediaProjection需要在一个前台服务中运行我们创建了一个前台service AudioCaptureService。AndroidManifest.xml中的service中声明AudioCaptureService为类型为mediaProjection的前台服务。
serviceandroid:name.Capture.AudioCaptureServiceandroid:foregroundServiceTypemediaProjectionandroid:enabledtrueandroid:exportedtrue/service获取MediaProjection必须在startForeground之后。
3. Audacity播放
先从Andorid设备中导出保存的audio.pcm文件到Windows上。打开Audacity。文件-导入-原始数据并选择audio.pcm文件。按照采集时设置的格式设置播放格式。导入并播放。 4 完整代码
github