m4a文件裁剪

原创 2016年08月31日 16:32:05
从一个功能说起:基于一个音频文件,截取其中一个片段作为铃声。实现这个功能,基本有两个方案:一是解码后再取相应片段出来编码;二是直接定位后截取出片段并保存成新文件。相比之下,第一个方案有时间上的明显消耗但可以通吃各种音频格式(只要能解码,然后最终编码为固定格式如aac)。这里介绍第二个方案的实现,并且只考虑m4a文件的截取。
第二个方案,概括来说,就是m4a格式的解析跟m4a文件的生成的过程。
m4a文件,实际是mp4文件(苹果公司重新起了一个名字,用来区分带有视频帧的一般的mp4文件),只是一般只存放音频流。所以,解析m4a文件格式就是解析mp4文件格式(对于写文件也同理)。
要截取m4a的片段,有必要先解析文件格式,获取相关信息(比如采样率、声道数、一帧的样本数、总帧数、每一帧的长度、每一帧的偏移等等),所以理解mp4的文件格式是有必要的。
mp4以atom(或者叫box)构成,所有的数据(包括各种信息以及裸的音频数据)都放在atom中。每个atom由三个字段组成:len(整个atom的长度,4Byte)、type(atom的类型,4Byte)、data(atom保存的数据)。atom可以嵌套。atom的类型有很多,但并不是所有类型都要有才能组成一个有效的mp4文件。有几个类型的atom是一定要有的:ftyp(标识文件格式)、stts(每一帧的样本数)、stsz(每一帧的长度)、stsc(帧与chunk的关系表)、mvhd(时长等信息)、mdat(裸数据)、moov等。具体的结构(包括每个atom的含意、每个字段的大小与含意)应该查看网络上的资源(最好能看到atom的字段表格)。
概念方面:track,即轨道(音频或视频)、流;sample,理解为帧(跟样本的概念不同),对于aac来说一帧包括的样本数是固定的(比如都为1024);chunk,即块,是帧的集合。
在实现上,java上,可以使用ringdroid这个开源的项目。ringdroid现在在git上维护,但它在实现上不同于早期的版本,现在是使用解码再编码的方案。可以找回ringdroid早期的版本,里面分出CheapAAC、CheapMP3等分别对不同的格式音频作处理并且是直接截取。
cheapaac的ReadFile完成m4a文件的解析,WriteFile完成新的m4a文件的写成。cheapaac还实现了增益的计算,可以用来显示音频的波形图。
对于截取,有几个信息是很重要的:{帧的长度即字节数}、{帧的偏移量},根据这两个集合就可以截取了。帧的长度(以及总帧数)在解析stsz时确定,帧的偏移在解析mdat时确定。


