转载请标明出处:
http://blog.csdn.net/hesong1120/article/details/79077013
本文出自:hesong的专栏

前言

本篇开始讲解音频编辑的具体操作,从相对简单的音频裁剪开始。要进行音频裁剪,我的方案是开启一个Service服务用于音频裁剪的耗时操作,主界面发送裁剪命令,同时注册EventBus接受裁剪的消息(当然也可以使用广播接受的方式)。因此,在本篇主要会讲解以下内容:
- 音频编辑项目的整体结构
- 音频裁剪方法的流程实现
- 获取音频文件相关信息
- 计算裁剪时间点对应文件中数据的位置
- 写入wav文件头信息
- 写入wav文件裁剪部分的音频数据

下面是音频裁剪效果图:

音频编辑项目的整体结构

该音频测试项目的结构其实很简单,大致就是以Fragment为基础的各个界面,以IntentService为基础的后台服务,以及最重要的音频编辑工具类实现。大致结构如下:
- CutFragment,裁剪页面。选择音频,裁剪音频,播放裁剪后的音频,同时注册了EventBus以便接受后台音频编辑操作发送的消息进行更新。
- AudioTaskService,音频编辑服务Service。继承自IntentService,可以在后台任务的线程中执行耗时音频编辑操作。
- AudioTaskCreator,音频编辑任务命令发送器。通过它可以启动音频编辑服务AudioTaskService,并发送具体的编辑操作给它。
- AudioTaskHandler,音频编辑任务处理器。AudioTaskService接受到的intent任务都交给它去处理。这里具体处理裁剪,合成等操作。
- AudioEditUtil, 音频编辑工具类。提供裁剪,合成等音频编辑的方法。
- 另外还有其他相关的音频工具类。

现在我们看看它们之间的主要流程实现:

CutFragment发起音频裁剪任务,同时接收更新音频编辑消息

