使用MediaRecord录音
使用 MediaRecorder 录音
MediaRecorder
是 Android 提供的用于录音和录像的 API,适合捕获音频(如麦克风输入)或视频(结合相机)。本文将重点介绍如何使用 MediaRecorder
进行音频录音,结合 Android 动画合集的背景,展示如何在录音过程中添加动画效果(如录音按钮的脉冲动画)。内容包括初始化、录音、保存音频、权限处理及错误管理。MediaRecorder
适合需要保存音频文件的场景(如语音备忘录、聊天应用),相比 AudioRecord
(低级 API),它更易用但控制粒度较低。
MediaRecorder 的作用与原理
- 作用:
MediaRecorder
提供简单的录音接口,支持从麦克风捕获音频并保存为文件(如 MP3、AAC)。它支持设置音频格式、编码器、采样率等参数。 - 原理:
MediaRecorder
通过底层媒体框架访问麦克风,处理音频数据并编码为指定格式,输出到文件或流。它使用状态机管理录音流程(如 Initialized、Prepared、Recording)。 - 应用场景:
- 语音备忘录或录音机应用。
- 聊天应用中的语音消息。
- 动画伴随录音(如录音时的进度条或按钮动画)。
- 音频分析(结合保存的音频文件)。
权限要求
录音需要以下权限,在 AndroidManifest.xml
中声明:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- API 28 及以下 -->
- 动态权限(API 23+):
Manifest.permission.RECORD_AUDIO
:访问麦克风。Manifest.permission.WRITE_EXTERNAL_STORAGE
:保存音频文件(API 28 及以下)。- 使用
ActivityCompat.requestPermissions
请求权限。
MediaRecorder 录音步骤
- 初始化 MediaRecorder:创建实例并设置音频源、格式、编码器等。
- 配置输出:指定保存路径或文件。
- 准备与录音:调用
prepare()
和start()
开始录音。 - 停止与保存:调用
stop()
保存音频文件。 - 释放资源:调用
release()
清理资源。
状态机
MediaRecorder
的状态包括:
- Initial:创建后。
- Initialized:设置数据源后(
setAudioSource
)。 - DataSourceConfigured:设置输出格式和编码器后。
- Prepared:调用
prepare()
后。 - Recording:调用
start()
后。 - Stopped:调用
stop()
后。
错误操作(如在错误状态调用方法)会导致 IllegalStateException
。
完整示例(录音 + 动画)
以下是一个使用 MediaRecorder
录音的示例,结合属性动画实现录音按钮的脉冲效果。
import android.Manifest;
import android.content.pm.PackageManager;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.widget.Button;
import android.animation.ObjectAnimator;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class MediaRecorderActivity extends AppCompatActivity {
private static final int REQUEST_RECORD_PERMISSION = 100;
private MediaRecorder mediaRecorder;
private boolean isRecording = false;
private String outputFile;
private Button recordButton;
private ObjectAnimator pulseAnimator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_recorder);
recordButton = findViewById(R.id.record_button);
// 初始化脉冲动画
pulseAnimator = ObjectAnimator.ofFloat(recordButton, "scaleX", 1f, 1.2f, 1f);
pulseAnimator.setDuration(1000);
pulseAnimator.setRepeatCount(ObjectAnimator.INFINITE);
// 录音按钮点击
recordButton.setOnClickListener(v -> {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_RECORD_PERMISSION);
} else {
toggleRecording();
}
});
}
private void toggleRecording() {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
}
private void startRecording() {
mediaRecorder = new MediaRecorder();
try {
// 设置音频源
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 设置输出格式
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 设置音频编码器
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
// 设置输出文件
outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),
"recording_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()) + ".m4a")
.getAbsolutePath();
mediaRecorder.setOutputFile(outputFile);
// 设置录音参数(可选)
mediaRecorder.setAudioSamplingRate(44100);
mediaRecorder.setAudioEncodingBitRate(96000);
// 准备录音
mediaRecorder.prepare();
mediaRecorder.start();
isRecording = true;
recordButton.setText("Stop Recording");
pulseAnimator.start(); // 启动动画
Toast.makeText(this, "Recording started", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "Failed to start recording", Toast.LENGTH_SHORT).show();
}
}
private void stopRecording() {
try {
mediaRecorder.stop();
mediaRecorder.release();
mediaRecorder = null;
isRecording = false;
recordButton.setText("Start Recording");
pulseAnimator.cancel(); // 停止动画
Toast.makeText(this, "Recording saved: " + outputFile, Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "Failed to stop recording", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_RECORD_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
toggleRecording();
} else {
Toast.makeText(this, "Record permission denied", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaRecorder != null) {
if (isRecording) {
mediaRecorder.stop();
}
mediaRecorder.release();
mediaRecorder = null;
}
pulseAnimator.cancel();
}
}
- 布局 XML(res/layout/activity_media_recorder.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/record_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Recording" />
</LinearLayout>
- AndroidManifest.xml(添加权限):
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- 说明:
- 权限:动态请求
RECORD_AUDIO
权限,保存文件需存储权限(API 28 及以下)。 - 录音配置:
- 音频源:
MIC
(麦克风)。 - 输出格式:
MPEG_4
(支持 MP3、AAC 等)。 - 编码器:
AAC
(高质量音频)。 - 输出路径:保存到
Environment.DIRECTORY_MUSIC
。
- 音频源:
- 动画:录音时按钮脉冲动画(
scaleX
循环缩放)。 - 生命周期:在
onDestroy
释放MediaRecorder
和动画资源。 - 错误处理:捕获异常(如麦克风占用、存储失败)并提示用户。
关键方法与配置
以下是 MediaRecorder
的核心方法和常用配置:
方法/配置 | 描述 | 用法示例 |
---|---|---|
setAudioSource(int) | 设置音频源(如 AudioSource.MIC )。 | mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); |
setOutputFormat(int) | 设置输出格式(如 OutputFormat.MPEG_4 、MP3 )。 | mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); |
setAudioEncoder(int) | 设置音频编码器(如 AudioEncoder.AAC 、AMR_NB )。 | mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); |
setOutputFile(String) | 设置输出文件路径。 | mediaRecorder.setOutputFile(filePath); |
prepare() | 准备录音(配置完成)。 | mediaRecorder.prepare(); |
start() | 开始录音。 | mediaRecorder.start(); |
stop() | 停止录音并保存。 | mediaRecorder.stop(); |
release() | 释放资源。 | mediaRecorder.release(); |
setAudioSamplingRate(int) | 设置采样率(如 44100 Hz)。 | mediaRecorder.setAudioSamplingRate(44100); |
setAudioEncodingBitRate(int) | 设置编码比特率(如 96000)。 | mediaRecorder.setAudioEncodingBitRate(96000); |
- 常用音频源:
MediaRecorder.AudioSource.MIC
:麦克风。MediaRecorder.AudioSource.DEFAULT
:默认音频源。MediaRecorder.AudioSource.VOICE_COMMUNICATION
:通话优化。- 常用格式与编码器:
- 格式:
OutputFormat.MPEG_4
(推荐)、THREE_GPP
、AMR_NB
。 - 编码器:
AudioEncoder.AAC
(高质量)、AMR_NB
(低带宽)。
错误处理与监听器
MediaRecorder
可能因麦克风占用、存储空间不足等原因失败,需妥善处理。
- 错误监听器:
mediaRecorder.setOnErrorListener((mr, what, extra) -> {
Log.e("MediaRecorder", "Error: " + what + ", Extra: " + extra);
Toast.makeText(this, "Recording error", Toast.LENGTH_SHORT).show();
});
- 信息监听器(如最大时长/文件大小):
mediaRecorder.setOnInfoListener((mr, what, extra) -> {
if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
stopRecording();
}
});
mediaRecorder.setMaxDuration(60000); // 最大 60 秒
- 常见错误:
IllegalStateException
:状态错误(如未调用prepare
)。IOException
:文件写入失败(如存储空间不足)。
优缺点
- 优点:
- 易用,适合快速实现录音功能。
- 支持多种音频格式(MP3、AAC、AMR)。
- 可设置采样率、比特率等参数。
- 缺点:
- 控制粒度较低(相比
AudioRecord
)。 - 不支持实时音频处理或流式传输。
- 状态管理需小心,避免
IllegalStateException
。 - 替代方案:
- AudioRecord:低级 API,适合实时音频处理。
- ExoPlayer(Media3):支持复杂音频/视频处理。
- Third-party Libraries:如 FFmpeg(高级音频处理)。
注意事项
- 权限:API 23+ 需动态请求权限,处理拒绝情况。
- 存储:API 29+ 推荐使用
MediaStore
保存音频,避免直接文件操作。 - 格式支持:确保设备支持目标格式和编码器(如某些设备不支持 AMR)。
- 生命周期:在
onPause
或onDestroy
停止并释放MediaRecorder
。 - 性能:录音占用内存和 CPU,建议限制最大时长(如 60 秒)。
- 调试:通过 Log 检查状态,验证输出文件是否存在。
扩展:Jetpack Compose 中的实现
在 Jetpack Compose 中,MediaRecorder
可直接使用,结合动画增强交互。以下是示例:
import android.Manifest
import android.content.pm.PackageManager
import android.media.MediaRecorder
import android.os.Environment
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun MediaRecorderScreen() {
val context = LocalContext.current
var isRecording by remember { mutableStateOf(false) }
var mediaRecorder by remember { mutableStateOf<MediaRecorder?>(null) }
var outputFile by remember { mutableStateOf("") }
val scale by animateFloatAsState(
targetValue = if (isRecording) 1.2f else 1f,
animationSpec = infiniteRepeatable(tween(1000))
)
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
toggleRecording(context, isRecording, { mediaRecorder = it }, { outputFile = it }, { isRecording = it })
} else {
// Handle permission denied
}
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(
onClick = {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
toggleRecording(context, isRecording, { mediaRecorder = it }, { outputFile = it }, { isRecording = it })
} else {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
},
modifier = Modifier.scale(scale)
) {
Text(if (isRecording) "Stop Recording" else "Start Recording")
}
if (outputFile.isNotEmpty()) {
Text("Saved: $outputFile")
}
}
DisposableEffect(Unit) {
onDispose {
mediaRecorder?.apply {
if (isRecording) stop()
release()
}
mediaRecorder = null
}
}
}
private fun toggleRecording(
context: android.content.Context,
isRecording: Boolean,
setMediaRecorder: (MediaRecorder?) -> Unit,
setOutputFile: (String) -> Unit,
setRecording: (Boolean) -> Unit
) {
if (isRecording) {
try {
mediaRecorder?.stop()
mediaRecorder?.release()
setMediaRecorder(null)
setRecording(false)
} catch (e: Exception) {
e.printStackTrace()
}
} else {
val recorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),
"recording_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.m4a")
setOutputFile(file.absolutePath)
setOutputFile(file.absolutePath)
try {
prepare()
start()
setMediaRecorder(this)
setRecording(true)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
- 说明:
- 使用
remember
缓存MediaRecorder
和状态。 animateFloatAsState
实现按钮脉冲动画。rememberLauncherForActivityResult
处理动态权限。DisposableEffect
确保资源释放。- 录音保存到
Environment.DIRECTORY_MUSIC
。
示例:录音进度动画
为录音添加进度条动画:
ValueAnimator progressAnimator = ValueAnimator.ofFloat(0f, 100f);
progressAnimator.setDuration(60000); // 60 秒
progressAnimator.addUpdateListener(animation -> {
float progress = (float) animation.getAnimatedValue();
progressBar.setProgress((int) progress);
});
recordButton.setOnClickListener(v -> {
if (isRecording) {
stopRecording();
progressAnimator.cancel();
} else {
startRecording();
progressAnimator.start();
}
});
如果需要更复杂功能(如实时音量检测、录像支持、与 Canvas 的高级结合)、或继续探讨 Android 动画合集(如过渡动画、Lottie),请告诉我!