溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

android中如何實現(xiàn)音頻裁剪

發(fā)布時間:2021-07-21 09:28:53 來源:億速云 閱讀:187 作者:小新 欄目:移動開發(fā)

這篇文章給大家分享的是有關(guān)android中如何實現(xiàn)音頻裁剪的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

下面是音頻裁剪效果圖:

android中如何實現(xiàn)音頻裁剪

音頻編輯項目的整體結(jié)構(gòu)

該音頻測試項目的結(jié)構(gòu)其實很簡單,大致就是以Fragment為基礎(chǔ)的各個界面,以IntentService為基礎(chǔ)的后臺服務(wù),以及最重要的音頻編輯工具類實現(xiàn)。大致結(jié)構(gòu)如下:

  1. CutFragment,裁剪頁面。選擇音頻,裁剪音頻,播放裁剪后的音頻,同時注冊了EventBus以便接受后臺音頻編輯操作發(fā)送的消息進(jìn)行更新。

  2. AudioTaskService,音頻編輯服務(wù)Service。繼承自IntentService,可以在后臺任務(wù)的線程中執(zhí)行耗時音頻編輯操作。

  3. AudioTaskCreator,音頻編輯任務(wù)命令發(fā)送器。通過它可以啟動音頻編輯服務(wù)AudioTaskService,并發(fā)送具體的編輯操作給它。

  4. AudioTaskHandler,音頻編輯任務(wù)處理器。AudioTaskService接受到的intent任務(wù)都交給它去處理。這里具體處理裁剪,合成等操作。

  5. AudioEditUtil, 音頻編輯工具類。提供裁剪,合成等音頻編輯的方法。

  6. 另外還有其他相關(guān)的音頻工具類。

現(xiàn)在我們看看它們之間的主要流程實現(xiàn):

CutFragment發(fā)起音頻裁剪任務(wù),同時接收更新音頻編輯消息

public class CutFragment extends Fragment {

 ...

