国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

android音頻編輯之音頻裁剪的示例代碼

2019-10-22 18:16:51
字體:
來源:轉載
供稿:網友

前言

本篇開始講解音頻編輯的具體操作,從相對簡單的音頻裁剪開始。要進行音頻裁剪,我的方案是開啟一個Service服務用于音頻裁剪的耗時操作,主界面發送裁剪命令,同時注冊EventBus接受裁剪的消息(當然也可以使用廣播接受的方式)。因此,在本篇主要會講解以下內容:

  1. 音頻編輯項目的整體結構
  2. 音頻裁剪方法的流程實現
  3. 獲取音頻文件相關信息
  4. 計算裁剪時間點對應文件中數據的位置
  5. 寫入wav文件頭信息
  6. 寫入wav文件裁剪部分的音頻數據

下面是音頻裁剪效果圖:

android,音頻裁剪,音頻編輯

音頻編輯項目的整體結構

該音頻測試項目的結構其實很簡單,大致就是以Fragment為基礎的各個界面,以IntentService為基礎的后臺服務,以及最重要的音頻編輯工具類實現。大致結構如下:

  1. CutFragment,裁剪頁面。選擇音頻,裁剪音頻,播放裁剪后的音頻,同時注冊了EventBus以便接受后臺音頻編輯操作發送的消息進行更新。
  2. AudioTaskService,音頻編輯服務Service。繼承自IntentService,可以在后臺任務的線程中執行耗時音頻編輯操作。
  3. AudioTaskCreator,音頻編輯任務命令發送器。通過它可以啟動音頻編輯服務AudioTaskService,并發送具體的編輯操作給它。
  4. AudioTaskHandler,音頻編輯任務處理器。AudioTaskService接受到的intent任務都交給它去處理。這里具體處理裁剪,合成等操作。
  5. AudioEditUtil, 音頻編輯工具類。提供裁剪,合成等音頻編輯的方法。
  6. 另外還有其他相關的音頻工具類。

現在我們看看它們之間的主要流程實現:

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/197217.html">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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VEVB武林網。


注:相關教程知識閱讀請移步到Android開發頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 城市| 沾益县| 平度市| 新河县| 大方县| 祁东县| 普陀区| 什邡市| 锦州市| 嵩明县| 武威市| 合水县| 甘洛县| 个旧市| 敦化市| 扎赉特旗| 磴口县| 公主岭市| 涞源县| 界首市| 辉县市| 台州市| 陇川县| 彰化市| 汉阴县| 永吉县| 乳山市| 邹平县| 含山县| 怀仁县| 金湖县| 兴国县| 盐城市| 灵台县| 鲜城| 鄂托克前旗| 旌德县| 青海省| 衢州市| 江陵县| 江西省|