功能上虽然简洁,但是技术上该项目“麻雀虽小,五脏俱全”。

下面从技术层面上做一些简单介绍:

首页使用了CoordinatorLayout+AppBarLayout+DrawerLayout+NavigationView的经典MD设计风格。

项目整体采用了MVP+databinding+rxjava2+rxandroid2+dagger2框架设计,数据缓存使用了greendao。

音频频谱的绘制主要是通过Visualizer中获取到的波形数据来进行绘制。

剪切功能上,mp3剪切核心功能使用了jaudiotagger jar包获取mp3元数据获取字节位置并进行文件io操作生成目标文件。此功能作为重点,本文后续会做详细的说明。

动画方面,欢迎页使用了lottie动画,如感兴趣可以看这篇博客做了详尽的步骤介绍,制作lottie动画并应用到android项目()。 项目中文件选择页以及关于页面使用了属性动画和属性动画组件AVLoadingIndicatorView(https://github.com/81813780/AVLoadingIndicatorView)。

自定义控件,范围选取控件CustomRangeSeekBar(https://github.com/zyl409214686/CustomRangeSeekBar),不是本文重点可以看之前的博文android 自定义范围选取控件CustomRangeSeekBar(https://www.jianshu.com/p/712c13584636)。

首页使用了CoordinatorLayout+AppBarLayout+DrawerLayout+NavigationView的经典MD设计风格。

项目整体采用了MVP+databinding+rxjava2+rxandroid2+dagger2框架设计,数据缓存使用了greendao。

音频频谱的绘制主要是通过Visualizer中获取到的波形数据来进行绘制。

剪切功能上,mp3剪切核心功能使用了jaudiotagger jar包获取mp3元数据获取字节位置并进行文件io操作生成目标文件。此功能作为重点,本文后续会做详细的说明。

动画方面,欢迎页使用了lottie动画,如感兴趣可以看这篇博客做了详尽的步骤介绍,制作lottie动画并应用到android项目()。 项目中文件选择页以及关于页面使用了属性动画和属性动画组件AVLoadingIndicatorView(https://github.com/81813780/AVLoadingIndicatorView)。

自定义控件,范围选取控件CustomRangeSeekBar(https://github.com/zyl409214686/CustomRangeSeekBar),不是本文重点可以看之前的博文android 自定义范围选取控件CustomRangeSeekBar(https://www.jianshu.com/p/712c13584636)。

2

使用说明+gif

Step1. 选择mp3文件

android MediaRecorder 生成mp3 mp3 editor on android_比特率

Step2. 通过滑块选择剪切范围然后点击剪切按钮

android MediaRecorder 生成mp3 mp3 editor on android_github_02

Tips:主界面上可以看到三个按钮,从左到右的功能分别为:

播放暂停

切换播放的滑块(切换当前播放的位置,前滑块or后滑块)

音乐剪切

播放暂停

切换播放的滑块(切换当前播放的位置,前滑块or后滑块)

音乐剪切

mp3剪切实现思想

实现思想主要有两点

获取mp3开始时间(要剪切的开始时间)所在的文件字节位置及结束时间所在文件的字节位置

根据开始时间的字节位置和结束时间的字节位置结合源文件生成我们的目标文件

获取mp3开始时间(要剪切的开始时间)所在的文件字节位置及结束时间所在文件的字节位置

根据开始时间的字节位置和结束时间的字节位置结合源文件生成我们的目标文件

3

mp3剪切技术实现

那么如何来获取mp3开始时间所在文件的字节位置呢? 这里用到了jaudiotagger.jar。

http://www.jthink.net/jaudiotagger/

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

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元数据
MP3Filemp3 = new MP3File( this.mp3File);
//获取mp3的元数据
MP3AudioHeaderheader = ( MP3AudioHeader) mp3.getAudioHeader();
2. 根据元数据获取mp3比特率
//根据元数据获取比特率
longbitRateKbps = header.getBitRateAsNumber();
可能你会问,什么是比特率?
比特率是每秒传输的比特(bit)数
比特率是每秒传输的比特(bit)数
来看我们取mp3比特率的方法看注释
long bitRate = header.getBitRateAsNumber();看该方法源码注释如下:
/**
*
* @ returnbitrate inkbps, no indicator isprovided asto
* whether ornotit isvbr
*/
public long getBitRateAsNumber()
{
returnbitrate;
}

通过注释得知,此方法返回的比特率单位为kbps(每秒千字节) ,而我们需要的比特率的单位是(每毫秒位),下一步进行单位转换计算。

3. 转换比特率

这里我们需要换算它为每毫秒位数,1字节是8位,1秒是1000毫秒,千字节是1024字节,那么转换后算到的也就是getBitRateAsNumber() *1024L / 8L / 1000L。代码如下:

//计算出开始字节位置
longbitRatebpm = bitRateKbps * 1024L/ 8L/ 1000L* beginTime;
4.计算开始字节
这个值就是开始时间所在文件的字节位置吗?当然不是,我们的mp3文件当中并不只包含音乐的数据,还包含有音乐的信息头数据。
同样我们可以从头信息中取到我们的mp3首帧字节位置。首帧字节位置+每毫秒位为单位比特率,就是我们要的mp3开始字节位置了。
代码如下:
longfirstFrameByte = header.getMp3StartByte();
longbeginByte = firstFrameByte + beginBitRateBpm;
5. 计算结束字节位置
同理, 利用上面计算出来的开始字节beginType+时间差(剪切结束时间-开始时间)的比特率(单位为每毫秒位)就可以计算出结束的字节位置了,代码入下:
//计算出结束字节位置
longendByte = beginByte + convertKbpsToBpm(bitRateKbps) * (endTime - beginTime);
long endIndex(截取结束字节位置) = beginIndex(截取开始字节位置) + bitRate *1024L / 8L / 1000L(比特率每毫秒位) * (endTime - beginTime)(截取的时长毫秒单位);
二、 有了开始时间的字节位置和结束时间的字节位置,那我们就可以结合源文件生成我们的目标文件拉。
读写文件我们可以使用RandomAccessFile实现随机的读写操作,通过RandomAccessFile.seek()方法调到指定位置。
问题&解决方案
如果我们要操作的mp3文件很大,比如我们截取的字节大小为100MB,这时候我们的app就会因为OOM直接crash掉了。
这里我的解决方案是通过一个缓存数组来限制每次读写的数据大小,每次操作指定大小的数据,这样无论文件多大,我们都不会出现OOM问题啦。
1.首先我们写一个工具方法,以缓存的方式来生成目标文件,源文件读取指定大小的数据读取写入到目标文件,代码如下:
/**
* @paramtargetFile 输出的文件
* @paramsourceFile 读取的文件
* @parambuffer 输入输出的缓存容器
* @paramoffset 读入文件时seek的偏移值
*/
privatestaticvoidwriteSourceToTargetFile(RandomAccessFile targetFile,
RandomAccessFile sourceFile,
bytebuffer[], longoffset)throwsException {
sourceFile.seek(offset);
sourceFile.read(buffer);
longfileLength = targetFile.length();
// 将写文件指针移到文件尾。
targetFile.seek(fileLength);
targetFile.write(buffer);
}
2. 需要根据需要剪切文件的字节大小,分别考虑小于缓存以及大于等于缓存的情况,分别进行操作。
代码如下:
privatestaticvoidwriteSourceToTargetFileWithBuffer(
RandomAccessFile targetFile,
RandomAccessFile sourceFile,
longtotalSize,
longoffset)throwsException {
//缓存大小,每次写入指定数据防止内存泄漏
intbuffersize = BUFFER_SIZE;
longcount = totalSize / buffersize;
if(count <= 1) {
//文件总长度小于小于缓存大小情况
writeSourceToTargetFile(targetFile, sourceFile, newbyte[( int) totalSize], offset);
} else{
//计算出整除后剩余的数据数
longremainSize = totalSize % buffersize;
bytedata[] = newbyte[buffersize];
//读入文件时seek的偏移量
for( inti = 0; i < count; i++) {
writeSourceToTargetFile(targetFile, sourceFile, data, offset);
offset += BUFFER_SIZE;
}
//写入剩余数据
if(remainSize > 0) {
writeSourceToTargetFile(targetFile, sourceFile, newbyte[( int) remainSize], offset);
}
}
}
3. 最后要考虑不但要讲mp3乐音帧相关数据写入, 还要讲头信息写入进去,代码如下:
/**
* 生成目标mp3文件
*
* @paramtargetFile
* @parambeginByte
* @paramendByte
* @paramfirstFrameByte
* @throwsException
*/
privatevoidgenerateTargetMp3File(RandomAccessFile targetFile,
longbeginByte, longendByte, longfirstFrameByte)throwsException {
RandomAccessFile sourceFile = newRandomAccessFile(mSourceMp3File, "rw");
try{
//write mp3 header info
writeSourceToTargetFileWithBuffer(targetFile, sourceFile, firstFrameByte, 0);
//write mp3 frame info
intsize = ( int) (endByte - beginByte);
writeSourceToTargetFileWithBuffer(targetFile, sourceFile, size, beginByte);
} catch(Exception e) {
e.printStackTrace();
} finally{
if(sourceFile != null)
sourceFile.close();
}
}
到这里就结束啦,能力有限,写的不对好的地方,请多提意见。
项目计划讲一直进行维护升级,谢谢您的关注!!!
https://github.com/zyl409214686/Mp3Cutter
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
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为剪切的开始时间及结束时间
laozi.mp3是源mp3
test.mp3是运行完单元测试,生成的mp3文件。
startTime、endTime为剪切的开始时间及结束时间
//计算出开始字节位置
longbitRatebpm = bitRateKbps * 1024L/ 8L/ 1000L* beginTime;
4.计算开始字节
这个值就是开始时间所在文件的字节位置吗?当然不是,我们的mp3文件当中并不只包含音乐的数据,还包含有音乐的信息头数据。
同样我们可以从头信息中取到我们的mp3首帧字节位置。首帧字节位置+每毫秒位为单位比特率,就是我们要的mp3开始字节位置了。
代码如下:
longfirstFrameByte = header.getMp3StartByte();
longbeginByte = firstFrameByte + beginBitRateBpm;
5. 计算结束字节位置
同理, 利用上面计算出来的开始字节beginType+时间差(剪切结束时间-开始时间)的比特率(单位为每毫秒位)就可以计算出结束的字节位置了,代码入下:
//计算出结束字节位置
longendByte = beginByte + convertKbpsToBpm(bitRateKbps) * (endTime - beginTime);
long endIndex(截取结束字节位置) = beginIndex(截取开始字节位置) + bitRate *1024L / 8L / 1000L(比特率每毫秒位) * (endTime - beginTime)(截取的时长毫秒单位);
二、 有了开始时间的字节位置和结束时间的字节位置,那我们就可以结合源文件生成我们的目标文件拉。
读写文件我们可以使用RandomAccessFile实现随机的读写操作,通过RandomAccessFile.seek()方法调到指定位置。
问题&解决方案
如果我们要操作的mp3文件很大,比如我们截取的字节大小为100MB,这时候我们的app就会因为OOM直接crash掉了。
这里我的解决方案是通过一个缓存数组来限制每次读写的数据大小,每次操作指定大小的数据,这样无论文件多大,我们都不会出现OOM问题啦。
1.首先我们写一个工具方法,以缓存的方式来生成目标文件,源文件读取指定大小的数据读取写入到目标文件,代码如下:
/**
* @paramtargetFile 输出的文件
* @paramsourceFile 读取的文件
* @parambuffer 输入输出的缓存容器
* @paramoffset 读入文件时seek的偏移值
*/
privatestaticvoidwriteSourceToTargetFile(RandomAccessFile targetFile,
RandomAccessFile sourceFile,
bytebuffer[], longoffset)throwsException {
sourceFile.seek(offset);
sourceFile.read(buffer);
longfileLength = targetFile.length();
// 将写文件指针移到文件尾。
targetFile.seek(fileLength);
targetFile.write(buffer);
}
2. 需要根据需要剪切文件的字节大小,分别考虑小于缓存以及大于等于缓存的情况,分别进行操作。
代码如下:
privatestaticvoidwriteSourceToTargetFileWithBuffer(
RandomAccessFile targetFile,
RandomAccessFile sourceFile,
longtotalSize,
longoffset)throwsException {
//缓存大小,每次写入指定数据防止内存泄漏
intbuffersize = BUFFER_SIZE;
longcount = totalSize / buffersize;
if(count <= 1) {
//文件总长度小于小于缓存大小情况
writeSourceToTargetFile(targetFile, sourceFile, newbyte[( int) totalSize], offset);
} else{
//计算出整除后剩余的数据数
longremainSize = totalSize % buffersize;
bytedata[] = newbyte[buffersize];
//读入文件时seek的偏移量
for( inti = 0; i < count; i++) {
writeSourceToTargetFile(targetFile, sourceFile, data, offset);
offset += BUFFER_SIZE;
}
//写入剩余数据
if(remainSize > 0) {
writeSourceToTargetFile(targetFile, sourceFile, newbyte[( int) remainSize], offset);
}
}
}
3. 最后要考虑不但要讲mp3乐音帧相关数据写入, 还要讲头信息写入进去,代码如下:
/**
* 生成目标mp3文件
*
* @paramtargetFile
* @parambeginByte
* @paramendByte
* @paramfirstFrameByte
* @throwsException
*/
privatevoidgenerateTargetMp3File(RandomAccessFile targetFile,
longbeginByte, longendByte, longfirstFrameByte)throwsException {
RandomAccessFile sourceFile = newRandomAccessFile(mSourceMp3File, "rw");
try{
//write mp3 header info
writeSourceToTargetFileWithBuffer(targetFile, sourceFile, firstFrameByte, 0);
//write mp3 frame info
intsize = ( int) (endByte - beginByte);
writeSourceToTargetFileWithBuffer(targetFile, sourceFile, size, beginByte);
} catch(Exception e) {
e.printStackTrace();
} finally{
if(sourceFile != null)
sourceFile.close();
}
}
到这里就结束啦,能力有限,写的不对好的地方,请多提意见。
项目计划讲一直进行维护升级,谢谢您的关注!!!
https://github.com/zyl409214686/Mp3Cutter
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
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为剪切的开始时间及结束时间
laozi.mp3是源mp3
test.mp3是运行完单元测试,生成的mp3文件。
startTime、endTime为剪切的开始时间及结束时间

android MediaRecorder 生成mp3 mp3 editor on android_数据_03