Android平台上裁剪m4a

Android手机上设置铃声的操作是比较灵活的,一般读者听到一首喜欢的歌曲,马上就可以对这首歌曲进行裁剪,裁剪到片段后,再通过系统的接口设置为铃声(电话铃声、闹钟铃声等)。

前提是,播放这首歌的APP,需要提供裁剪歌曲的功能。

那么,怎么样实现截取音频文件的一个片段的功能呢?

小程很自然就想到使用FFmpeg命令来实现,之前介绍“从视频中提取图片”的内容就可以提取片段,比如:

ffmpeg -ss 10 -i audio.mp3 -t 5 out.mp3
上面的命令,从第10秒开始,提取5秒的片段。

读者可以关注“广州小程”微信公众号,并查阅“音视频->FFmpeg结构&应用”菜单项的内容。

但是,FFmpeg命令在pc上可以很方便地使用,但在手机APP上,就不能直接使用了。

小程这里针对Android平台,介绍另外的裁剪音频文件的办法,并且,这里假定原音频文件是m4a封装格式。

本文介绍如何在Android平台上裁剪m4a音频文件,并得到一个音频片段。

实现这个功能,基本有两个方案:

  • 一是解码原音频文件,然后提取相应片段,再对这个片段进行编码。
  • 二是直接定位到裁剪的起点,提取出片段,再保存成新的音频文件。

相比之下,第一个方案在性能上有更明显的消耗,但这个方案可以通吃各种音频格式(只要能解码,并能最终编码为固定格式即可)。

第二个方案,需要考虑不同格式(包括原音频,以及最终音频的格式)的实现,但在性能上占优,比第一个方案更省时间。

小程这里介绍第二个方案的实现,并且只考虑m4a文件的截取与生成。

第二个方案,概括来说,就是m4a格式的解析及m4a文件的生成过程

(一)m4a介绍

m4a文件,实际是mp4文件,一般只存放音频流。m4a是苹果公司起的名字,用来区分带有视频帧的一般的mp4文件。

解析m4a文件格式就是解析mp4文件格式,这对于写文件也是同样的道理。

要截取m4a的片段,有必要先解析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的字段表格)。

比如:


(二)方案实现

第二个方案的实现,可以使用ringdroid这个开源的项目。

ringdroid在git上维护,它最新的版本使用解码再编码的方案。

可以找回ringdroid早期的版本,里面有CheapAAC、CheapMP3等,分别对不同格式的音频作处理,并且是直接截取。

CheapAAC的ReadFile完成m4a文件的解析,WriteFile完成新的m4a文件的写入。

CheapAAC还实现了增益的计算,可以用来显示音频的波形图。

对于截取,有几个信息是很重要的:{帧的长度即字节数}、{帧的偏移量},根据这两个集合就可以实现截取。

帧的长度(以及总帧数)在解析stsz时确定,帧的偏移在解析mdat时确定。

读者可以详细阅读CheapAAC的代码,来理解截取的过程。小程这里只提一下CheapAAC存在的问题,也是读者可能遇到的问题。

(1)不兼容neroAacEnc编码的m4a文件

对于neroAacEnc编码出来的m4a文件,CheapAAC在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");
        }

(2)截取片段的时长不对

截取出来的片段的时长没有重新设置,仍使用原文件的时长。

可以在WriteFile里面重新设置片段的时长,但要注意,如果最终是使用mediaplayer来播放,则不能加以下代码,因为mediaplayer解码的处理跟FFmpeg等不一致。如果最终是交给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;

(三)其它概念

在CheapAAC中涉及到一些音频概念,小程简单解释一下。读者也可以关注“广州小程”微信公众号,查阅“音视频”菜单下的文章。

track,即轨道(音频或视频),也叫流;
sample,理解为帧(跟样本的概念不同),对于aac来说一帧包括的样本数是固定的,都为1024个;
chunk,即块,是帧的集合。

neroAcc命令使用示例:

ffmpeg -i "1.mp3" -f wav - | neroAacEnc -br 32000 -ignorelength -if - -of "1.m4a"
-br 码率
-lc/-he/-hev2 编码方式,默认是he
-if 输入文件
-of 输出文件
-ignorelength 在以其它输出(如ffmpeg)作为输入时使用

至此,在Android平台裁剪m4a的实现就介绍完毕了。



总结一下,本文介绍了在Android平台上,使用CheapAAC来裁剪m4a得到片段文件的实现办法,同时也介绍了m4a结构的概念,以及可能遇到的问题。

原文地址:http://blog.51cto.com/13136504/2108882

时间: 2024-10-27 12:42:28

Android平台上裁剪m4a的相关文章

如何在android平台上使用js直接调用Java方法[转]

