每日推荐
如果你想研究下如何设计一个ImageLoader框架,不妨先从这篇文章开始学习:
《设计自己的ImageLoader图片加载框架》
http://blog.csdn.net/wozuihaole/article/details/78812411
链接:
https://juejin.im/post/5a324f3f5188253da72e7956
该app使用了MD规范,界面风格简洁,功能上mp3剪切铃声制作,实用性比较强。
1技术介绍
功能上虽然简洁,但是技术上该项目“麻雀虽小,五脏俱全”。
下面从技术层面上做一些简单介绍:
首页使用了CoordinatorLayout AppBarLayout DrawerLayout NavigationView的经典MD设计风格。
项目整体采用了MVP databinding rxjava2 rxandroid2 dagger2框架设计,数据缓存使用了greendao。
音频频谱的绘制主要是通过Visualizer中获取到的波形数据来进行绘制。
剪切功能上,mp3剪切核心功能使用了jaudiotagger jar包获取mp3元数据获取字节位置并进行文件io操作生成目标文件。此功能作为重点,本文后续会做详细的说明。
动画方面,欢迎页使用了lottie动画,如感兴趣可以看这篇博客做了详尽的步骤介绍,制作lottie动画并应用到android项目(https://juejin.im/post/5a355cf5f265da433227a53d)。 项目中文件选择页以及关于页面使用了属性动画和属性动画组件AVLoadingIndicatorView(https://github.com/81813780/AVLoadingIndicatorView)。
自定义控件,范围选取控件CustomRangeSeekBar(https://github.com/zyl409214686/CustomRangeSeekBar),不是本文重点可以看之前的博文android 自定义范围选取控件CustomRangeSeekBar(https://www.jianshu.com/p/712c13584636)。
2使用说明 gif
Step1. 选择mp3文件
Tips:主界面上可以看到三个按钮,从左到右的功能分别为:
播放\暂停
切换播放的滑块(切换当前播放的位置,前滑块or后滑块)
音乐剪切
mp3剪切实现思想
实现思想主要有两点
获取mp3开始时间(要剪切的开始时间)所在的文件字节位置及结束时间所在文件的字节位置
根据开始时间的字节位置和结束时间的字节位置结合源文件生成我们的目标文件
3mp3剪切技术实现
那么如何来获取mp3开始时间所在文件的字节位置呢?这里用到了jaudiotagger.jar。
http://www.jthink.net/jaudiotagger/
它的主页是这样描述它的
Jaudiotagger is a Java API for audio metatagging. Both a common API and format specific APIs are available, currently supports reading and writing metadata for:Mp3、Flac、OggVorbis、Mp4、Aiff、Wav、Wma、Dsf
它是一个音频元标记的java库,可以支持mp3等特定格式进行读写元数据操作。
mp3剪切实现细节:
一、我们要做的事通过Jaudiotagger获取到mp3的元数据,通过元数据取到mp3的首帧字节位置以及比特率。然后根据首帧字节位置以及比特率和开始时间可以其对应文件的字节位置。最后得到开始字节位置和结束字节位置。
1.获取mp3元数据
MP3File mp3 = new MP3File(this.mp3File);//获取mp3的元数据MP3AudioHeader header = (MP3AudioHeader) mp3.getAudioHeader();
2. 根据元数据获取mp3比特率
//根据元数据获取比特率long bitRateKbps = header.getBitRateAsNumber();
可能你会问,什么是比特率?
比特率是每秒传输的比特(bit)数
来看我们取mp3比特率的方法看注释long bitRate = header.getBitRateAsNumber();看该方法源码注释如下:
/**** @return bitrate in kbps, no indicator is provided as to * whether or not it is vbr*/public long getBitRateAsNumber(){ return bitrate;}
通过注释得知,此方法返回的比特率单位为kbps(每秒千字节) ,而我们需要的比特率的单位是(每毫秒位),下一步进行单位转换计算。
3. 转换比特率
这里我们需要换算它为每毫秒位数,1字节是8位,1秒是1000毫秒,千字节是1024字节,那么转换后算到的也就是getBitRateAsNumber() *1024L / 8L / 1000L。代码如下:
//计算出开始字节位置long bitRatebpm = bitRateKbps *1024L / 8L / 1000L * beginTime;
4.计算开始字节
这个值就是开始时间所在文件的字节位置吗?当然不是,我们的mp3文件当中并不只包含音乐的数据,还包含有音乐的信息头数据。
同样我们可以从头信息中取到我们的mp3首帧字节位置。首帧字节位置 每毫秒位为单位比特率,就是我们要的mp3开始字节位置了。
代码如下:
long firstFrameByte = header.getMp3StartByte();long beginByte = firstFrameByte beginBitRateBpm;
5. 计算结束字节位置
同理, 利用上面计算出来的开始字节beginType 时间差(剪切结束时间-开始时间)的比特率(单位为每毫秒位)就可以计算出结束的字节位置了,代码入下:
//计算出结束字节位置long endByte = beginByte convertKbpsToBpm(bitRateKbps) * (endTime – beginTime);
long endIndex(截取结束字节位置) = beginIndex(截取开始字节位置) bitRate *1024L / 8L / 1000L(比特率每毫秒位) * (endTime – beginTime)(截取的时长毫秒单位);
二、 有了开始时间的字节位置和结束时间的字节位置,那我们就可以结合源文件生成我们的目标文件拉。
读写文件我们可以使用RandomAccessFile实现随机的读写操作,通过RandomAccessFile.seek()方法调到指定位置。
问题&解决方案
如果我们要操作的mp3文件很大,比如我们截取的字节大小为100MB,这时候我们的app就会因为OOM直接crash掉了。
这里我的解决方案是通过一个缓存数组来限制每次读写的数据大小,每次操作指定大小的数据,这样无论文件多大,我们都不会出现OOM问题啦。
1.首先我们写一个工具方法,以缓存的方式来生成目标文件,源文件读取指定大小的数据读取写入到目标文件,代码如下:
/** * @param targetFile 输出的文件 * @param sourceFile 读取的文件 * @param buffer 输入输出的缓存容器 * @param offset 读入文件时seek的偏移值 */private static void writeSourceToTargetFile(RandomAccessFile targetFile, RandomAccessFile sourceFile, byte buffer[], long offset) throws Exception { sourceFile.seek(offset); sourceFile.read(buffer); long fileLength = targetFile.length(); // 将写文件指针移到文件尾。 targetFile.seek(fileLength); targetFile.write(buffer);}
2. 需要根据需要剪切文件的字节大小,分别考虑小于缓存以及大于等于缓存的情况,分别进行操作。
代码如下:
private static void writeSourceToTargetFileWithBuffer( RandomAccessFile targetFile, RandomAccessFile sourceFile, long totalSize, long offset) throws Exception { //缓存大小,每次写入指定数据防止内存泄漏 int buffersize = BUFFER_SIZE; long count = totalSize / buffersize; if (count <= 1) { //文件总长度小于小于缓存大小情况 writeSourceToTargetFile(targetFile, sourceFile, new byte[(int) totalSize], offset); } else { //计算出整除后剩余的数据数 long remainSize = totalSize % buffersize; byte data[] = new byte[buffersize]; //读入文件时seek的偏移量 for (int i = 0; i < count; i ) { writeSourceToTargetFile(targetFile, sourceFile, data, offset); offset = BUFFER_SIZE; } //写入剩余数据 if (remainSize > 0) { writeSourceToTargetFile(targetFile, sourceFile, new byte[(int) remainSize], offset); } }}
3. 最后要考虑不但要讲mp3乐音帧相关数据写入, 还要讲头信息写入进去,代码如下:
/** * 生成目标mp3文件 * * @param targetFile * @param beginByte * @param endByte * @param firstFrameByte * @throws Exception */private void generateTargetMp3File(RandomAccessFile targetFile, long beginByte, long endByte, long firstFrameByte) throws Exception { RandomAccessFile sourceFile = new RandomAccessFile(mSourceMp3File, “rw”); try { //write mp3 header info writeSourceToTargetFileWithBuffer(targetFile, sourceFile, firstFrameByte, 0); //write mp3 frame info int size = (int) (endByte – beginByte); writeSourceToTargetFileWithBuffer(targetFile, sourceFile, size, beginByte); } catch (Exception e) { e.printStackTrace(); } finally { if (sourceFile != null) sourceFile.close(); }}
https://github.com/zyl409214686/Mp3Cutter
github APK下载
https://github.com/zyl409214686/Mp3Cutter/blob/master/apk/app-cutter-release.apk
蒲公英 APK下载
https://www.pgyer.com/mp3cutter
单元测试
如果没有手机或其他原因不方便使用app。项目中提供了单元测试和mp3文件,可以通过单元测试来体验mp3剪切功能。
laozi.mp3是源mp3
test.mp3是运行完单元测试,生成的mp3文件。
startTime、endTime为剪切的开始时间及结束时间
推荐阅读:
上一篇:Java反射以及在Android中的特殊应用
Android APP 性能优化的一些思考