人间观察
为了等你,我错过了等我的人。
介绍
Android中在一些短视频的制作app软件上,会有给视频增加背景音乐的功能,而背景音乐/歌曲(一般是mp3)是从服务器上下载后,然后本地解码,往往用户会选择一首歌曲的部分的时间段。所以实现方案就是:下载mp3->解码部分mp3为pcm->其它操作(比如文件,pcm处理)。所以此篇主要介绍解码部分的mp3为pcm,可以理解为mp3的剪切。
后续会介绍如何给视频增加背景音&视频的剪切等
上篇介绍过mp3的解码,这里主要介绍的是对其进行部分解码
代码仍然采用kotlin编写
技术点
本篇总体上讲比较简单,主要涉及如下技术点
- 音频硬解码,上篇有介绍
- 如何从文件中找到对应的音视频的轨道
- 如何提取指定时间段的音频,MediaExtractor
- pcm转wav,上篇有介绍
MediaExtractor提取器
官方API文档:
https://developer.android.com/reference/android/media/MediaExtractor?hl=es
如果想要解码一个mp3文件的一段音频或者提取一个视频中的视频流或者音频流,在Android 上层的api中需要借助MediaExtractor类,MediaExtractor类字面意思是多媒体提取器,它在Android的音视频开发里主要负责提取音视频相关信息(例如将视频文件剥离出音频与视频)和分离音视频。
主要API
setDataSource(String path):即可以设置本地文件又可以设置网络文件
getTrackCount():得到源文件通道数
getTrackFormat(int index):获取指定(index)的通道格式
getSampleTime():返回当前的时间戳
readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
advance():读取下一帧数据
release(): 读取结束后释放资源
使用的一般步骤
1.设置数据源 setDataSource
2.分离轨道 getTrackCount,getTrackFormat
3.选择轨道 selectTrack,unselectTrack
4.读取数据 readSampleData
5.下一帧 advance
6.释放 release
获取轨道
在音视频的封装格式中包含了多个数据流,比如一个mp4文件中可以包含一个视频轨道一个音频轨道,也可以有多个音频轨道。因为视频本身就是视频和音视按照一定的封装格式组成的嘛。对于mp3而言一般就只有音频轨道了。
如果我们要对视频或者音视进行剪切等剪辑的操作就需要先选中对应的轨道,然后在该轨道上进行后续的读取数据操作才可以。选取轨道用:
// 提取器
val mediaExtractor = MediaExtractor()
mediaExtractor.setDataSource(srcMp3Path)
// 选择音频轨道
val audioTrackIndex = selectTrack(mediaExtractor)
if (audioTrackIndex == -1) {
Log.w(TAG, "audioTrackIndex=-1")
return
}
private fun selectTrack(mediaExtractor: MediaExtractor): Int {
//获取轨道数量
val count = mediaExtractor.trackCount
for (i in 0 until count) {
// 获取当前轨道的编码信息
val format = mediaExtractor.getTrackFormat(i)
// 通过编码格式字符串对比获取指定轨道
if (format.getString(MediaFormat.KEY_MIME)!!.startsWith("audio/")) {
return i
}
}
return -1
}
音频流的轨道格式就是audio/
开头的,比如audio/mp4a-latm
,audio/mpeg
视频流的轨道格式就是video/
开头的,比如video/avc
获取编码信息
上面的获取轨道的代码里,我们可以看到使用MediaFormat可以获取当前轨道的编码格式.除了获取编码格式我们还能获取到很多其他信息.
对于音频来说,比如
获取采样率
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);//获取采样率
获取比特率
int bitRate = mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE);//获取比特
获取声道数量
int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);//获取声道数量
对于视频来说,比如
获取帧率
int frameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);//帧率
具体可以看下MediaFormat
类,里面定义了所有的音视频相关的常量
指定开始时间
选择完轨道之后,就需要在指定开始的时间,从哪个时间点开始读取对应的视频流/音频流数据。单位是us(微秒)。1毫秒=1000微秒,1ms=1000us 使用如下方法,同样适用于视频
// 从哪个时间开始提取
public void seekTo (long timeUs, int mode)
timeUs
是要seek
的时间戳,mode
是seek
的模式,可以是SEEK_TO_PREVIOUS_SYNC
, SEEK_TO_CLOSEST_SYNC
, SEEK_TO_NEXT_SYNC
,分别是seek指定帧的上一帧,最近帧和下一帧。
解码
关于MediaCodec解码的解码流程和API的使用可以参考前面的文章的介绍。对于mp3的解码。可以参考上一篇android音视频【九】音频硬编解码pcm&aac&wav
其中有一点需要注意的是mp3的信息比如:时间戳PTS,是否为关键帧等信息需要保留,不能因为解码而丢弃。
mediaExtractor.sampleFlags
mediaExtractor.sampleTime
mediaExtractor.sampleTime
获取到的DTS解码顺序不是PTS(展示)的顺序,这里是音频pts==dts,使用没问题。对于视频有b帧的可能要注意下。
剪切&解码的数据处理
- 通过
mediaExtractor.readSampleData(buffer, 0)
读取mp3数据到buffer
中。 - 利用
mediaCodec
的mediaCodec.queueInputBuffer
把读取到buffer的mp3送给解码器。 - 然后mediaExtractor前进一帧mp3数据
mediaExtractor.advance()
- 同时解码器解码出pcm,对pcm进行保存等操作
- 往复循环即可
如何认为是解码结束呢? 利用时间戳即可。当sampleTime大于endTimeUs
(我们的截取的结束时间)就退出解码循环即可。
全部的关键代码不到150行(这些都是上层的API嘛较简单),这里就全部贴出了。
package com.bj.gxz.mp3clipdemo
import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
/**
* Created by guxiuzhong@baidu.com on 2021/3/7.
*/
object Clip {
const val TAG = "Clip"
fun clip(srcMp3Path: String, destWavPath: String, startTimeUs: Long, endTimeUs: Long) {
if (endTimeUs < startTimeUs) {
Log.w(TAG, "endTimeMs < startTimeMs")
return
}
// 解码MP3为PCM,保存pcm的临时文件
val outPcmFile = File(Environment.getExternalStorageDirectory(), "temp.pcm")
val outPcmChannel = FileOutputStream(outPcmFile).channel
// 提取器
val mediaExtractor = MediaExtractor()
mediaExtractor.setDataSource(srcMp3Path)
// 选择音频轨道
val audioTrackIndex = selectTrack(mediaExtractor)
if (audioTrackIndex == -1) {
Log.w(TAG, "audioTrackIndex=-1")
return
}
// 选择提取的数据轨道
mediaExtractor.selectTrack(audioTrackIndex)
// 最关键的一句,从哪个时间开始提取
mediaExtractor.seekTo(startTimeUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
// 获取对应轨道的信息,这里是音频轨道,比如采样率等
val format = mediaExtractor.getTrackFormat(audioTrackIndex)
Log.d(TAG, "format=$format")
// 缓存区
val maxBufferSize: Int
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
maxBufferSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
Log.d(TAG, "KEY_MAX_INPUT_SIZE")
} else {
maxBufferSize = 100 * 1000
}
Log.d(TAG, "maxBufferSize=$maxBufferSize")
val buffer = ByteBuffer.allocateDirect(maxBufferSize)
// 创建硬解码器(一般是硬解码)
val mediaCodec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!)
mediaCodec.configure(format, null, null, 0)
mediaCodec.start()
// 解码
val info = MediaCodec.BufferInfo()
while (true) {
val inputIndex = mediaCodec.dequeueInputBuffer(10_1000)
if (inputIndex >= 0) {
val sampleTimeUs = mediaExtractor.sampleTime
if (sampleTimeUs == -1L) {
Log.d(TAG, "-1 break")
break
} else if (sampleTimeUs > endTimeUs) {
Log.d(TAG, "sampleTimeUs > endTimeUs break")
break
} else if (sampleTimeUs < startTimeUs) {
mediaExtractor.advance()
}
info.presentationTimeUs = sampleTimeUs
info.flags = mediaExtractor.sampleFlags
info.size = mediaExtractor.readSampleData(buffer, 0)
val data = ByteArray(buffer.remaining())
buffer.get(data)
// 送入MP3数据到解码器
val inputBuffer = mediaCodec.getInputBuffer(inputIndex)
inputBuffer!!.clear()
inputBuffer.put(data)
mediaCodec.queueInputBuffer(
inputIndex,
0,
info.size,
info.presentationTimeUs,
info.flags
)
// 读取下一个采样
mediaExtractor.advance()
}
// 获取解码后的pcm
var outputIndex = mediaCodec.dequeueOutputBuffer(info, 10_1000)
while (outputIndex >= 0) {
val outByteBuffer = mediaCodec.getOutputBuffer(outputIndex)
// PCM数据,你也可以做其它操作
// val data = ByteArray(info.size)
// buffer.get(data)
// 这里写入文件
outPcmChannel.write(outByteBuffer)
mediaCodec.releaseOutputBuffer(outputIndex, false)
outputIndex = mediaCodec.dequeueOutputBuffer(info, 0)
}
}
// 各种释放
outPcmChannel.close()
mediaCodec.stop()
mediaCodec.release()
mediaExtractor.release()
// demo的MP3:采样率是44100hz,声道数是 双声道 2,16位的
val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
val channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
Log.d(TAG, "pcm -> WAV sampleRate:$sampleRate")
Log.d(TAG, "pcm -> WAV channelCount:$channelCount")
// pcm -> WAV
val outWavPath = File(destWavPath)
PcmToWavUtil(sampleRate, channelCount, 16)
.pcmToWav(outPcmFile.absolutePath, outWavPath.absolutePath)
Log.d(TAG, "pcm -> WAV done:$outWavPath")
}
private fun selectTrack(mediaExtractor: MediaExtractor): Int {
//获取轨道数量
val count = mediaExtractor.trackCount
for (i in 0 until count) {
// 获取当前轨道的编码信息
val format = mediaExtractor.getTrackFormat(i)
// 通过编码格式字符串对比获取指定轨道
if (format.getString(MediaFormat.KEY_MIME)!!.startsWith("audio/")) {
return i
}
}
return -1
}
}
整个的代码很简单。最后把pcm文件转为了wav,当然你可以做其它处理。
总结
介绍了对于音频的硬解码操作,如何从一个封装格式的文件中(常见的mp3,mp4)中选中和获取对应的轨道以及对应的编码参数信息,如何进行解码mp3和解码截取其中的mp3数据,如何把pcm转为pcm,以及其中注意的一些点。可以看到其中有些知识点涉及到了上一篇,建议两篇结合着看,融会贯通。