cheapaac在实现上有两个问题:
1. 截取出来的片段的时长没有重新设置,仍使用原文件的时长。可以在WriteFile里面这样设置片段的时长,但要注意对于使用mediaplayer来播放的情况是不能加以下的代码的,这可能是解码的处理跟ffmpeg等不一致:
// 在写完stco之后,增加:
long time = System.currentTimeMillis() / 1000;
        time += (66 * 365 + 16) * 24 * 60 * 60;  // number of seconds between 1904 and 1970
        byte[] createTime = new byte[4];
        createTime[0] = (byte)((time >> 24) & 0xFF);
        createTime[1] = (byte)((time >> 16) & 0xFF);
        createTime[2] = (byte)((time >> 8) & 0xFF);
        createTime[3] = (byte)(time & 0xFF);
        long numSamples = 1024 * numFrames;
        long durationMS = (numSamples * 1000) / mSampleRate;
        if ((numSamples * 1000) % mSampleRate > 0) {  // round the duration up.
            durationMS++;
        }
        byte[] numSaplesBytes = new byte[] {
                (byte)((numSamples >> 26) & 0XFF),
                (byte)((numSamples >> 16) & 0XFF),
                (byte)((numSamples >> 8) & 0XFF),
                (byte)(numSamples & 0XFF)
        };
        byte[] durationMSBytes = new byte[] {
                (byte)((durationMS >> 26) & 0XFF),
                (byte)((durationMS >> 16) & 0XFF),
                (byte)((durationMS >> 8) & 0XFF),
                (byte)(durationMS & 0XFF)
        };


        int type = kMDHD;
        Atom atom = mAtomMap.get(type);
        if (atom == null) {
            atom = new Atom();
            mAtomMap.put(type, atom);
        }
        atom.data = new byte[] {
                0, // version, 0 or 1
                0, 0, 0,  // flag
                createTime[0], createTime[1], createTime[2], createTime[3],  // creation time.
                createTime[0], createTime[1], createTime[2], createTime[3],  // modification time.
                0, 0, 0x03, (byte)0xE8,  // timescale = 1000 => duration expressed in ms.  1000为单位
                durationMSBytes[0], durationMSBytes[1], durationMSBytes[2], durationMSBytes[3],  // duration in ms.
                0, 0,     // languages
                0, 0      // pre-defined;
        };
        atom.len = atom.data.length + 8;


        type = kMVHD;
        atom = mAtomMap.get(type);
        if (atom == null) {
            atom = new Atom();
            mAtomMap.put(type, atom);
        }
        atom.data = new byte[] {
                0, // version, 0 or 1
                0, 0, 0, // flag
                createTime[0], createTime[1], createTime[2], createTime[3],  // creation time.
                createTime[0], createTime[1], createTime[2], createTime[3],  // modification time.
                0, 0, 0x03, (byte)0xE8,  // timescale = 1000 => duration expressed in ms.  1000为单位
                durationMSBytes[0], durationMSBytes[1], durationMSBytes[2], durationMSBytes[3],  // duration in ms.
                0, 1, 0, 0,  // rate = 1.0
                1, 0,        // volume = 1.0
                0, 0,        // reserved
                0, 0, 0, 0,  // reserved
                0, 0, 0, 0,  // reserved
                0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // unity matrix for video, 36bytes
                0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0,
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 2   // next track ID, 4bytes
        };
        atom.len = atom.data.length + 8;
2. 对于由neroAacEnc编码出来的m4a文件,parseMdat不能正常解析裸数据,原因是neroAacEnc在裸数据之前多加了8个字节,这8个字节会使得计算出来的每一帧的偏移都不对,导致后继WriteFile时写出来的每一帧的数据都不对。可以考虑跳过8个字节来解决(在判断为nero编码出来的m4a时):
if (mMdatOffset > 0 && mMdatLength > 0) {
            final int neroAACFrom = 570;
            int neroSkip = 0;
            if (mMdatOffset - neroAACFrom > 0) {
                FileInputStream cs = new FileInputStream(mInputFile);
                cs.skip(mMdatOffset - neroAACFrom);
                final int flagSize = 14;
                byte[] buffer = new byte[flagSize];
                cs.read(buffer, 0, flagSize);
                if (buffer[0] == 'N' && buffer[1] == 'e' && buffer[2] == 'r' && buffer[3] == 'o' && buffer[5] == 'A'
                        && buffer[6] == 'A' && buffer[7] == 'C' && buffer[9] == 'c' && buffer[10] == 'o'
                        && buffer[11] == 'd' && buffer[12] == 'e' && buffer[13] == 'c') {
                    neroSkip = 8;
                }
                cs.close();
            }


            stream = new FileInputStream(mInputFile);
            mMdatOffset += neroSkip; // slip 8 Bytes if need
            stream.skip(mMdatOffset);
            mOffset = mMdatOffset;
            parseMdat(stream, mMdatLength);
        } else {
            throw new java.io.IOException("Didn't find mdat");
        }


