如何拦截其他Android应用程序播放器的原始音频数据自定义保存下来?

1,386 阅读8分钟

145659l3n7e93x7sxyq9gs.png.thumb.jpg

音视频开发对于客户端来讲,是一个深水区,
而音视频开发中音频处理编辑又是深水区里面最深的那一块了。

逃离客户端开发最卷的UI应用层
避免学完这样UI学那样UI,
学完Android学React Native,
学完React Native学Flutter,
学完Flutter 学Kotlin Multiplatform(KMP)Compose Multiplatform(CMP)
然后你还要学Html + Css +Js,还要学Vue, Uniapp
何不一心一意搞音视频开发呢

一、前言

今天给大家带来音视频中皮毛中的皮毛功能

如何拦截其他Android应用程序播放器的原始音频数据自定义保存下来?
什么意思?
有个词叫内录
先介绍外录吧:
就是原始音频信号数据是怎么来的,是通过麦克风录制,传输到硬件设备,转化为数字信号,保存为文件比如:mp4,mp3等,画一个简单图如下: img_v3_02ih_547b4d90-340d-4b00-83f1-2416b952bb1g.jpg 通过上述图可以很清楚的知道:
外录:其实就是录制麦克风采样的环境中的声音,然后转化为数字信号,
这里是真正的有麦克风采样捕获环境中声音的过程,捕获完交给硬件DSP芯片编码成音频数据文件,如aac,mp3等
内录:其实没有真正录制采样的过程,只是相对外录命名的一个词语,它是怎么实现的过程呢?它是在上图中拦截了解码出能播放的数据,在播放该数据时,直接通过录音机,将该数据写入(原始数字信号数据直接拿来往里面写,当作是录制过程,这个是没有录制采样的过程),所以这种方式操作保存出来的音频流是和原始音频文件流是一样的。如果音频录制配置的参数和原来的碰巧一样,基本上等于复制一份,其实大多数情况是一样的。即便你播放时候把外放声音全部关掉,结果是没有任何影响的。

这样做有什么意义,有哪些应用场景
  • 1. 手机录屏,外加播放音乐,不录环境中任何声音的制作视频(直接麦克风录制会有环境中噪音,那样需要对音频进行降噪音处理

  • 2. 其他应用程序播放的音乐和视频,但是找不到它的原文件链接(找到源文件链接直接自己下载下来提取出里面音频数据就可以),可以这样内录出来拿到自己想要的音频数据

  • 3. 和2中一样,现在绝大多数VIP音乐收费,但是看MV免费,但是手机上MV可能抓包抓不到地址,也或者自己不想抓包或者不会抓,可以把MV中音频内录取下来,又可能有些音乐播放器能免费听,但实际它对链接进行了安全加密,抓取破解难度大了点,但是有试用期时间期限,可以在这个期限内先把自己喜欢的歌曲内录下来。保存好MP3.

二、基础工具Api介绍

  • 1. MediaProjection:是Android系统提供的一个API,用于屏幕捕捉和屏幕录制
  • 2. AudioPlaybackCaptureConfiguration:允许应用捕获正在播放的音频数据。需要设置MediaProjection的权限,并且要在支持该特性的设备上运行
  • 3. MediaCodec:是Android平台上用于音视频编解码的API,它允许开发者直接访问底层的编解码器,实现高效的音视频处理。MediaCodec支持硬件加速,可以利用设备的硬件资源来提高编解码的性能。其主要功能和特点包括:
    3.1. ‌音视频编解码‌:MediaCodec可以对音频和视频进行编解码处理,支持常见的音视频格式如H.264、AAC、MP3等‌。
    3.2. ‌硬件加速‌:MediaCodec支持硬件加速,可以利用设备的GPU或专用编解码芯片来提高编解码的效率和性能‌
    3.3. ‌实时处理‌:适用于实时通信、直播等需要实时处理的场景‌。
    3.4. ‌自定义处理‌:支持自定义的音视频处理,如滤镜、特效等操作‌
  • 4. AudioRecord:是Android平台上用于录制音频的工具,它支持捕获原始的PCM音频数据。与MediaRecorder不同,AudioRecord更加底层,提供了更多的控制权,允许开发者自定义处理音频数据,如录音存储、音频分析和语音识别等‌。
    4.1.主要作用:是从麦克风实时捕获音频。它支持多种音频源,如麦克风、电话线路和语音识别等。通过设置不同的参数,如采样率、声道配置和音频格式,开发者可以控制录音的质量和需求‌
    4.2.主要参数:
    audioSource‌:音频的来源,通常是麦克风(MediaRecorder.AudioSource.MIC)。 sampleRateInHz‌:采样率,常见的有16000Hz(电话音质)和44100Hz(CD音质)。 channelConfig‌:声道配置,可以是单声道(AudioFormat.CHANNEL_IN_MONO)或双声道(AudioFormat.CHANNEL_IN_STEREO)。
    audioFormat‌:音频数据的编码格式,常见的有16位PCM(AudioFormat.ENCODING_PCM_16BIT)和8位PCM(AudioFormat.ENCODING_PCM_8BIT)。 bufferSizeInBytes‌:录音时使用的缓存大小,需要通过AudioRecord.getMinBufferSize()计算得出‌
  • 5. MediaMuxer: 用来封装编码后的视频流和音频流到mp4容器中,MediaMuxer从api18开始提供,可以封装编码后的视频流和音频流到视频文件中。目前MediaMuxer支持的文件输出格式包括MP4,webm和3gp。

三、代码实现过程

1. 配置录音权限,和文件读写权限,即配置:


<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application android:name=".App" android:allowAudioPlaybackCapture="true" tools:targetApi="q"/>

2. 申请录音权限,文件读写权限

fun checkPermission(): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(
            Manifest.permission.CAMERA
        ) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
    ) {
        requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), 1)
    }
    return false
}