public class CutFragment extends Fragment {.../*** 裁剪音频*/private void cutAudio() {String path1 = tvAudioPath1.getText().toString();if(TextUtils.isEmpty(path1)){ToastUtil.showToast("音频路径为空");return;}float startTime = Float.valueOf(etStartTime.getText().toString());float endTime = Float.valueOf(etEndTime.getText().toString());if(startTime <= 0){ToastUtil.showToast("时间不对");return;}if(endTime <= 0){ToastUtil.showToast("时间不对");return;}if(startTime >= endTime){ToastUtil.showToast("时间不对");return;}//调用AudioTaskCreator发起音频裁剪任务AudioTaskCreator.createCutAudioTask(getContext(), path1, startTime, endTime);}/*** 接收并更新裁剪消息*/@Subscribe(threadMode = ThreadMode.MAIN) public void onReceiveAudioMsg(AudioMsg msg) {if(msg != null && !TextUtils.isEmpty(msg.msg)){tvMsgInfo.setText(msg.msg);mCurPath = msg.path;}}}

AudioTaskCreator启动音频裁剪任务AudioTaskService

public class AudioTaskCreator {.../*** 启动音频裁剪任务* @param context* @param path*/public static void createCutAudioTask(Context context, String path, float startTime, float endTime){Intent intent = new Intent(context, AudioTaskService.class);intent.setAction(ACTION_AUDIO_CUT);intent.putExtra(PATH_1, path);intent.putExtra(START_TIME, startTime);intent.putExtra(END_TIME, endTime);context.startService(intent);}}

AudioTaskService服务将接受的Intent任务交给AudioTaskHandler处理

/*** 执行后台任务的服务*/
public class AudioTaskService extends IntentService {private AudioTaskHandler mTaskHandler;public AudioTaskService() {super("AudioTaskService");}@Override public void onCreate() {super.onCreate();mTaskHandler = new AudioTaskHandler();}/*** 实现异步任务的方法** @param intent Activity传递过来的Intent,数据封装在intent中*/@Override protected void onHandleIntent(Intent intent) {if (mTaskHandler != null) {mTaskHandler.handleIntent(intent);}}
}

AudioTaskService服务将接受的Intent任务交给AudioTaskHandler处理,根据不同的Intent action,调用不同的处理方法

/*** */
public class AudioTaskHandler {public void handleIntent(Intent intent){if(intent == null){return;}String action = intent.getAction();switch (action){case AudioTaskCreator.ACTION_AUDIO_CUT:{//裁剪String path = intent.getStringExtra(AudioTaskCreator.PATH_1);float startTime = intent.getFloatExtra(AudioTaskCreator.START_TIME, 0);float endTime = intent.getFloatExtra(AudioTaskCreator.END_TIME, 0);cutAudio(path, startTime, endTime);}break;//其他编辑任务...default:break;}}/*** 裁剪音频* @param srcPath 源音频路径* @param startTime 裁剪开始时间* @param endTime 裁剪结束时间*/private void cutAudio(String srcPath, float startTime, float endTime){//具体裁剪操作}}

音频裁剪方法的实现

接下来是音频裁剪的具体操作。还记得上一篇文章说的,音频的裁剪操作都是要基于PCM文件或者WAV文件上进行的,所以对于一般的音频文件都是需要先解码得到PCM文件或者WAV文件,才能进行具体的音频编辑操作。因此音频裁剪操作需要经历以下步骤:
1. 计算解码后的wav音频路径
2. 对源音频进行解码,得到解码后源WAV文件
3. 创建源wav文件和目标WAV音频频的RandomAccessFile,以便对它们后面对它们进行读写操作
4. 根据采样率,声道数,采样位数,和当前时间,计算开始时间和结束时间对应到源文件的具体位置
5. 根据采样率,声道数,采样位数,裁剪音频数据大小等,计算得到wav head文件头byte数据
6. 将wav head文件头byte数据写入到目标文件中
7. 将源文件的开始位置到结束位置的数据复制到目标文件中
8. 删除源wav文件,重命名目标wav文件为源wav文件,即得到最终裁剪后的wav文件

如下,对源音频进行解码,得到解码后的音频文件,然后根据解码音频文件得到Audio音频相关信息,里面记录音频相关的信息如采样率,声道数,采样位数等。

/*** */
public class AudioTaskHandler {/*** 裁剪音频* @param srcPath 源音频路径* @param startTime 裁剪开始时间* @param endTime 裁剪结束时间*/private void cutAudio(String srcPath, float startTime, float endTime){String fileName = new File(srcPath).getName();String nameNoSuffix = fileName.substring(0, fileName.lastIndexOf('.'));fileName = nameNoSuffix + Constant.SUFFIX_WAV;String outName = nameNoSuffix + "_cut.wav";//裁剪后音频的路径String destPath = FileUtils.getAudioEditStorageDirectory() + File.separator + outName;//解码源音频,得到解码后的文件decodeAudio(srcPath, destPath);if(!FileUtils.checkFileExist(destPath)){ToastUtil.showToast("解码失败" + destPath);return;}//获取根据解码后的文件得到audio数据Audio audio = getAudioFromPath(destPath);//裁剪操作if(audio != null){AudioEditUtil.cutAudio(audio, startTime, endTime);}//裁剪完成,通知消息String msg = "裁剪完成";EventBus.getDefault().post(new AudioMsg(AudioTaskCreator.ACTION_AUDIO_CUT, destPath, msg));}/*** 获取根据解码后的文件得到audio数据* @param path* @return*/private Audio getAudioFromPath(String path){if(!FileUtils.checkFileExist(path)){return null;}if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {try {Audio audio = Audio.createAudioFromFile(new File(path));return audio;} catch (Exception e) {e.printStackTrace();}}return null;}}

获取音频文件相关信息

而获取Audio信息其实就是解码时获取MediaFormat,然后获取音频相关的信息的。

/*** 音频信息*/
public class Audio {private String path;private String name;private float volume = 1f;private int channel = 2;private int sampleRate = 44100;private int bitNum = 16;private int timeMillis;...@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) public static Audio createAudioFromFile(File inputFile) throws Exception {MediaExtractor extractor = new MediaExtractor();MediaFormat format = null;int i;try {extractor.setDataSource(inputFile.getPath());}catch (Exception ex){ex.printStackTrace();extractor.setDataSource(new FileInputStream(inputFile).getFD());}int numTracks = extractor.getTrackCount();for (i = 0; i < numTracks; i++) {format = extractor.getTrackFormat(i);if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {extractor.selectTrack(i);break;}}if (i == numTracks) {throw new Exception("No audio track found in " + inputFile);}Audio audio = new Audio();audio.name = inputFile.getName();audio.path = inputFile.getAbsolutePath();audio.sampleRate = format.containsKey(MediaFormat.KEY_SAMPLE_RATE) ? format.getInteger(MediaFormat.KEY_SAMPLE_RATE) : 44100;audio.channel = format.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ? format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) : 1;audio.timeMillis = (int) ((format.getLong(MediaFormat.KEY_DURATION) / 1000.f));//根据pcmEncoding编码格式,得到采样精度,MediaFormat.KEY_PCM_ENCODING这个值不一定有int pcmEncoding = format.containsKey(MediaFormat.KEY_PCM_ENCODING) ? format.getInteger(MediaFormat.KEY_PCM_ENCODING) : AudioFormat.ENCODING_PCM_16BIT;switch (pcmEncoding){case AudioFormat.ENCODING_PCM_FLOAT:audio.bitNum = 32;break;case AudioFormat.ENCODING_PCM_8BIT:audio.bitNum = 8;break;case AudioFormat.ENCODING_PCM_16BIT:default:audio.bitNum = 16;break;}extractor.release();return audio;}}

这里要注意,通过MediaFormat获取音频信息的时候,获取采样位数是要先查找MediaFormat.KEY_PCM_ENCODING这个key对应的值,如果是AudioFormat.ENCODING_PCM_8BIT,则是8位采样精度,如果是AudioFormat.ENCODING_PCM_16BIT,则是16位采样精度,如果是AudioFormat.ENCODING_PCM_FLOAT(android 5.0 版本新增的类型),则是32位采样精度。当然可能MediaFormat中没有包含MediaFormat.KEY_PCM_ENCODING这个key信息,这时就使用默认的AudioFormat.ENCODING_PCM_16BIT,即默认的16位采样精度(也可以说2个字节作为一个采样点编码)。

接下来就是真正的裁剪操作了。根据audio中的音频信息得到将要写入的wav文件头信息字节数据,创建随机读写文件,写入文件头数据,然后源随机读写文件移动到指定的开始时间开始读取,目标随机读写文件将读取的数据写入,知道源随机文件读到指定的结束时间停止,这样就完成了音频文件的裁剪操作。

public class AudioEditUtil {/*** 裁剪音频* @param audio 音频信息* @param cutStartTime 裁剪开始时间* @param cutEndTime 裁剪结束时间*/public static void cutAudio(Audio audio, float cutStartTime, float cutEndTime){if(cutStartTime == 0 && cutEndTime == audio.getTimeMillis() / 1000f){return;}if(cutStartTime >= cutEndTime){return;}String srcWavePath = audio.getPath();int sampleRate = audio.getSampleRate();int channels = audio.getChannel();int bitNum = audio.getBitNum();RandomAccessFile srcFis = null;RandomAccessFile newFos = null;String tempOutPath = srcWavePath + ".temp";try {//创建输入流srcFis = new RandomAccessFile(srcWavePath, "rw");newFos = new RandomAccessFile(tempOutPath, "rw");//源文件开始读取位置,结束读取文件,读取数据的大小final int cutStartPos = getPositionFromWave(cutStartTime, sampleRate, channels, bitNum);final int cutEndPos = getPositionFromWave(cutEndTime, sampleRate, channels, bitNum);final int contentSize = cutEndPos - cutStartPos;//复制wav head 字节数据byte[] headerData = AudioEncodeUtil.getWaveHeader(contentSize, sampleRate, channels, bitNum);copyHeadData(headerData, newFos);//移动到文件开始读取处srcFis.seek(WAVE_HEAD_SIZE + cutStartPos);//复制裁剪的音频数据copyData(srcFis, newFos, contentSize);} catch (Exception e) {e.printStackTrace();return;}finally {//关闭输入流if(srcFis != null){try {srcFis.close();} catch (IOException e) {e.printStackTrace();}}if(newFos != null){try {newFos.close();} catch (IOException e) {e.printStackTrace();}}}// 删除源文件,new File(srcWavePath).delete();//重命名为源文件FileUtils.renameFile(new File(tempOutPath), audio.getPath());}
}

计算裁剪时间点对应文件中数据的位置

需要注意的是根据时间计算在文件中的位置,它是这么实现的:

  /*** 获取wave文件某个时间对应的数据位置* @param time 时间* @param sampleRate 采样率* @param channels 声道数* @param bitNum 采样位数* @return*/private static int getPositionFromWave(float time, int sampleRate, int channels, int bitNum) {int byteNum = bitNum / 8;int position = (int) (time * sampleRate * channels * byteNum);//这里要特别注意,要取整(byteNum * channels)的倍数position = position / (byteNum * channels) * (byteNum * channels);return position;}

这里要特别注意,因为time是个float的数,所以计算后的position取整它并不一定是(byteNum * channels)的倍数,而position的位置必须要是(byteNum * channels)的倍数,否则后面的音频数据就全部乱了,那么在播放时就是撒撒撒撒的噪音,而不是原来的声音了。原因是音频数据是按照一个个采样点来计算的,一个采样点的大小就是(byteNum * channels),所以要取(byteNum * channels)的整数倍。

写入wav文件头信息

接着看看往新文件写入wav文件头是怎么实现的,这个在上一篇中也是有讲过的,不过还是列出来吧:

  /*** 获取Wav header 字节数据* @param totalAudioLen 整个音频PCM数据大小* @param sampleRate 采样率* @param channels 声道数* @param bitNum 采样位数* @throws IOException*/public static byte[] getWaveHeader(long totalAudioLen, int sampleRate, int channels, int bitNum) throws IOException {//总大小,由于不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM文件大小long totalDataLen = totalAudioLen + 36;//采样字节byte率long byteRate = sampleRate * channels * bitNum / 8;byte[] header = new byte[44];header[0] = 'R'; // RIFFheader[1] = 'I';header[2] = 'F';header[3] = 'F';header[4] = (byte) (totalDataLen & 0xff);//数据大小header[5] = (byte) ((totalDataLen >> 8) & 0xff);header[6] = (byte) ((totalDataLen >> 16) & 0xff);header[7] = (byte) ((totalDataLen >> 24) & 0xff);header[8] = 'W';//WAVEheader[9] = 'A';header[10] = 'V';header[11] = 'E';//FMT Chunkheader[12] = 'f'; // 'fmt 'header[13] = 'm';header[14] = 't';header[15] = ' ';//过渡字节//数据大小header[16] = 16; // 4 bytes: size of 'fmt ' chunkheader[17] = 0;header[18] = 0;header[19] = 0;//编码方式 10H为PCM编码格式header[20] = 1; // format = 1header[21] = 0;//通道数header[22] = (byte) channels;header[23] = 0;//采样率,每个通道的播放速度header[24] = (byte) (sampleRate & 0xff);header[25] = (byte) ((sampleRate >> 8) & 0xff);header[26] = (byte) ((sampleRate >> 16) & 0xff);header[27] = (byte) ((sampleRate >> 24) & 0xff);//音频数据传送速率,采样率*通道数*采样深度/8header[28] = (byte) (byteRate & 0xff);header[29] = (byte) ((byteRate >> 8) & 0xff);header[30] = (byte) ((byteRate >> 16) & 0xff);header[31] = (byte) ((byteRate >> 24) & 0xff);// 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数header[32] = (byte) (channels * 16 / 8);header[33] = 0;//每个样本的数据位数header[34] = 16;header[35] = 0;//Data chunkheader[36] = 'd';//dataheader[37] = 'a';header[38] = 't';header[39] = 'a';header[40] = (byte) (totalAudioLen & 0xff);header[41] = (byte) ((totalAudioLen >> 8) & 0xff);header[42] = (byte) ((totalAudioLen >> 16) & 0xff);header[43] = (byte) ((totalAudioLen >> 24) & 0xff);return header;}

这里比上一篇中精简了一些,只要传入音频数据大小,采样率,声道数,采样位数这四个参数,就可以得到wav文件头信息了,然后再将它写入到wav文件开始处。

/*** 复制wav header 数据** @param headerData wav header 数据* @param fos 目标输出流*/private static void copyHeadData(byte[] headerData, RandomAccessFile fos) {try {fos.seek(0);fos.write(headerData);} catch (Exception ex) {ex.printStackTrace();}}

写入wav文件裁剪部分的音频数据

接下来就是将裁剪部分的音频数据写入到文件中了。这里要先移动源文件的读取位置到裁剪起始处,即

//移动到文件开始读取处
srcFis.seek(WAVE_HEAD_SIZE + cutStartPos);

这样就可以从源文件读取裁剪处的数据了

  /*** 复制数据** @param fis 源输入流* @param fos 目标输出流* @param cooySize 复制大小*/private static void copyData(RandomAccessFile fis, RandomAccessFile fos, final int cooySize) {byte[] buffer = new byte[2048];int length;int totalReadLength = 0;try {while ((length = fis.read(buffer)) != -1) {fos.write(buffer, 0, length);totalReadLength += length;int remainSize = cooySize - totalReadLength;if (remainSize <= 0) {//读取指定位置完成break;} else if (remainSize < buffer.length) {//离指定位置的大小小于buffer的大小,换remainSize的bufferbuffer = new byte[remainSize];}}} catch (Exception ex) {ex.printStackTrace();}}

上面代码目的就是读取startPos开始,到startPos+copySize之间的数据。

总结

到这里的话,想必对裁剪的整体流程有一定的了解了,总结起来的话,首先是对音频解码,得到解码后的wav文件或者pcm文件,然后取得音频的文件头信息(包括采样率,声道数,采样位数,时间等),然后计算得到裁剪时间对应到文件中数据位置,以及裁剪的数据大小,然后计算得到裁剪后的wav文件头信息,并写入新文件中,最后将源文件裁剪部分的数据写入到新文件中,最终得到裁剪后的wav文件了。

读者可能会有疑问,我想要裁剪的是mp3文件,这里只是得到裁剪后的wav文件,那怎么得到裁剪后的mp3文件呢?这个就需要对该wav文件进行mp3编码压缩了,具体实现可以参考我的Github项目 AudioEdit

我的博客
GitHub
我的简书
群号:194118438,欢迎入群
微信公众号 hesong ,微信扫一扫下方二维码即可关注:

android音频编辑之音频裁剪相关推荐

  1. 【SeeMusic】音频编辑 ( 进入音频编辑页面 | 音频延迟设置 )

    SeeMusic 系列文章目录 [SeeMusic]下载安装并注册 SeeMusic 软件 [SeeMusic]创建 SeeMusic 工程并编辑相关内容 ( 创建工程 | 导入 MIDI 文件 | ...

  2. android音频编辑之音频裁剪的示例代码

    音频编辑项目的整体结构 音频裁剪方法的流程实现 获取音频文件相关信息 计算裁剪时间点对应文件中数据的位置 写入wav文件头信息 写入wav文件裁剪部分的音频数据 下面是音频裁剪效果图: 音频编辑项目的 ...

  3. android音频资源,android音频编辑之音频裁剪的示例代码

    前言 本篇开始讲解音频编辑的具体操作,从相对简单的音频裁剪开始.要进行音频裁剪,我的方案是开启一个Service服务用于音频裁剪的耗时操作,主界面发送裁剪命令,同时注册EventBus接受裁剪的消息( ...

  4. android音频编辑之音频转换PCM与WAV

    前言 本篇开始讲解在Android平台上进行的音频编辑开发,首先需要对音频相关概念有基础的认识.所以本篇要讲解以下内容: 常用音频格式简介 WAV和PCM的区别和联系 WAV文件头信息 采样率简介 声 ...

  5. 原 android音频编辑之音频转换PCM与WAV

    http://blog.csdn.net/hesong1120/article/details/79043482 本文出自: hesong的专栏 前言 本篇开始讲解在Android平台上进行的音频编辑 ...

  6. android 视频编辑框架(分割,裁剪,旋转,合并,添加logo,背景音乐等等)

    EpMedia Android上基于FFmpeg开发的视频处理框架,简单易用,体积小,帮助使用者快速实现视频处理功能.包含以下功能:剪辑,裁剪,旋转,镜像,合并,分离,变速,添加LOGO,添加滤镜,添 ...

  7. android音频编辑之音频合成

    前言 音频编辑系列: android音频编辑之音频转换PCM与WAV android音频编辑之音频裁剪 android音频编辑之音频合成 本篇主要讲解音频PCM数据的合成,这里合成包括音频之间的拼接, ...

  8. android声音播放函数双声道合并,Android音频编辑之音频合成功能

    前言 音频编辑系列: 本篇主要讲解音频PCM数据的合成,这里合成包括音频之间的拼接,混合. - 音频拼接:一段音频连接着另一段音频,两段音频不会同时播放,有先后顺序. - 音频混合:一段音频和另一段音 ...

  9. 给App加上音频编辑功能,让你的用户Show起来

    如今短视频当道,BGM无处不在,用户在每个能秀的地方都想要加上个性表达的音频.作为一个开发者,需不断探索和迎合用户的行为喜好,音频编辑功能成为用户在编辑个人信息.内容创作.生活分享等场景下的必需品. ...

最新文章

  1. 在Ubuntu 14.04 64bit上搭建单机本地节点Spark 1.3.0环境
  2. 选择、分组、引用,指定匹配的位置
  3. Android之利用回调函数onCreateDialog实现加载对话框
  4. Mininet 系列实验(一)
  5. 快为网易云官网出谋划策,5步轻松获得网易味央猪肉!
  6. android实现重复动画,android – 多次重复AnimatorSet动画
  7. Java容器 | 基于源码分析List集合体系
  8. 07-异常处理——动手动脑
  9. x86伺服器 走向虛擬化
  10. Could not find an NgModule. Use the skip-import option to skip importing in NgModule.
  11. 使用 Kotlin , Groovy ,Java 开发一个自己的 Gradle 插件
  12. OpenExplorer For Eclipse
  13. linux war 权限,Linux中mv重命名作用及打包war压缩文件及分配权限
  14. 设备性能测试 : 内存带宽的测试
  15. Gluster FS 部署复制卷与常用命令 常见问题解决
  16. 大陆首款车量AI芯片 开启国产替代新纪元
  17. 10040---微信与朋友圈后台架构
  18. 基于Android+SpringBoot+MySQL的外卖APP系统设计与实现
  19. 如何利用原生JS实现回到顶部以及吸顶效果
  20. Nginx总结(反向代理、负载均衡、动静分离)篇

热门文章

  1. 使用ROS驱动激光雷达YDLIDAR-G4的详细过程总结(教你避开使用的各种坑....)
  2. 为什么有些大公司技术弱爆了?
  3. vue-cli、脚手架创建、eslint、alias别名配置、proxy代理配置、axios、scoped、穿透、媒体查询、12栅格、动态rem、1px边框、移动端事件、300ms延迟问题(六)
  4. 剖析抖音快速涨粉的文案号,了解大佬运营技巧,学以致用
  5. 从头认识Spring-1.14 SpEl表达式(3)-SpEl表达式的两个坑:Bean的顺序与Bean的toString方法
  6. 关于vue-cli3的浏览器兼容性
  7. 环境工程部门怎么实施自动化软件学习时间更多
  8. 哪家宽带网速是最好最快的?
  9. FOC控制笔记 -基本术语概念
  10. 文明重启战局服务器维护中,王牌战争文明重启8月23日更新公告