附带,neroAcc的命令的使用示例:ffmpeg -i "1.mp3" -f wav  - | neroAacEnc -br 32000 -ignorelength -if - -of  "1.m4a"
-br 码率
-lc/-he/-hev2 编码方式,默认可能是he
-if 输入文件
-of 输出文件
-ignorelength 在以其它输出(如ffmpeg)作为输入时使用
另,使用ffmepg也可以转音频格式:
ffmpeg -i "1.mp3" -f mp4 "1.m4a"
ffmpeg -i "1.mp3" -f ipod "1.m4a"



版权声明:本文为公众号“广州小程”原创文章,欢迎扫描博客左侧的二维码,关注有用的技能与通用能力。

M4A格式的ID3信息提取

原始代码:http://blog.csdn.net/werocpp/article/details/8602707 结合一个m4a文件进行十六进制分析,然后顺便把代码翻译成了java,和原文的代...
  • zengming00
  • zengming00
  • 2016年08月03日 17:16
  • 749

安卓上裁剪m4a | 铃声是怎么制作出来的

怎么简单地实现铃声制作?
  • freejet2018
  • freejet2018
  • 2017年07月25日 00:14
  • 513

推荐一款基于Java的音视频处理开源项目--JAVE

JAVE(Java Audio Video Encoder),是一款将音频和视频在不同格式间进行转化的工具,是基于ffmpeg项目的Java封装...
  • softwave
  • softwave
  • 2010年08月18日 00:39
  • 15170

编解码学习笔记(五):Mpeg系列——AAC音频

下面资料来自wiki。AAC在MPEG2和MPEG4中定义。 扩展名:.m4a, .m4b, .m4p, .m4v, .m4r, .3gp, .mp4, .aac  互联网媒体类型:audio...
  • mandagod
  • mandagod
  • 2016年06月10日 13:57
  • 706

AAC 音频编码格式解析

最近在做音频编解码相关的工作,有不少同事迷惑于AAC编解码格式同mp4/m4a编码容器,在此做个详细的AAC解析供编解码入门读者研读。 AAC( Advanced Audio Coding) 自M...
  • qiumingjian
  • qiumingjian
  • 2015年04月28日 16:43
  • 2943

文件签名表

转自:http://www.garykessler.net/library/file_sigs.html This table of file signatures (aka "magic num...
  • zhaoxd_1
  • zhaoxd_1
  • 2015年12月18日 16:38
  • 1597

m4a文件裁剪

从一个功能说起:基于一个音频文件,截取其中一个片段作为铃声。实现这个功能,基本有两个方案:一是解码后再取相应片段出来编码;二是直接定位后截取出片段并保存成新文件。相比之下,第一个方案有时间上的明显消耗...
  • freejet2018
  • freejet2018
  • 2016年08月31日 16:32
  • 2734

如何对音频文件进行剪辑

如何需要对多个音频文件进行拼接,可以使用cool Edit Pro软件。使用方法很简单,选择需要编辑的文件,然后在波形图中进行复制/剪切,粘贴即可。注意需要选择上下波形,右下角可以对时间范围进行手工输...
  • tiger119
  • tiger119
  • 2010年01月28日 07:16
  • 3276

iOS音频文件拼接+裁剪(音频文件拼接做了性能优化)

==============================.h文件 #import @interface AudioPieceTogether : NSObject // 1...
  • u010309384
  • u010309384
  • 2015年12月02日 13:14
  • 3508

如何修复破损的录音文件(m4a)

你的录音文件损坏了吗?你可以自己修复!这篇文章里,我将告诉你所需的步骤。 录音文件的拓展名为“m4a”. 这种音频数据是用AAC格式编码的,然后封装在了MPEG4文件中。 在安卓系统的QuickVoi...
  • dj0379
  • dj0379
  • 2016年11月03日 13:14
  • 4859
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:m4a文件裁剪
举报原因:
原因补充:

(最多只允许输入30个字)