3.申请录屏权限:

val mediaProjectionManager = context.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = mediaProjectionManager.createScreenCaptureIntent()
(context as Activity).startActivityForResult(intent, 1001)

4. 得到录屏权限后需要拿到 MediaProjection

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 1001 && resultCode == RESULT_OK) {
        val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        //需要存到外部变量使用
        val mediaProjection: MediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data!!)
    }
}

5. 封装好内录类:WXAudioRecorder,传入MediaProjection和要生成的音频文件audioOutputFile,准备好MediaMuxerAudioPlaybackCaptureConfiguration如下:


@RequiresApi(Build.VERSION_CODES.Q)
class WXAudioRecorder(val context: Context, val mediaProjection: MediaProjection, val audioOutputFile: File) {

    private var mCodec: MediaCodec? = null
    private var mAudioRecord: AudioRecord? = null

    private val mMuxer by lazy {
        MediaMuxer(audioOutputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
    }
    private val playbackConfig by lazy {
        AudioPlaybackCaptureConfiguration.Builder(mediaProjection).addMatchingUsage(AudioAttributes.USAGE_MEDIA).addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).addMatchingUsage(AudioAttributes.USAGE_GAME).build();
    }
}

6. 准备录制参数配置数据:

// 采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
private const val SAMPLE_RATE_INHZ = 44100

// 声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO

// 返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT

// 音频采样位数
private const val AUDIO_BIT_RATE = 196000

private const val MIC_VOLUME_SCALE: Float = 1.4f
private const val TIMEOUT = 500L

7. 录音机监听系统播放背景音乐

//返回成功创建AudioRecord对象所需的最小缓冲区大小
val minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT) * 2
//音频数据编码方式
val format = AudioFormat.Builder().setEncoding(AUDIO_FORMAT).setSampleRate(SAMPLE_RATE_INHZ).setChannelMask(CHANNEL_CONFIG).build()
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
    //使用前需要先申请号权限
    return
}
//录音机和系统播放音频关联起来
mAudioRecord = AudioRecord.Builder().setAudioFormat(format).setAudioPlaybackCaptureConfig(playbackConfig).build()