转载自:http://www.cocos.com/docs/html5/v3/reflection/zh.html #如何在android平台上使用js直接调用Java方法 在cocos2d-js 3.0beta中加入了一个新特性,在android平台上我们可以通过反射直接在js中调用java的静态方法.它的使用方法很简单: var o = jsb.reflection.callStaticMethod(className, methodName, methodSignature, parame

【cocos2d-js官方文档】二十四、如何在android平台上使用js直接调用Java方法

在cocos2d-js 3.0beta中加入了一个新特性,在android平台上我们可以通过反射直接在js中调用java的静态方法.它的使用方法很简单: var o = jsb.reflection.callStaticMethod(className, methodName, methodSignature, parameters...) 在callStaticMethod方法中,我们通过传入Java的类名,方法名,方法签名,参数就可以直接调用Java的静态方法,并且可以获得Java方法的返回

Android平台上抓包

经常会有一些测试需求比如:测试手机上某个app的网络通信是否已经加密,或者测试某个app是否偷偷链接某些网站. 根据抓包的对象,主要有两种途径: 1. 在路由器端抓包:使用一台已经连入路由器的PC来抓取路由器上所有的数据传输信息. 2. 在终端抓包:在终端(手机,平板等)上抓取次终端设备的网络传输信息. 注意:可以很容易通过网络下载到很多可以用来抓包的apk,但是前提是android手机或者终端要root过,笔者试过其中的一.二款,感觉不够理想. 其实网上的这些工具也都是使用tcpdump这款命

Android平台上关于IM的实践总结

前言 IM通信在互联网发展到现在已经是码农的世界里人尽皆知的技术,特别在当下移动互联网迅猛发展的时代这种技术的开发也更加火热,其中老牌的代表作就有QQ和MSN,和最近新崛起的微信,默默,易信,来往等眼花缭乱的各种应用都把IM技术应用其中.我是Android开发人员,写这篇文章主要原因也是因为我自己从事开发以来主要做过的几款APP都是包含着IM通信,在不断的摸爬滚打的解决问题的过程中,积累了一些经验记录便将其记录到博客中作为自己一个阶段性的总结,也可以分享其他需要的开发者,作为一种参考实践的方案,

Freeline - Android平台上的秒级编译方案

FreeLine是什么? Freeline是蚂蚁金服旗下一站式理财平台蚂蚁聚宝团队15年10月在Android平台上的量身定做的一个基于动态替换的编译方案,5月阿里集团内部开源,稳定性方面:完善的基线对齐,进程级别异常隔离机制.性能方面:内部采用了类似Facebook的开源工具buck的多工程多任务并发思想:端口扫描,代码扫描,并发编译,并发dx,并发merge dex等策略,在多核机器上有明显加速效果,另外在class及dex,resources层面作了相应缓存策略,做到真正增量开发,另外引入

Android平台上最好的几款免费的代码编辑器

使用正确的开发工具能够快速有效地完成源代码的编写和测试,使编程事半功倍.在网络信息高速发展的今天,移动设备的方便快捷已经深入人心,越来越多的程序员会选择在任何感觉舒适的地方使用移动设备查看或者编辑源代码.于是,Android平台上大量基于代码编程的应运而生,谷歌应用商店里的代码编辑器.编译器和开发环境比比皆是.由于不同的工具特性和缺点不尽相同,因此如何选择一款最适合自己的开发工具便成了一件头疼的事情.在这里,我们列出了Android平台上5款最好的代码编辑器来帮助你作出选择. 1. Quoda

在Android平台上编译faad2

1.从官网下载源码 2.编辑一个config.h: /* config.h. Generated from config.h.in by configure. */ /* config.h.in. Generated from configure.in by autoheader. */ /* Define if you want to use libfaad together with Digital Radio Mondiale (DRM) */ /* #undef DRM */ /* De

cocos js js java互调 (如何在ANDROID平台上使用JS直接调用JAVA)

在cocos2d-js 3.0beta中加入了一个新特性,在android平台上我们可以通过反射直接在js中调用java的静态方法.它的使用方法很简单: var o = jsb.reflection.callStaticMethod(className, methodName, methodSignature, parameters...) 在callStaticMethod方法中,我们通过传入Java的类名,方法名,方法签名,参数就可以直接调用Java的静态方法,并且可以获得Java方法的返回

Android平台上直接物理内存读写漏洞的那些事

/* 本文章由 莫灰灰 编写,转载请注明出处. 作者:莫灰灰    邮箱: [email protected] */ 通过mmap直接操作物理内存的漏洞应该算是比较常见的一类漏洞了,在2012年.2013年的这段时间里,爆出了好几个物理内存读写相关的漏洞.主要是因为某些设备本身具有mmap物理内存的功能,但是其权限又是全局可读写的,最后,黑客通过精心构造的参数,patch相关函数或者数据结构达到权限提升的目的. 这样的设备主要有以下几个 /dev/exynos-mem CVE-2012-6422