AudioPlaybackCapture结合AudioRecord实现音频内录

4,506 阅读3分钟

现在很多安卓手机都自带了录音机或者屏幕录制功能,但是系统自带的录音机是录制麦克风声音;而系统的屏幕录制又是录制的视频,仅需要系统声音的话,使用其他第三方剪辑软件把声音单独剪出来。如果你恰恰只需要录制手机本身发出的声音,那么本文应该对你有些帮助。

在安卓5.0的时候,系统就开放了视频录制接口:MediaProjection 和 MediaProjectionManager,到了安卓10.0的时候系统又提供了音频捕捉接口AudioPlaybackCapture,有了这个接口我们就可以实现对系统播放的音频进行录制了。

AndroidManifest配置

申请和声明必要的用户权限,注意高版本在kt或者java代码中动态申请。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

AndroidManifest.xml中配置允许音频播放捕获

<application
    android:name=".App"
    android:allowAudioPlaybackCapture="true"
    ...
    tools:targetApi="q">

Activity中向用户申请录制授权

  1. 首先是向用户申请授权。
val mProjectionManager = this.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val screenCaptureIntent = mProjectionManager.createScreenCaptureIntent()
startActivityForResult(screenCaptureIntent, reqCode)

注意,此时弹出系统弹窗,该弹窗的样式是无法改变的,主要是为了保护用户隐私,防止钓鱼违规录音。

  1. 在用户同意授权之后,我们会在onActivityResult中收到回调。然后启动音频录制的service
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == reqCode && resultCode == Activity.RESULT_OK) {
        val intent = Intent(this, RecordService::class.java)
        intent.putExtra("resultData", data)
        intent.putExtra("resultCode", resultCode)
        startForegroundService(intent)
    }
}

service中执行录音功能

  1. 定义通知栏
override fun onCreate() {
    super.onCreate()
    EventBus.getDefault().register(this)
    initNotification()
}
...
...
...
/**
 * 初始化通知栏
 */
private fun initNotification() {
    val builder: Notification.Builder
    val channelID = "MRecordService"
    val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
    val channel = NotificationChannel(channelID, "录音服务", NotificationManager.IMPORTANCE_HIGH)
    channel.enableLights(true) //设置提示灯
    channel.lightColor = Color.RED //设置提示灯颜色
    channel.setShowBadge(true) //显示logo
    channel.description = "MRecordService Notification" //设置描述
    channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC //设置锁屏可见 VISIBILITY_PUBLIC=可见
    manager.createNotificationChannel(channel)
    builder = Notification.Builder(this, channelID)
    val notification = builder.setAutoCancel(false)
        .setContentTitle("录音服务") //标题
        .setContentText("录音服务正在运行...") //内容
        .setWhen(System.currentTimeMillis())
        .setSmallIcon(R.mipmap.ic_launcher) //设置小图标
        .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))//设置大图标
        .build()
    startForeground(1, notification)
}   
  1. 初始化录音机
...
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
    val currentResultCode = intent.getIntExtra("resultCode", 0)
    val resultData = intent.getParcelableExtra<Intent>("resultData")
    initAudioRecord(currentResultCode, resultData)
    return super.onStartCommand(intent, flags, startId)
}
...
...

/**
 * 初始化录音器
 */
private fun initAudioRecord(resultCode: Int, intent: Intent) {
    minBufferSize = AudioRecord.getMinBufferSize(mSampleRateInHZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
    val mediaProjectionManager = baseContext.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
    //设置应用程序录制系统音频的能力
    val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, intent)
    val builder = AudioRecord.Builder()
    builder.setAudioFormat(AudioFormat.Builder()
            .setSampleRate(mSampleRateInHZ) //设置采样率(一般为可选的三个-> 8000Hz 、16000Hz、44100Hz)
            .setChannelMask(AudioFormat.CHANNEL_IN_MONO) //音频通道的配置,可选的有-> AudioFormat.CHANNEL_IN_MONO 单声道,CHANNEL_IN_STEREO为双声道,立体声道,选择单声道就行
            .setEncoding(AudioFormat.ENCODING_PCM_16BIT).build()) //音频数据的格式,可选的有-> AudioFormat.ENCODING_PCM_8BIT,AudioFormat.ENCODING_PCM_16BIT
        .setBufferSizeInBytes(minBufferSize) //设置最小缓存区域
    val config = AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
        .addMatchingUsage(AudioAttributes.USAGE_MEDIA) //设置捕获多媒体音频
        .addMatchingUsage(AudioAttributes.USAGE_GAME) //设置捕获游戏音频
        .build()
    //将 AudioRecord 设置为录制其他应用播放的音频
    builder.setAudioPlaybackCaptureConfig(config)
    try {
        if (ContextCompat.checkSelfPermission(this,Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
            audioRecord = builder.build()
        }
    } catch (e: Exception) {
        e.printStackTrace()
        LogUtils.e("录音器错误", "录音器初始化失败")
    }
    //做完准备工作,就可以开始录音了
    startRecord()
}
  1. 开始录音,保存录音
...

/**
 * 开始录音
 */
private fun startRecord() {
    if (isRecording) {
        ToastUtils.showShort("已经在录音了")
        return
    }
    ToastUtils.showShort("开始录音了")
    isRecording = true
    //承接音频数据的字节数组
    val mAudioData = ByteArray(320)
    //保存到本地录音文件名
    val tmpName = System.currentTimeMillis().toString()
    //新建文件,承接音频数据
    val tmpFile = Utils.createFile("$tmpName.pcm")
    //新建文件,后面会把音频数据转换为.wav格式,写入到该文件
    val tmpOutFile = Utils.createFile("$tmpName.wav")
    //开始录音
    audioRecord?.startRecording()
    Thread {
        try {
            val outputStream = FileOutputStream(tmpFile.absoluteFile)
            while (isRecording) {
                //循环从音频硬件读取音频数据录制到字节数组中
                audioRecord?.read(mAudioData, 0, mAudioData.size)
                //将字节数组写入到tmpFile文件
                outputStream.write(mAudioData)
            }
            outputStream.close()
            //将.pcm文件转换为.wav文件
            Utils.pcmToWave(mSampleRateInHZ.toLong(), minBufferSize, tmpFile.absolutePath, tmpOutFile.absolutePath)
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }.start()
}
  1. 停止录音

我们可以在activity中发出停止录音的消息,service接收到该消息之后,执行停止录音。我这里用的是EventBus

...
...
@Subscribe(threadMode = ThreadMode.POSTING)
fun OnEvent(event: String?) {
    if (TextUtils.equals(event, "stop")) {
        ToastUtils.showShort("停止录音了")
        isRecording = false
        audioRecord!!.stop()
    }
}
ps:参考,感谢: