Android音视频【十】音频mp3剪切

人间观察
为了等你,我错过了等我的人。

介绍

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的时间戳,modeseek的模式,可以是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帧的可能要注意下。

剪切&解码的数据处理

  1. 通过mediaExtractor.readSampleData(buffer, 0)读取mp3数据到buffer中。
  2. 利用mediaCodecmediaCodec.queueInputBuffer把读取到buffer的mp3送给解码器。
  3. 然后mediaExtractor前进一帧mp3数据mediaExtractor.advance()
  4. 同时解码器解码出pcm,对pcm进行保存等操作
  5. 往复循环即可

如何认为是解码结束呢? 利用时间戳即可。当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,以及其中注意的一些点。可以看到其中有些知识点涉及到了上一篇,建议两篇结合着看,融会贯通。

源码

https://github.com/ta893115871/MP3ClipDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容