8. 准备手机硬编码解码工具MediaCodec,参数配置

//封装描述媒体数据格式的信息
val medFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE_INHZ, 1)
// 设置音频编码音质等级
medFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
// 设置音频编码比特率
medFormat.setInteger(MediaFormat.KEY_BIT_RATE, AUDIO_BIT_RATE)
//音频的字节顺序。对于 PCM 编码的音频
medFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AUDIO_FORMAT)
//实例化支持给定mime类型输出数据的首选编码器、MIMETYPE_AUDIO_AAC是一种压缩格式
mCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC).apply {
    //配置参数
    configure(medFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
}

9. 开始拦截捕获实现内录::需要在异步线程里面执行

fun start() {
    jobAudioRecorder = CoroutineScope(Dispatchers.IO).launch {
        mAudioRecord?.startRecording()
        mCodec?.start()
        if (mAudioRecord?.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
            throw IllegalStateException("ScreenInternalAudioRecorder Audio recording failed to start");
        }
        flowAudio?.collect()//后面9,10,11都得在异步线程里面执行,我这里采用flow切换到线程里面实现,可以其他方式,只要去报在异步里面不影响UI主线程就行
    }
}

10. 拿到播放的音频数据存入到byte数组中:

var buffer =  ByteArray(minBufferSize) 
while (true) {
    var  readBytes = mAudioRecord!!.read(buffer!!, 0, buffer.size)
    //exit the loop when at end of stream
    if (readBytes < 0) {
        break
    }
    encode(buffer, readBytes)
}

11. 拿到数据后重新去编码成可生产音频文件的数据

//音频编码
private fun encode(buffer: ByteArray, readBytes: Int) {
    var readBytes = readBytes
    var offset = 0
    while (readBytes > 0) {
        var totalBytesRead = 0
        val bufferIndex = mCodec!!.dequeueInputBuffer(TIMEOUT)
        if (bufferIndex < 0) {
            writeOutput()
            return
        }
        val buff = mCodec!!.getInputBuffer(bufferIndex)
        buff!!.clear()
        val bufferSize = buff!!.capacity()
        val bytesToRead = min(readBytes.toDouble(), bufferSize.toDouble()).toInt()
        totalBytesRead += bytesToRead
        readBytes -= bytesToRead
        buff!!.put(buffer, offset, bytesToRead)
        offset += bytesToRead
        mCodec!!.queueInputBuffer(bufferIndex, 0, bytesToRead, mPresentationTime, 0)
        mTotalBytes += totalBytesRead
        mPresentationTime = 1000000L * (mTotalBytes / 2) / SAMPLE_RATE_INHZ
        writeOutput()
    }
}

12. 编码完成保存到音频文件

private fun writeOutput() {
    while (true) {
        val bufferInfo = MediaCodec.BufferInfo()
        val bufferIndex = mCodec!!.dequeueOutputBuffer(bufferInfo, TIMEOUT)
        if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            mTrackId = mMuxer.addTrack(mCodec!!.outputFormat)
            mMuxer.start()
            continue
        }
        if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
            break
        }
        if (mTrackId < 0) return
        val buff = mCodec!!.getOutputBuffer(bufferIndex)
        if (!((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0 && bufferInfo.size != 0)) {
            mMuxer.writeSampleData(mTrackId, buff!!, bufferInfo)
        }
        mCodec!!.releaseOutputBuffer(bufferIndex, false)
    }
}

13. 停止内录结束:

private fun endStream() {
    val bufferIndex = mCodec!!.dequeueInputBuffer(TIMEOUT)
    mCodec!!.queueInputBuffer(bufferIndex, 0, 0, mPresentationTime, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
    writeOutput()
}

四、总结

本文主要分享了:

  1. 如何拦截其他Android应用程序播放器的原始音频数据自定义保存下来?
  2. 内录的过程,它相对于外录的区别
  3. 内录相关API介绍以及代码实现过程

感谢阅读:

欢迎 关注,点赞、收藏

这里你会学到不一样的东西