這篇文章給大家分享的是有關android中如何實現音頻裁剪的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
下面是音頻裁剪效果圖:

音頻編輯項目的整體結構
該音頻測試項目的結構其實很簡單,大致就是以Fragment為基礎的各個界面,以IntentService為基礎的后臺服務,以及最重要的音頻編輯工具類實現。大致結構如下:
CutFragment,裁剪頁面。選擇音頻,裁剪音頻,播放裁剪后的音頻,同時注冊了EventBus以便接受后臺音頻編輯操作發送的消息進行更新。
AudioTaskService,音頻編輯服務Service。繼承自IntentService,可以在后臺任務的線程中執行耗時音頻編輯操作。
AudioTaskCreator,音頻編輯任務命令發送器。通過它可以啟動音頻編輯服務AudioTaskService,并發送具體的編輯操作給它。
AudioTaskHandler,音頻編輯任務處理器。AudioTaskService接受到的intent任務都交給它去處理。這里具體處理裁剪,合成等操作。
AudioEditUtil, 音頻編輯工具類。提供裁剪,合成等音頻編輯的方法。
另外還有其他相關的音頻工具類。
現在我們看看它們之間的主要流程實現:
CutFragment發起音頻裁剪任務,同時接收更新音頻編輯消息
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;
}
//調用AudioTaskCreator發起音頻裁剪任務
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啟動音頻裁剪任務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),所以要?。╞yteNum * 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之間的數據。
感謝各位的閱讀!關于“android中如何實現音頻裁剪”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。