 /**
  * 裁剪音頻
  */
 private void cutAudio() {

  String path2 = tvAudioPath2.getText().toString();

  if(TextUtils.isEmpty(path2)){
   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;
  }

  //調(diào)用AudioTaskCreator發(fā)起音頻裁剪任務(wù)
  AudioTaskCreator.createCutAudioTask(getContext(), path2, 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啟動音頻裁剪任務(wù)AudioTaskService

public class AudioTaskCreator {
 ...
 /**
  * 啟動音頻裁剪任務(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í)行后臺任務(wù)的服務(wù)
 */
public class AudioTaskService extends IntentService {

 private AudioTaskHandler mTaskHandler;

 public AudioTaskService() {
  super("AudioTaskService");
 }

 @Override public void onCreate() {
  super.onCreate();

  mTaskHandler = new AudioTaskHandler();
 }

 /**
  * 實現(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 裁剪開始時間
  * @param endTime 裁剪結(jié)束時間
  */
 private void cutAudio(String srcPath, float startTime, float endTime){
  //具體裁剪操作
 } 
}

音頻裁剪方法的實現(xiàn)

接下來是音頻裁剪的具體操作。還記得上一篇文章說的,音頻的裁剪操作都是要基于PCM文件或者WAV文件上進(jìn)行的,所以對于一般的音頻文件都是需要先解碼得到PCM文件或者WAV文件,才能進(jìn)行具體的音頻編輯操作。因此音頻裁剪操作需要經(jīng)歷以下步驟:

  1. 計算解碼后的wav音頻路徑

  2. 對源音頻進(jìn)行解碼,得到解碼后源WAV文件

  3. 創(chuàng)建源wav文件和目標(biāo)WAV音頻頻的RandomAccessFile,以便對它們后面對它們進(jìn)行讀寫操作

  4. 根據(jù)采樣率,聲道數(shù),采樣位數(shù),和當(dāng)前時間,計算開始時間和結(jié)束時間對應(yīng)到源文件的具體位置

  5. 根據(jù)采樣率,聲道數(shù),采樣位數(shù),裁剪音頻數(shù)據(jù)大小等,計算得到wav head文件頭byte數(shù)據(jù)

  6. 將wav head文件頭byte數(shù)據(jù)寫入到目標(biāo)文件中

  7. 將源文件的開始位置到結(jié)束位置的數(shù)據(jù)復(fù)制到目標(biāo)文件中

  8. 刪除源wav文件,重命名目標(biāo)wav文件為源wav文件,即得到最終裁剪后的wav文件

如下,對源音頻進(jìn)行解碼,得到解碼后的音頻文件,然后根據(jù)解碼音頻文件得到Audio音頻相關(guān)信息,里面記錄音頻相關(guān)的信息如采樣率,聲道數(shù),采樣位數(shù)等。

/**
 * 
 */
public class AudioTaskHandler {

 /**
  * 裁剪音頻
  * @param srcPath 源音頻路徑
  * @param startTime 裁剪開始時間
  * @param endTime 裁剪結(jié)束時間
  */
 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信息其實就是解碼時獲取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這個值不一定有
    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ù)是要先查找MediaFormat.KEY_PCM_ENCODING這個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這個key信息,這時就使用默認(rèn)的AudioFormat.ENCODING_PCM_16BIT,即默認(rèn)的16位采樣精度(也可以說2個字節(jié)作為一個采樣點編碼)。

接下來就是真正的裁剪操作了。根據(jù)audio中的音頻信息得到將要寫入的wav文件頭信息字節(jié)數(shù)據(jù),創(chuàng)建隨機(jī)讀寫文件,寫入文件頭數(shù)據(jù),然后源隨機(jī)讀寫文件移動到指定的開始時間開始讀取,目標(biāo)隨機(jī)讀寫文件將讀取的數(shù)據(jù)寫入,知道源隨機(jī)文件讀到指定的結(jié)束時間停止,這樣就完成了音頻文件的裁剪操作。

public class AudioEditUtil {
 /**
  * 裁剪音頻
  * @param audio 音頻信息
  * @param cutStartTime 裁剪開始時間
  * @param cutEndTime 裁剪結(jié)束時間
  */
 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);

   //移動到文件開始讀取處
   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());
 }
}

計算裁剪時間點對應(yīng)文件中數(shù)據(jù)的位置

需要注意的是根據(jù)時間計算在文件中的位置,它是這么實現(xiàn)的:

 /**
  * 獲取wave文件某個時間對應(yīng)的數(shù)據(jù)位置
  * @param time 時間
  * @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;
 }

這里要特別注意,因為time是個float的數(shù),所以計算后的position取整它并不一定是(byteNum * channels)的倍數(shù),而position的位置必須要是(byteNum * channels)的倍數(shù),否則后面的音頻數(shù)據(jù)就全部亂了,那么在播放時就是撒撒撒撒的噪音,而不是原來的聲音了。原因是音頻數(shù)據(jù)是按照一個個采樣點來計算的,一個采樣點的大小就是(byteNum * channels),所以要?。╞yteNum * channels)的整數(shù)倍。

寫入wav文件頭信息

接著看看往新文件寫入wav文件頭是怎么實現(xiàn)的,這個在上一篇中也是有講過的,不過還是列出來吧:

/**
  * 獲取Wav header 字節(jié)數(shù)據(jù)
  * @param totalAudioLen 整個音頻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;
  //采樣率,每個通道的播放速度
  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)一次要處理多少個這樣字節(jié)的數(shù)據(jù),確定緩沖區(qū),通道數(shù)*采樣位數(shù)
  header[32] = (byte) (channels * 16 / 8);
  header[33] = 0;
  //每個樣本的數(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ù)這四個參數(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ù)寫入到文件中了。這里要先移動源文件的讀取位置到裁剪起始處,即

//移動到文件開始讀取處
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ù)。

感謝各位的閱讀!關(guān)于“android中如何實現(xiàn)音頻裁剪”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI