使用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 录音步骤

  1. 初始化 MediaRecorder:创建实例并设置音频源、格式、编码器等。
  2. 配置输出:指定保存路径或文件。
  3. 准备与录音:调用 prepare()start() 开始录音。
  4. 停止与保存:调用 stop() 保存音频文件。
  5. 释放资源:调用 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_4MP3)。mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
setAudioEncoder(int)设置音频编码器(如 AudioEncoder.AACAMR_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_GPPAMR_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)。
  • 生命周期:在 onPauseonDestroy 停止并释放 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),请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注