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"



版权声明:本文为公众号“编程牛人”原创文章,欢迎扫描博客左侧的二维码,并注明csdn,添加我为好友后取得授权,或加入技术讨论群。

相关文章推荐

m4a格式资源集合

转载:http://www.file-recovery.com/m4a-signature-format.htm MPEG-4 Part 14 Audio (M4A,M4B,M4P) Format &...
  • xph23
  • xph23
  • 2016年09月14日 14:53
  • 489

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

你的录音文件损坏了吗?你可以自己修复!这篇文章里,我将告诉你所需的步骤。 录音文件的拓展名为“m4a”. 这种音频数据是用AAC格式编码的,然后封装在了MPEG4文件中。 在安卓系统的QuickVoi...
  • dj0379
  • dj0379
  • 2016年11月03日 13:14
  • 3507

JAVE 视音频转码

官方参考文档:http://www.sauronsoftware.it/projects/jave/manual.php 一、什么是JAVE     JAVE(Java Aud...

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

http://blog.csdn.net/softwave/article/details/5819699 JAVE(Java Audio Video Encoder),是一款将音频...

mp4文件moov atom放置在mdat atom之前 代码实现

使用nginx搭建http mp4/flv流媒体服务器,要求mp4文件moov atom要放置在mdat atom前,才能边下载边播放。参考php-qtfaststart项目,测试代码如下:   ...

getchar()和getch()区别

很多时候我们都需要从用户的键盘上获取一些字符,比如搞一些小游戏,搞一个用户选择啦。 例如 switch(a=getchar()) { case: ............. break; ...

Linux 下使用clock_gettime给程序计时详解

Linux 下使用clock_gettime给程序计时详解,函数的原型如下: int clock_gettime(clockid_t clk_id, struct timespect *tp...

通信系统建模与仿真 笔记4 加法器M文件S函数

通信系统建模与仿真 笔记4 加法器M文件S函数

.y4m文件解析

.y4m文件解析 muxers.c typedef struct {  FILE *fh;//文件指针,用来保存一个已打开的文件的指针  int wid...
  • xkfz008
  • xkfz008
  • 2012年06月27日 22:14
  • 2535

运维笔记4(用户信息涉及到的文件,用户管理命令,用户权限的下放,更新:关于useradd -b -d -m -k的一些理解和使用)

概述:        理解多用户,在linux下对用户的创建,删除,更改等命令,最后附带一道给力的小题。 1.什么是用户? 从日常生活中想的话,我们每个人都是充当过用户,去银行你有个账户,对于银行你就...
  • No_red
  • No_red
  • 2016年10月02日 22:49
  • 1117
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:m4a文件裁剪
举报原因:
原因补充:

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