android音頻編輯(裁剪,合成)(2)

.. .-..---...-. -.-----..- (“I love you ”莫斯電碼),這是逛知乎的時候看到程序員的表白情書,感覺我們碼農情商好高啊!哈哈,好了開始搬磚。

上一篇介紹了音頻的采集過程。之后產品經理找我談了話,表示功能跟界面都湊合!但是(聽到“但是”表示頭皮發麻),需要再加一個功能,就是音頻在錄制的過程中,可以暫停,并且可以刪除到上次暫停的地方(此刻內心億萬頭草泥馬飛奔而過,官大一級壓死人啊!)。下面先對上次的音頻采集過程中出現的bug進行一下簡單的修改。

一、音頻的標記操作
上次的操作標記的移動速度是固定的,也就是每次surfaceView進行繪制(每隔20ms)時,標記點位向左平移3個像素,這就導致標記點位的移動速度與波形圖不能形成相同速度的平移,由于刷新的頻率較高,所以標記點在以肉眼的可見的偏移量偏離標記的位置。

這就很尷尬了。做出的修改就是在,計算錄音采集的字節數跟總的畫布長度的時候,計算每次移除list集合的字節數,再進行標記點位移。

下面是源碼截圖,只需要加一個參數即可:


這里寫圖片描述

這里寫圖片描述

二、音頻采集的回刪操作,下面是定下來的界面:

主要的改變也只是在音頻采集的下面加了一個跑動的條形,每次暫停的時候,會在在這個條上畫一個分隔線,刪除的時候,條向右移動,

錄制的時候,條形是向左平移的,它的平移速度跟上面的時間刻度條是一致的。

回刪的操作,其實是在錄制完成后,剪輯形成合并形成的,所以請看下面。


這里寫圖片描述

三、進入正題,音頻的編輯。
先看界面如下:


這里寫圖片描述

操作者可以在底部那個左右滑動,控制切割點的位置,時間軸的生成方式與編輯的時間軸是不一樣的,這個時間軸是動態的,是用

linerLayout動態添加子View生成的,很簡單,每個刻度我這里的是60dp,你自己可以根據需要更改,ll_wave_content是包裹

timeLine的父控件。代碼如下:

/**
     * 音頻的時間刻度
     */
    private void timeSize() {
        timeLine = (LinearLayout)this.findViewById(R.id.ll_time_counter);
        tv_totalTime.setText(formatTime(totalTime)+"");
        timeLine.removeAllViews();
        totleLength = totalTime*DensityUtil.dip2px(60);
//      timeLine1.removeAllViews();
        ll_wave_content1.setLayoutParams(new FrameLayout.LayoutParams(totalTime*DensityUtil.dip2px
(60),LayoutParams.MATCH_PARENT));
        ll_wave_content.setLayoutParams(new FrameLayout.LayoutParams(totalTime*DensityUtil.dip2px
(60),LayoutParams.MATCH_PARENT));
        timeLine1.setLayoutParams(new RelativeLayout.LayoutParams(totalTime*DensityUtil.dip2px
(60),LayoutParams.MATCH_PARENT));
        for(int i=0;i<totalTime;i++){
        LinearLayout line1=new LinearLayout(this);
        line1.setOrientation(LinearLayout.HORIZONTAL);
        line1.setLayoutParams(new LayoutParams(DensityUtil.dip2px
(60),LinearLayout.LayoutParams.WRAP_CONTENT));
        line1.setGravity(Gravity.CENTER);
        TextView timeText=new TextView(this);
        timeText.setText(formatTime(i));
        timeText.setWidth(DensityUtil.dip2px(60)-2);
        timeText.setGravity(Gravity.CENTER_HORIZONTAL);
        TextPaint paint = timeText.getPaint();
        paint.setFakeBoldText(true); //字體加粗設置
        timeText.setTextColor(Color.rgb(204, 204, 204));
        View line2=new View(this);
        line2.setBackgroundColor(Color.rgb(204, 204, 204));
        line2.setPadding(0, 10, 0, 0);
        line1.addView(timeText);
        line1.addView(line2);
        timeLine.addView(line1);
        }

相對其他格式的音頻文件,wav格式的相對比較簡單,只是在pcm之上添加了頭部,wav的頭部格式如下:


這里寫圖片描述

好,看的不明白的同學可自行百度活谷歌,有很多文章介紹;

既然pcm格式加上wav的頭部就可,那剪輯或者合成就很方便了,合成的方法奉上:

        /**
         * merge *.wav files 
         * @param target  output file
         * @param paths the files that need to merge
         * @return whether merge files success
         */
        public static boolean mergeAudioFiles(String target,List<String> paths) {
            try {
                FileOutputStream fos = new FileOutputStream(target);            
                int size=0;
                byte[] buf = new byte[1024 * 1000];
                int PCMSize = 0;
                for(int i=0;i<paths.size();i++){
                    FileInputStream fis = new FileInputStream(paths.get(i));
                    size = fis.read(buf);
                     while (size != -1){
                        PCMSize += size;
                        size = fis.read(buf);
                    }
                    fis.close();
                }
                PCMSize=PCMSize-paths.size()*44;
                WaveHeader header = new WaveHeader();
                header.fileLength = PCMSize + (44 - 8);
                header.FmtHdrLeth = 16;
                header.BitsPerSample = 16;
                header.Channels = 1;
                header.FormatTag = 0x0001;
                header.SamplesPerSec = 16000;
                header.BlockAlign = (short) (header.Channels * header.BitsPerSample / 8);
                header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
                header.DataHdrLeth = PCMSize;
                byte[] h = header.getHeader();
                assert h.length == 44;
                fos.write(h, 0, h.length);
                for(int j=0;j<paths.size();j++){
                    FileInputStream fis = new FileInputStream(paths.get(j));
                    size = fis.read(buf);
                    boolean isFirst=true;
                    while (size != -1){
                        if(isFirst){
                            fos.write(buf, 44, size-44);
                            size = fis.read(buf);
                            isFirst=false;
                        }else{
                            fos.write(buf, 0, size);
                            size = fis.read(buf);
                        }
                    }
                    fis.close();
                }
                fos.close();
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }

剪輯的類,注意的是,需要先將你操作的wav文件塞進去,進行頭文件的格式解析,之后就可算出你需要刪除的幀區間,之后就是相關的邏輯運算了,這里我就不一一啰嗦了:

public class CheapWAV extends CheapSoundFile {
    public static Factory getFactory() {
        return new Factory() {
            public CheapSoundFile create() {
                return new CheapWAV();
            }
            public String[] getSupportedExtensions() {
                return new String[] { "wav" };
            }
        };
    }

    // Member variables containing frame info
    private int mNumFrames;
    private int[] mFrameOffsets;
    private int[] mFrameLens;
    private int[] mFrameGains;
    private int mFrameBytes;
    private int mFileSize;
    private int mSampleRate;
    private int mChannels;
    // Member variables used during initialization
    private int mOffset;

    public CheapWAV() {
    }

    public int getNumFrames() {
        return mNumFrames;
    }

    public int getSamplesPerFrame() {
        return mSampleRate / 50;
    }

    public int[] getFrameOffsets() {
        return mFrameOffsets;
    }

    public int[] getFrameLens() {
        return mFrameLens;
    }

    public int[] getFrameGains() {
        return mFrameGains;
    }

    public int getFileSizeBytes() {
        return mFileSize;        
    }

    public int getAvgBitrateKbps() {
        return mSampleRate * mChannels * 2 / 1024;
    }

    public int getSampleRate() {
        return mSampleRate;
    }

    public int getChannels() {
        return mChannels;
    }

    public String getFiletype() {
        return "WAV";
    }
    
//    public int secondsToFrames(double seconds) {
//        return (int)(1.0 * seconds * mSampleRate / mSamplesPerFrame + 0.5);
//    }
    

    public void ReadFile(File inputFile)
            throws java.io.FileNotFoundException,
                   java.io.IOException {
        super.ReadFile(inputFile);
        mFileSize = (int)mInputFile.length();

        if (mFileSize < 128) {
            throw new java.io.IOException("File too small to parse");
        }

        FileInputStream stream = new FileInputStream(mInputFile);
        byte[] header = new byte[12];
        stream.read(header, 0, 12);
        mOffset += 12;
        if (header[0] != 'R' ||
            header[1] != 'I' ||
            header[2] != 'F' ||
            header[3] != 'F' ||
            header[8] != 'W' ||
            header[9] != 'A' ||
            header[10] != 'V' ||
            header[11] != 'E') {
            throw new java.io.IOException("Not a WAV file");
        }

        mChannels = 0;
        mSampleRate = 0;
        while (mOffset + 8 <= mFileSize) {
            byte[] chunkHeader = new byte[8];
            stream.read(chunkHeader, 0, 8);
            mOffset += 8;

            int chunkLen =
                ((0xff & chunkHeader[7]) << 24) |
                ((0xff & chunkHeader[6]) << 16) |
                ((0xff & chunkHeader[5]) << 8) |
                ((0xff & chunkHeader[4]));

            if (chunkHeader[0] == 'f' &&
                chunkHeader[1] == 'm' &&
                chunkHeader[2] == 't' &&
                chunkHeader[3] == ' ') {
                if (chunkLen < 16 || chunkLen > 1024) {
                    throw new java.io.IOException(
                        "WAV file has bad fmt chunk");
                }

                byte[] fmt = new byte[chunkLen];
                stream.read(fmt, 0, chunkLen);
                mOffset += chunkLen;

                int format =
                    ((0xff & fmt[1]) << 8) |
                    ((0xff & fmt[0]));
                mChannels =
                    ((0xff & fmt[3]) << 8) |
                    ((0xff & fmt[2]));
                mSampleRate =
                    ((0xff & fmt[7]) << 24) |
                    ((0xff & fmt[6]) << 16) |
                    ((0xff & fmt[5]) << 8) |
                    ((0xff & fmt[4]));

                if (format != 1) {
                    throw new java.io.IOException(
                        "Unsupported WAV file encoding");
                }

            } else if (chunkHeader[0] == 'd' &&
                       chunkHeader[1] == 'a' &&
                       chunkHeader[2] == 't' &&
                       chunkHeader[3] == 'a') {
                if (mChannels == 0 || mSampleRate == 0) {
                    throw new java.io.IOException(
                        "Bad WAV file: data chunk before fmt chunk");
                }

                int frameSamples = (mSampleRate * mChannels) / 50;
                mFrameBytes = frameSamples * 2;
                mNumFrames = (chunkLen + (mFrameBytes - 1)) / mFrameBytes;
                mFrameOffsets = new int[mNumFrames];
                mFrameLens = new int[mNumFrames];
                mFrameGains = new int[mNumFrames];

                byte[] oneFrame = new byte[mFrameBytes];

                int i = 0;
                int frameIndex = 0;
                while (i < chunkLen) {
                    int oneFrameBytes = mFrameBytes;
                    if (i + oneFrameBytes > chunkLen) {
                        i = chunkLen - oneFrameBytes;
                    }

                    stream.read(oneFrame, 0, oneFrameBytes);

                    int maxGain = 0;
                    for (int j = 1; j < oneFrameBytes; j += 4 * mChannels) {
                        int val = java.lang.Math.abs(oneFrame[j]);
                        if (val > maxGain) {
                            maxGain = val;
                        }
                    }

                    mFrameOffsets[frameIndex] = mOffset;
                    mFrameLens[frameIndex] = oneFrameBytes;
                    mFrameGains[frameIndex] = maxGain;

                    frameIndex++;
                    mOffset += oneFrameBytes;
                    i += oneFrameBytes;

                    if (mProgressListener != null) {
                        boolean keepGoing = mProgressListener.reportProgress(
                            i * 1.0 / chunkLen);
                        if (!keepGoing) {
                            break;
                        }
                    }
                }

            } else {
                stream.skip(chunkLen);
                mOffset += chunkLen;
            }
        }
    }

    public void WriteFile(File outputFile, int startFrame, int numFrames)
            throws java.io.IOException {
        outputFile.createNewFile();
        FileInputStream in = new FileInputStream(mInputFile);
        FileOutputStream out = new FileOutputStream(outputFile);

        long totalAudioLen = 0;
        for (int i = 0; i < numFrames; i++) {
            totalAudioLen += mFrameLens[startFrame + i];
        }

        long totalDataLen = totalAudioLen + 36;
        long longSampleRate = mSampleRate;
        long byteRate = mSampleRate * 2 * mChannels;

        byte[] header = new byte[44];
        header[0] = 'R';  // RIFF/WAVE header
        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';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f';  // 'fmt ' chunk
        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;
        header[20] = 1;  // format = 1
        header[21] = 0;
        header[22] = (byte) mChannels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        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) (2 * mChannels);  // block align
        header[33] = 0;
        header[34] = 16;  // bits per sample
        header[35] = 0;
        header[36] = 'd';
        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);
        out.write(header, 0, 44);

        byte[] buffer = new byte[mFrameBytes];
        int pos = 0;
        for (int i = 0; i < numFrames; i++) {
            int skip = mFrameOffsets[startFrame + i] - pos;
            int len = mFrameLens[startFrame + i];
            if (skip < 0) {
                continue;
            }
            if (skip > 0) {
                in.skip(skip);
                pos += skip;
            }
            in.read(buffer, 0, len);
            out.write(buffer, 0, len);
            pos += len;
        }

        in.close();
        out.close();
    }
};

好了,最近一段時間確實太忙了,其他項目的維護升級什么的,搞的頭皮發麻。有什么問題可以留言交流。

聲明:音頻的裁剪這個類的原作者的一個開源小項目叫音樂快剪,我只是在其基礎上進行了修改!其他格式的音頻MP3的話還好,但是ACC或者M4a格式的裁剪就比較麻煩,需要進行重新編碼,建議使用FFMPEG進行格式重新編碼裁剪,至于FFMPEG的android平臺移植,GITHUB上有很多,很多人的博客也有介紹,個人建議不要自己編譯(您時間富裕除外),很多已經編譯好了,直接使用即可。

Github地址(大家下載的時候順便給個star也是對作者勞動成果的肯定,謝謝):
https://github.com/T-chuangxin/VideoMergeDemo

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容