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

前言

本篇开始讲解音频编辑的具体操作,从相对简单的音频裁剪开始。要进行音频裁剪,我的方案是开启一个Service服务用于音频裁剪的耗时操作,主界面发送裁剪命令,同时注册EventBus接受裁剪的消息(当然也可以使用广播接受的方式)。因此,在本篇主要会讲解以下内容:

音频编辑项目的整体结构

音频裁剪方法的流程实现

获取音频文件相关信息

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

写入wav文件头信息

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

下面是音频裁剪效果图:

0405c9d3f971ee98750981eb461b7281.png

音频编辑项目的整体结构

该音频测试项目的结构其实很简单,大致就是以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文件,才能进行具体的音频编辑操作。因此音频裁剪操作需要经历以下步骤:

计算解码后的wav音频路径

对源音频进行解码,得到解码后源WAV文件

创建源wav文件和目标WAV音频频的RandomAccessFile,以便对它们后面对它们进行读写操作

根据采样率,声道数,采样位数,和当前时间,计算开始时间和结束时间对应到源文件的具体位置

根据采样率,声道数,采样位数,裁剪音频数据大小等,计算得到wav head文件头byte数据

将wav head文件头byte数据写入到目标文件中

将源文件的开始位置到结束位置的数据复制到目标文件中

删除源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'; // RIFF

header[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';//WAVE

header[9] = 'A';

header[10] = 'V';

header[11] = 'E';

//FMT Chunk

header[12] = 'f'; // 'fmt '

header[13] = 'm';

header[14] = 't';

header[15] = ' ';//过渡字节

//数据大小

header[16] = 16; // 4 bytes: size of 'fmt ' chunk

header[17] = 0;

header[18] = 0;

header[19] = 0;

//编码方式 10H为PCM编码格式

header[20] = 1; // format = 1

header[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);

//音频数据传送速率,采样率*通道数*采样深度/8

header[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 chunk

header[36] = 'd';//data

header[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的buffer

buffer = new byte[remainSize];

}

}

} catch (Exception ex) {

ex.printStackTrace();

}

}

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

总结

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值