android音頻編輯之音頻裁剪的示例代碼
前言
本篇開始講解音頻編輯的具體操作,從相對簡單的音頻裁剪開始。要進(jìn)行音頻裁剪,我的方案是開啟一個(gè)Service服務(wù)用于音頻裁剪的耗時(shí)操作,主界面發(fā)送裁剪命令,同時(shí)注冊EventBus接受裁剪的消息(當(dāng)然也可以使用廣播接受的方式)。因此,在本篇主要會(huì)講解以下內(nèi)容:
- 音頻編輯項(xiàng)目的整體結(jié)構(gòu)
- 音頻裁剪方法的流程實(shí)現(xiàn)
- 獲取音頻文件相關(guān)信息
- 計(jì)算裁剪時(shí)間點(diǎn)對應(yīng)文件中數(shù)據(jù)的位置
- 寫入wav文件頭信息
- 寫入wav文件裁剪部分的音頻數(shù)據(jù)
下面是音頻裁剪效果圖:
音頻編輯項(xiàng)目的整體結(jié)構(gòu)
該音頻測試項(xiàng)目的結(jié)構(gòu)其實(shí)很簡單,大致就是以Fragment為基礎(chǔ)的各個(gè)界面,以IntentService為基礎(chǔ)的后臺(tái)服務(wù),以及最重要的音頻編輯工具類實(shí)現(xiàn)。大致結(jié)構(gòu)如下:
- CutFragment,裁剪頁面。選擇音頻,裁剪音頻,播放裁剪后的音頻,同時(shí)注冊了EventBus以便接受后臺(tái)音頻編輯操作發(fā)送的消息進(jìn)行更新。
- AudioTaskService,音頻編輯服務(wù)Service。繼承自IntentService,可以在后臺(tái)任務(wù)的線程中執(zhí)行耗時(shí)音頻編輯操作。
- AudioTaskCreator,音頻編輯任務(wù)命令發(fā)送器。通過它可以啟動(dòng)音頻編輯服務(wù)AudioTaskService,并發(fā)送具體的編輯操作給它。
- AudioTaskHandler,音頻編輯任務(wù)處理器。AudioTaskService接受到的intent任務(wù)都交給它去處理。這里具體處理裁剪,合成等操作。
- AudioEditUtil, 音頻編輯工具類。提供裁剪,合成等音頻編輯的方法。
- 另外還有其他相關(guān)的音頻工具類。
現(xiàn)在我們看看它們之間的主要流程實(shí)現(xiàn):
CutFragment發(fā)起音頻裁剪任務(wù),同時(shí)接收更新音頻編輯消息
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("時(shí)間不對"); return; } if(endTime <= 0){ ToastUtil.showToast("時(shí)間不對"); return; } if(startTime >= endTime){ ToastUtil.showToast("時(shí)間不對"); return; } //調(diào)用AudioTaskCreator發(fā)起音頻裁剪任務(wù) 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啟動(dòng)音頻裁剪任務(wù)AudioTaskService
public class AudioTaskCreator { ... /** * 啟動(dòng)音頻裁剪任務(wù) * @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服務(wù)將接受的Intent任務(wù)交給AudioTaskHandler處理
/** * 執(zhí)行后臺(tái)任務(wù)的服務(wù) */ public class AudioTaskService extends IntentService { private AudioTaskHandler mTaskHandler; public AudioTaskService() { super("AudioTaskService"); } @Override public void onCreate() { super.onCreate(); mTaskHandler = new AudioTaskHandler(); } /** * 實(shí)現(xiàn)異步任務(wù)的方法 * * @param intent Activity傳遞過來的Intent,數(shù)據(jù)封裝在intent中 */ @Override protected void onHandleIntent(Intent intent) { if (mTaskHandler != null) { mTaskHandler.handleIntent(intent); } } }
AudioTaskService服務(wù)將接受的Intent任務(wù)交給AudioTaskHandler處理,根據(jù)不同的Intent action,調(diào)用不同的處理方法
/** * */ 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; //其他編輯任務(wù) ... default: break; } } /** * 裁剪音頻 * @param srcPath 源音頻路徑 * @param startTime 裁剪開始時(shí)間 * @param endTime 裁剪結(jié)束時(shí)間 */ private void cutAudio(String srcPath, float startTime, float endTime){ //具體裁剪操作 } }
音頻裁剪方法的實(shí)現(xiàn)
接下來是音頻裁剪的具體操作。還記得上一篇文章說的,音頻的裁剪操作都是要基于PCM文件或者WAV文件上進(jìn)行的,所以對于一般的音頻文件都是需要先解碼得到PCM文件或者WAV文件,才能進(jìn)行具體的音頻編輯操作。因此音頻裁剪操作需要經(jīng)歷以下步驟:
- 計(jì)算解碼后的wav音頻路徑
- 對源音頻進(jìn)行解碼,得到解碼后源WAV文件
- 創(chuàng)建源wav文件和目標(biāo)WAV音頻頻的RandomAccessFile,以便對它們后面對它們進(jìn)行讀寫操作
- 根據(jù)采樣率,聲道數(shù),采樣位數(shù),和當(dāng)前時(shí)間,計(jì)算開始時(shí)間和結(jié)束時(shí)間對應(yīng)到源文件的具體位置
- 根據(jù)采樣率,聲道數(shù),采樣位數(shù),裁剪音頻數(shù)據(jù)大小等,計(jì)算得到wav head文件頭byte數(shù)據(jù)
- 將wav head文件頭byte數(shù)據(jù)寫入到目標(biāo)文件中
- 將源文件的開始位置到結(jié)束位置的數(shù)據(jù)復(fù)制到目標(biāo)文件中
- 刪除源wav文件,重命名目標(biāo)wav文件為源wav文件,即得到最終裁剪后的wav文件
如下,對源音頻進(jìn)行解碼,得到解碼后的音頻文件,然后根據(jù)解碼音頻文件得到Audio音頻相關(guān)信息,里面記錄音頻相關(guān)的信息如采樣率,聲道數(shù),采樣位數(shù)等。
/** * */ public class AudioTaskHandler { /** * 裁剪音頻 * @param srcPath 源音頻路徑 * @param startTime 裁剪開始時(shí)間 * @param endTime 裁剪結(jié)束時(shí)間 */ 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; } //獲取根據(jù)解碼后的文件得到audio數(shù)據(jù) 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)); } /** * 獲取根據(jù)解碼后的文件得到audio數(shù)據(jù) * @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; } }
獲取音頻文件相關(guān)信息
而獲取Audio信息其實(shí)就是解碼時(shí)獲取MediaFormat,然后獲取音頻相關(guān)的信息的。
/** * 音頻信息 */ 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)); //根據(jù)pcmEncoding編碼格式,得到采樣精度,MediaFormat.KEY_PCM_ENCODING這個(gè)值不一定有 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獲取音頻信息的時(shí)候,獲取采樣位數(shù)是要先查找MediaFormat.KEY_PCM_ENCODING這個(gè)key對應(yīng)的值,如果是AudioFormat.ENCODING_PCM_8BIT,則是8位采樣精度,如果是AudioFormat.ENCODING_PCM_16BIT,則是16位采樣精度,如果是AudioFormat.ENCODING_PCM_FLOAT(android 5.0 版本新增的類型),則是32位采樣精度。當(dāng)然可能MediaFormat中沒有包含MediaFormat.KEY_PCM_ENCODING這個(gè)key信息,這時(shí)就使用默認(rèn)的AudioFormat.ENCODING_PCM_16BIT,即默認(rèn)的16位采樣精度(也可以說2個(gè)字節(jié)作為一個(gè)采樣點(diǎn)編碼)。
接下來就是真正的裁剪操作了。根據(jù)audio中的音頻信息得到將要寫入的wav文件頭信息字節(jié)數(shù)據(jù),創(chuàng)建隨機(jī)讀寫文件,寫入文件頭數(shù)據(jù),然后源隨機(jī)讀寫文件移動(dòng)到指定的開始時(shí)間開始讀取,目標(biāo)隨機(jī)讀寫文件將讀取的數(shù)據(jù)寫入,知道源隨機(jī)文件讀到指定的結(jié)束時(shí)間停止,這樣就完成了音頻文件的裁剪操作。
public class AudioEditUtil { /** * 裁剪音頻 * @param audio 音頻信息 * @param cutStartTime 裁剪開始時(shí)間 * @param cutEndTime 裁剪結(jié)束時(shí)間 */ 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 { //創(chuàng)建輸入流 srcFis = new RandomAccessFile(srcWavePath, "rw"); newFos = new RandomAccessFile(tempOutPath, "rw"); //源文件開始讀取位置,結(jié)束讀取文件,讀取數(shù)據(jù)的大小 final int cutStartPos = getPositionFromWave(cutStartTime, sampleRate, channels, bitNum); final int cutEndPos = getPositionFromWave(cutEndTime, sampleRate, channels, bitNum); final int contentSize = cutEndPos - cutStartPos; //復(fù)制wav head 字節(jié)數(shù)據(jù) byte[] headerData = AudioEncodeUtil.getWaveHeader(contentSize, sampleRate, channels, bitNum); copyHeadData(headerData, newFos); //移動(dòng)到文件開始讀取處 srcFis.seek(WAVE_HEAD_SIZE + cutStartPos); //復(fù)制裁剪的音頻數(shù)據(jù) copyData(srcFis, newFos, contentSize); } catch (Exception e) { e.printStackTrace(); return; }finally { //關(guān)閉輸入流 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()); } }
計(jì)算裁剪時(shí)間點(diǎn)對應(yīng)文件中數(shù)據(jù)的位置
需要注意的是根據(jù)時(shí)間計(jì)算在文件中的位置,它是這么實(shí)現(xiàn)的:
/** * 獲取wave文件某個(gè)時(shí)間對應(yīng)的數(shù)據(jù)位置 * @param time 時(shí)間 * @param sampleRate 采樣率 * @param channels 聲道數(shù) * @param bitNum 采樣位數(shù) * @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)的倍數(shù) position = position / (byteNum * channels) * (byteNum * channels); return position; }
這里要特別注意,因?yàn)閠ime是個(gè)float的數(shù),所以計(jì)算后的position取整它并不一定是(byteNum * channels)的倍數(shù),而position的位置必須要是(byteNum * channels)的倍數(shù),否則后面的音頻數(shù)據(jù)就全部亂了,那么在播放時(shí)就是撒撒撒撒的噪音,而不是原來的聲音了。原因是音頻數(shù)據(jù)是按照一個(gè)個(gè)采樣點(diǎn)來計(jì)算的,一個(gè)采樣點(diǎn)的大小就是(byteNum * channels),所以要?。╞yteNum * channels)的整數(shù)倍。
寫入wav文件頭信息
接著看看往新文件寫入wav文件頭是怎么實(shí)現(xiàn)的,這個(gè)在上一篇中也是有講過的,不過還是列出來吧:
/** * 獲取Wav header 字節(jié)數(shù)據(jù) * @param totalAudioLen 整個(gè)音頻PCM數(shù)據(jù)大小 * @param sampleRate 采樣率 * @param channels 聲道數(shù) * @param bitNum 采樣位數(shù) * @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; //采樣字節(jié)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);//數(shù)據(jù)大小 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] = ' ';//過渡字節(jié) //數(shù)據(jù)大小 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; //通道數(shù) header[22] = (byte) channels; header[23] = 0; //采樣率,每個(gè)通道的播放速度 header[24] = (byte) (sampleRate & 0xff); header[25] = (byte) ((sampleRate >> 8) & 0xff); header[26] = (byte) ((sampleRate >> 16) & 0xff); header[27] = (byte) ((sampleRate >> 24) & 0xff); //音頻數(shù)據(jù)傳送速率,采樣率*通道數(shù)*采樣深度/8 header[28] = (byte) (byteRate & 0xff); header[29] = (byte) ((byteRate >> 8) & 0xff); header[30] = (byte) ((byteRate >> 16) & 0xff); header[31] = (byte) ((byteRate >> 24) & 0xff); // 確定系統(tǒng)一次要處理多少個(gè)這樣字節(jié)的數(shù)據(jù),確定緩沖區(qū),通道數(shù)*采樣位數(shù) header[32] = (byte) (channels * 16 / 8); header[33] = 0; //每個(gè)樣本的數(shù)據(jù)位數(shù) 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; }
這里比上一篇中精簡了一些,只要傳入音頻數(shù)據(jù)大小,采樣率,聲道數(shù),采樣位數(shù)這四個(gè)參數(shù),就可以得到wav文件頭信息了,然后再將它寫入到wav文件開始處。
/** * 復(fù)制wav header 數(shù)據(jù) * * @param headerData wav header 數(shù)據(jù) * @param fos 目標(biāo)輸出流 */ private static void copyHeadData(byte[] headerData, RandomAccessFile fos) { try { fos.seek(0); fos.write(headerData); } catch (Exception ex) { ex.printStackTrace(); } }
寫入wav文件裁剪部分的音頻數(shù)據(jù)
接下來就是將裁剪部分的音頻數(shù)據(jù)寫入到文件中了。這里要先移動(dòng)源文件的讀取位置到裁剪起始處,即
//移動(dòng)到文件開始讀取處 srcFis.seek(WAVE_HEAD_SIZE + cutStartPos);
這樣就可以從源文件讀取裁剪處的數(shù)據(jù)了
/** * 復(fù)制數(shù)據(jù) * * @param fis 源輸入流 * @param fos 目標(biāo)輸出流 * @param cooySize 復(fù)制大小 */ 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之間的數(shù)據(jù)。
總結(jié)
到這里的話,想必對裁剪的整體流程有一定的了解了,總結(jié)起來的話,首先是對音頻解碼,得到解碼后的wav文件或者pcm文件,然后取得音頻的文件頭信息(包括采樣率,聲道數(shù),采樣位數(shù),時(shí)間等),然后計(jì)算得到裁剪時(shí)間對應(yīng)到文件中數(shù)據(jù)位置,以及裁剪的數(shù)據(jù)大小,然后計(jì)算得到裁剪后的wav文件頭信息,并寫入新文件中,最后將源文件裁剪部分的數(shù)據(jù)寫入到新文件中,最終得到裁剪后的wav文件了。
讀者可能會(huì)有疑問,我想要裁剪的是mp3文件,這里只是得到裁剪后的wav文件,那怎么得到裁剪后的mp3文件呢?這個(gè)就需要對該wav文件進(jìn)行mp3編碼壓縮了,具體實(shí)現(xiàn)可以參考我的Github項(xiàng)目 AudioEdit
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android?nonTransitiveRClass資源沖突問題淺析
這篇文章主要介紹了Android?nonTransitiveRClass資源沖突問題,別搞錯(cuò)了,nonTransitiveRClass不能解決資源沖突,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-12-12詳解 android 光線傳感器 light sensor的使用
這篇文章主要介紹了詳解 android 光線傳感器 light sensor的使用的相關(guān)資料,需要的朋友可以參考下2017-06-06android獲取監(jiān)聽SD Card狀態(tài)的方法
這篇文章主要介紹了android獲取監(jiān)聽SD Card狀態(tài)的方法,涉及Android實(shí)現(xiàn)SD Card監(jiān)聽的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04Flutter如何輕松實(shí)現(xiàn)動(dòng)態(tài)更新ListView淺析
在Android中通常都會(huì)用到listview.那么flutter里面怎么用呢?下面這篇文章主要給大家介紹了關(guān)于Flutter如何輕松實(shí)現(xiàn)動(dòng)態(tài)更新ListView的相關(guān)資料,需要的朋友可以參考下2022-02-02