2017年8月30日 星期三

新北投,丹鳳山,熱海岩場,軍艦岩。


住在新北投已經幾年了,最近在看一些新北投文化的資料,這裡原來充滿許多在地北投人的回億。又最近腳踏車破胎,就往旁邊的丹鳳山亂走去運動,之前我都去陽明山爬山,竟然不知道旁邊的丹鳳山,風景其實滿漂亮的是爬山的好地方,也是攀岩聖地,裡面滿多小路,丹鳳山寫著大大的2個字丹鳳,熱海岩場是天然的攀岩場,熱海岩場從電訊台下去不好找,可從另一頭銀光巷上來比較好找,然後可繞到軍艦岩,風景滿好的。






































read more...

2017年6月26日 星期一

好可愛的寶可夢

寶可夢已經重大更新,多了團戰,並翻新道館,去年夏天的感動已不在了,心裏感傷,人們總是缺乏毅力,一款能出外運動段練身心的好手遊,人們己經忘記了,雖然如此,自己假日一樣打開寶可夢,去外面捕捉可愛的寶可夢,吹吹風,欣賞外面的風景,當作運動,寫這文章是要讓自己記得,勉勵自己。
read more...

2017年5月17日 星期三

透過MediaExtractor MediaCodec MediaMuxer作音視頻後製處理,影片加上時間



未處理的音視頻


處理音視頻加上時間



前言,對MediaCodec有一番認識後,試著對音視頻作後製處理,我試著對影片加上時間,再網路上搜尋一堆資料,有初步體認後,就動手試寫了,沒想到工程滿浩大的,還有很多坑,最後做出來了,但不是每部手機都能跑得完整,HTC Desire820跑OK,HTC e9 plus無法用codec.getOutputImage(index)取得一偵圖片,用ByteBuffer buffer = codec.getOutputBuffer(index)來處理,但是視頻有殘影,原因是在轉RGB時再轉回YUV420沒轉好,找不到方法,只能先這樣,實際上COLOR_FormatYUV420Flexible格式ByteBuffer就是YUV420,用Image image = codec.getOutputImage(index)還得多轉成YUV420位元組(byte[]),HTC Desire820用getInputBuffer來處理視頻花掉(應該只是轉換就會好,但是找不到資料先不管),用getOutputImager來處理就OK

首先是圖片繪製觀念,要在每一偵圖片作Canvas繪製,前提圖片需是BMP,解碼器視頻抽出一偵圖片Image image = codec.getOutputImage(index)是YUV420所以要轉到BMP,因為MediaCodec format需是COLOR_FormatYUV420Flexible格式,因此要轉成RGB後,壓成BMP才能用Canvas繪製,繪製完再轉回YUV420,才能餵給編碼器,讓合成器作音視頻合成,這轉換時間太耗時了,我用HTC Desire820處理一分鐘影片快要一個鐘頭,轉換流程如下,data是image轉成的YUV420位元組(byte[]),網路上有轉換程式(我列在下面的getDataFromImage就是轉換程式),然後用YuvImage壓成流,再decode成bmp,我們就可用canvas來作畫,這裡bmp添加時間,添完時間,就要再轉回YUV420這樣byte[]就可餵給MediaCodec,getNV21是轉換程式,網路上也找的到(我改寫列在下面),這裡強調的是流程,全部程式太多了就不全列出來,只列出轉換的程式


        //data YUV420轉rgb,作渲染
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Rect rect = new Rect(0, 0, width, height);
        //YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, rect.width(), rect.height(), null);
        YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, rect.width(), rect.height(), null);
        yuvImage.compressToJpeg(rect, 100, out);
        //RGB data
        byte[] imagebytes = out.toByteArray();
        Bitmap bmp = BitmapFactory.decodeByteArray(imagebytes, 0, imagebytes.length).copy(Bitmap.Config.ARGB_8888, true);
        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        Canvas canvas = new Canvas(bmp);
        paint.setStrokeWidth(15);
        paint.setTextSize(50);
        canvas.drawText(get_time(), 100, 150, paint);
        //Log.d("TAG_T",""+get_time());
        //canvas.drawText(videoTimeFormat.format(info.presentationTimeUs / 1000), 100, 150, paint);
        //b=true save file
        if (IS_SAVE) {compressToJpeg(i,bmp);}
        //再轉回YUV420這個可給MediaCodec
        byte[] encode_data = getNV21(width, height, bmp);



下面是image YUV420轉成 YUV420格式為NV21或I420 byte array(這是google找到的,這裡第2參數必需用colorFormat=COLOR_FormatNV21),Image image = codec.getOutputImage(index)就需透過下面轉成YUV 420 byte array,才能轉到RGB

////////////////////////////////////////YUV 420 byte array//////////////////////////////////////////////////////
    private  byte[] getDataFromImage(Image image, int colorFormat) {
        if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) {
            throw new IllegalArgumentException("only support COLOR_FormatI420 " + "and COLOR_FormatNV21");
        }
        if (!isImageFormatSupported(image)) {
            throw new RuntimeException("can't convert Image to byte array, format " + image.getFormat());
        }
        Rect crop = image.getCropRect();
        int format = image.getFormat();
        int width = crop.width();
        int height = crop.height();
        Image.Plane[] planes = image.getPlanes();
        byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
        byte[] rowData = new byte[planes[0].getRowStride()];
        if (VERBOSE)Log.d("TAG_L", "get data from " + planes.length + " planes");
        int channelOffset = 0;
        int outputStride = 1;

        for (int i = 0; i < planes.length; i++) {
            switch (i) {
                case 0:
                    channelOffset = 0;
                    outputStride = 1;
                    break;
                case 1:
                    if (colorFormat == COLOR_FormatI420) {
                        channelOffset = width * height;
                        outputStride = 1;
                    } else if (colorFormat == COLOR_FormatNV21) {
                        channelOffset = width * height + 1;
                        outputStride = 2;
                    }
                    break;
                case 2:
                    if (colorFormat == COLOR_FormatI420) {
                        channelOffset = (int) (width * height * 1.25);
                        outputStride = 1;
                    } else if (colorFormat == COLOR_FormatNV21) {
                        channelOffset = width * height;
                        outputStride = 2;
                    }
                    break;
            }
            ByteBuffer buffer = planes[i].getBuffer();
            int rowStride = planes[i].getRowStride();
            int pixelStride = planes[i].getPixelStride();
            if (VERBOSE) {
                Log.v(TAG, "pixelStride " + pixelStride);
                Log.v(TAG, "rowStride " + rowStride);
                Log.v(TAG, "width " + width);
                Log.v(TAG, "height " + height);
                Log.v(TAG, "buffer size " + buffer.remaining());
            }
            //if i==0 fhift=0 else shift=1
            int shift = (i == 0) ? 0 : 1;
            int w = width >> shift;
            int h = height >> shift;
            buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
            for (int row = 0; row < h; row++) {
                int length;
                if (pixelStride == 1 && outputStride == 1) {
                    length = w;
                    buffer.get(data, channelOffset, length);
                    channelOffset += length;
                } else {
                    length = (w - 1) * pixelStride + 1;
                    buffer.get(rowData, 0, length);
                    for (int col = 0; col < w; col++) {
                        data[channelOffset] = rowData[col * pixelStride];
                        channelOffset += outputStride;
                    }
                }
                if (row < h - 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
            }
            if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i);

        }

        return data;

    }


下面是bmp轉回YUV420 byte array,也是google找到的,不過跟原本程式不一樣了,我改寫for迴圈內只用weight和height處理每一點像素,這樣才能用Thread,可開啟多個Thread來處理,split=2就是把height切成2段,會開啟2個Thread,第1個Thread處理0到height/2,第2個Thread處理height/2到height,並Thead加入List,當List中所有Thread都執行完畢,才會往下走,split=32,就會開啟32個Thread,本來以為開啟越多Thread會快很多,但我錯了,CPU不快在多Thread也沒用,我這選split=2,還有特別注意一點下面的B=Color.red(argb[num]),R=Color.blue(argb[num]),B應該是blue,R是red才對,這不是寫錯了,是BMP的B跟R是相反的,一開始不知道有這個坑,跑出來顏色一直不正確,在google找好久,找到一篇說BMP的B跟R是相反的,我將B和R對調後顏色就正確了.


  public  byte[] getNV21(int width, int height, Bitmap scaled) {
        int[] argb = new int[width * height];
        scaled.getPixels(argb, 0, width, 0, 0, width, height);
        byte[] yuv = new byte[width * height * 3 / 2];
        encodeYUV420SP(yuv, argb, width, height);
        scaled.recycle();
        return yuv;
    }


    public  void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
        final int frameSize = width * height;
        List<caulate> th_list=new ArrayList<>();
        //int yIndex = 0;
        //int uvIndex = frameSize;
        //數字越大,啟用的thread越多,越多thread不一定越快
        int split=2;//2的N次方
        int dh=height/split;
        //使用多個thread處理YUV420SP
        for (int i=0;i<height;i=i+dh ){
            int end=i+dh;
            if(end>height){end=height;}
            caulate ac=new caulate(yuv420sp, argb,  width, height,i,end);
            th_list.add(ac);
        }
        //當所有thread都結束
        for(int j=0;j<th_list.size();j++){
            while (th_list.get(j).isAlive()){}
        }


    }

private class caulate extends Thread{
    int[] argb;
    byte[] yuv420sp;
    int width;
    int height;
    int jbegin;
    int jend;
    public caulate(byte[] myuv420sp, int[] margb, int mwidth, int mheight,int mjbegin,int mjend){
        yuv420sp=myuv420sp;
        argb=margb;
        width=mwidth;
        height=mheight;
        jend=mjend;
        jbegin=mjbegin;
        this.start();
    }
    @Override
    public void run(){
        final int frameSize = width * height;

        //int yIndex = 0;
        //int uvIndex = frameSize;

        int a, R, G, B, Y, U, V;
        //int index = 0;
        for (int j =jbegin ; j < jend; j++) {
            for (int i = 0; i < width; i++) {
                int num=i+j*width;
                //BMP是BGR 我們把R,B對調
                //a = (argb[index] & 0xff000000) >> 24; // a is not used obviously
                //R = (argb[index] & 0xff0000) >> 16;
                //G = (argb[index] & 0xff00) >> 8;
                //B = (argb[index] & 0xff) >> 0;
                B=Color.red(argb[num]);
                G=Color.green(argb[num]);
                R=Color.blue(argb[num]);

                // well known RGB to YUV algorithm
                Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
                U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
                V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

                // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
                // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
                // pixel AND every other scanline.
                yuv420sp[num] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));

                if (j % 2 == 0 && (num) % 2 == 0) {
                    //if v<0 等於0,否則V>255 等於255否則等與V
                   // yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
                   // yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
                    int unum=i+frameSize;
                    int u_num=unum+j*width/2;
                    int u_num1=u_num+1;
                    //if v<0 等於0,否則V>255 V等於255否則等與V
                    yuv420sp[u_num] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
                    yuv420sp[u_num1] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
                    //Log.d("TAG_CA","j :" +j);
                    //Log.d("TAG_CA","u_num :" +u_num1);
                    //Log.d("TAG_CA","uvIndex :" +uvIndex);
                    //try{Thread.sleep(50);}catch(Exception e){}
                }

                //index++;
            }
        }
    }

    }






再來說音視頻解碼加編碼,讓視頻跟音頻分離要用MediaExtractor,視頻分離後從MediaExtractor readSampleDataget寫入InputBuffer,queueInputBuffer讓解碼器抽出每一偵getOutputBuffer(或getOutputImage),作Canvas繪製就可丟給編碼器,然後releaseOutputBuffer,編碼器編完碼再用MediaMuxer作合成,總共要2個MediaExtractor(一個也行作音視頻分離),4個MediaCodec(video解碼,video編碼,audio解碼,audio編碼),一個MediaMuxer(作音視頻合成)

整個流程

                              視頻 -> 解碼(decode) ->繪製處理 - 編碼(encode)
MediaExtractor( 音視頻分離 )->                                              ->MediaMuxer(作音視頻合成)
                              音頻 ->      解碼(decode)  ->     編碼(encode)


視頻解碼(decode)流程
readSampleDataget(extracter)-> InputBuffer->queueInputBuffer解碼(decode)->getOutputBuffer->canvas繪製處理(處理後可給編碼器)->releaseOutputBuffer

音頻解碼(decode)流程
readSampleDataget(extracter)-> InputBuffer->queueInputBuffer解碼(decode)->getOutputBuffer(buffer資料可給編碼器)->releaseOutputBuffer

視頻編碼(decode)流程
canvas繪製處理(處理後可給編碼器)-> InputBuffer->queueInputBuffer編碼(encode)->getOutputBuffer(buffer寫入合成器)->releaseOutputBuffer

音頻編碼(decode)流程
(buffer資料可給編碼器)-> InputBuffer->queueInputBuffer編碼(encode)->getOutputBuffer(buffer寫入合成器)->releaseOutputBuffer



一開始4個MediaCodec都是用異步處理,在音頻編碼時,聲音異常,一邊跑程式我用AudioTrack來play聲音測試,在解碼這部份過程都OK,但play放到編碼的InputBuffer這段後,聲音測試就不對了,猜想,因為解碼跟編碼不是同步,會有問題,有可能解碼丟2筆,可是編碼再第一或第三筆資料,因為異步處理一個Mediacodec就是一個Thread,要解這問題,可用儲列方法,解碼放到一個List,編碼再一筆一筆拿出來,不過我沒試這方法,我將音頻編碼改用同步處理且不用Thread去處裡,這樣音頻問題就解掉了,寫這程式遇到好多陷阱跟問題.


read more...

2017年5月3日 星期三

錄製手機螢幕聲音MediaCodec異步處理


MediaCodec異步處理,之前那方法是同步處理,之前文章我說錯了,寫入影音文件應該說編碼器才對,處理讀取影音文件才是解碼器,編碼解碼原理都一樣,
試著將錄取手機螢幕影像,之前程式改成MediaCodec異步處理,異步處理比同步處理更為直覺,寫法比較清楚,比較符合物件導向,我比較喜歡這樣的寫法.

說一下MediaCodec異步處理寫法原理,我喜歡用extends然後覆寫其中方法,總覺得這樣寫法才優雅,我們首先extends  MediaCodec.Callback,然後覆寫其中4個方法Method(如下面),在啟動MediaCodec前MediaCodec.setCallback(this)設定回傳Class為this,MediaCodec.start()開始後,這4個覆寫方法,就會開始時時監聽MediaCodec狀態,

1. onInputBufferAvailable監聽輸入端buffer可獲取,
2. onOutputBufferAvailable監聽輸出端buffer可獲取,
3. onOutputFormatChanged監聽格式發生變化,
4. onError監聽發生錯誤,

當然在這之前要先格式化MediaCodec,準備好代為處理輸入端的VirtualDisplay,方法中codec參數和index參數,info參數,就是監聽MediaCodec後的數據值,用這些參數來處理我們想要的資料,codec參數指該MediaCodec,index參數指MediaCodec索引值,info參數指MediaCodec buffer資訊,參數format指MediaCodec 格式發生變化format(onOutputFormatChange)
譬如codec.getOutputBuffer(index)就能取得輸出buffer,info.size()取得buffer size值


 //1 監聽輸入端buffer可獲取
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {    
        //codec索引值index,處理codec
        //這裡沒用到,Virtualdisplay已取代輸入端
    }
 //2 監聽輸出端buffer可獲取
    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        Log.d("TAG","Output buffer begin.");
        ByteBuffer encodeData = codec.getOutputBuffer(index);
        ........................................................
        codec.releaseOutputBuffer(index, false);//觸理完一回,一定要釋放輸出buffer
    }
 
    //3 監聽codec發生錯誤重設codec
    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
                Log.d("TAG","Error:"+e);
                codec.reset();
    }
    //4 監聽格式發生變化重設格式,啟動合成器MediaMuxer
    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
        Log.d("TAG","output format already changed!");
        //vedio resetOutputFormat
        //MediaFormat newFormat = codec.getOutputFormat();
        //MediaMuxer addtrack
        mVideoTrackIndex = mMediaMuxer.addTrack(format);
        mMediaMuxer.start();
    }


關鍵碼,部份拿掉,這樣流程較清楚




public class surface_mediacodec_advance extends  MediaCodec.Callback{
    //private Boolean MUXER_START = false;
    private MediaProjection mp;
    private AtomicBoolean mQuit = new AtomicBoolean(false);
    private int mscreendi;
    private VirtualDisplay vp;
    private String save_path = "/sdcard/capture.mp4";
    private check_file_save_finish mcheck_file_save_finish;
    private long lastpresentationTimeUs;
    private int VIDEO_KEY_BIT_RATE = 4000000;//2000000 4000000
    private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
    private static final int FRAME_RATE = 30; // 30 fps
    private static final int IFRAME_INTERVAL = 10; // 10 seconds between I-frames
    private static final int TIMEOUT_US = 10000;
    private int CH_TYPE;
    //使用者已許可MediaProjection mp
    public surface_mediacodec_advance(int windowWidth, int windowHeight, int sdi, MediaProjection m, int audio_sample_rate, int ch) {
        CH_TYPE=ch;
        //初始化MediaCodec
        mwindowWidth = windowWidth;
        mwindowHeight = windowHeight;
        mp = m;

        mscreendi = sdi;
        try {
            mMediaMuxer= new MediaMuxer(save_path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        Log.d("TAG"," CH TYPE :"+CH_TYPE);
    }
    //開始錄製
    public final void START_RECORDER() {
         preparerecorder_other();//設置MediaCodec video格式
        //preapareaudioencoder();//設置MediaCodec audio
        vp = mp.createVirtualDisplay("record_screen",
                mwindowWidth, mwindowHeight, mscreendi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                mSurface, null, null);
    }
    //停止錄製
    public final void STOP_RECORDER() {
        release();//停止要release
    }
    //設置mMediaCodec格式
    private void preparerecorder_other() {
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mwindowWidth, mwindowHeight);
        //format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
        //MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_KEY_BIT_RATE);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        Log.d("TAG", "created video format: " + format);
        try {
            mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
        } catch (IOException e) {
        }
        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mSurface = mMediaCodec.createInputSurface();
        Log.d("TAG", "created input surface: " + mSurface);
        //設定MediaCodec回傳Class
        mMediaCodec.setCallback(this);
        mMediaCodec.start();
    }
    //release Mediacodec這順序要對否則格式會錯
    private void release() {
        mcheck_file_save_finish=new check_file_save_finish(save_path);
        while (!mcheck_file_save_finish.check_file_save_finish()){
            Log.d("TAG_NO","NO FINISH");
        }

        if (vp != null) {
            vp.release();
        }

        if (mMediaMuxer != null) {
            mMediaMuxer.stop();
            mMediaMuxer.release();
            mMediaMuxer = null;
        }
        //
        if (mMediaCodec != null) {
            mMediaCodec.stop();
            mMediaCodec.release();
            mMediaCodec= null;
        }
    //監聽輸入buffer可獲取
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        //這裡沒用到,Virtualdisplay已取代輸入端
    }
    //監聽輸出buffer可獲取
    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        Log.d("TAG","Output buffer begin.");
        ByteBuffer encodeData = codec.getOutputBuffer(index);
        if ((info.flags & codec.BUFFER_FLAG_CODEC_CONFIG) != 0) {//編碼數據,非媒體數據,忽略它
            info.size = 0;
        }
        if (info.size == 0) {//編碼數據,非媒體數據,忽略它
            encodeData = null;
            Log.d("TAG", "Ignore Data");
        } else {                  //非媒體數據,產生提示
            Log.d("TAG", "Get Info: size="
                    + info.size + " Video presentationTimeUs="
                    + info.presentationTimeUs + " offset="
                    + info.offset);
        }
   
        if (encodeData != null) {                             //媒體數據寫入
            encodeData.position(info.offset);
            encodeData.limit(info.offset + info.size);
            mMediaMuxer.writeSampleData(mVideoTrackIndex, encodeData, info);
            Log.d("TAG", "Send Info: size=" + info.size);
        }
   
        codec.releaseOutputBuffer(index, false);//釋放輸出buffer
    }
    //codec發生錯誤重設codec
    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
                Log.d("TAG","Error:"+e);
                codec.reset();
    }
    //格式發生變化重設格式
    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {

        Log.d("TAG","output format already changed!");
        //vedio resetOutputFormat
        //MediaFormat newFormat = codec.getOutputFormat();
        //MediaMuxer addtrack
        mVideoTrackIndex = mMediaMuxer.addTrack(format);
        mMediaMuxer.start();
    }

}

據上面錄製手機改影像編碼器為異步處理後,接著又完成了MediaCodec異步處理音頻編碼,音頻編碼異步流程,同影像編碼只是多處理輸入端

監聽輸入onInputBufferAvailable中去讀取MIC麥克風buffer資料,getInputBuffer(index)取得MediaCodec輸入buffer,將資料填入這buffer,然後queueInputBuffer(),此時onOutputBufferAvailable監聽輸出buffer可獲取,透過界面送到影像編碼寫入合成器,然後釋放此buffer,然後監聽輸入onInputBufferAvailable變又可獲取,重複上面,又去讀取MIC麥克風buffer資料.................

音頻編碼器,設計一個界面capture_audio_outpu_buffer,含實作2個方法
        //捕捉輸出buffer資料,以便寫入合成器
        void on_capture_audio_outpu_buffer(MediaCodec codec,ByteBuffer encodedAudioData,MediaCodec.BufferInfo info);
        //audio MediaCodec初始化MediaFormat,以便產生AudioTrackIndex,使能夠啟動合成
        void on_capture_mediaformat(MediaFormat mMediaFormat);

影像編碼器,實作上面界面的2個方法,影像編碼器在監聽onOutputFormatChanged中實例化音頻編碼器,set_capture_audio_outpu_buffer_listener監聽this,start()後on_capture_mediaformat會產生AudioTrackIndex,on_capture_audio_outpu_buffer開始監聽音頻編碼器的輸出資料,寫入合成器,並啟動合成器

後記:遇到有些手機AudioRecoderformat 設置立體聲,聲道數不同錄製完,回放影片聲音會變快,看一下程式就知道,是因為Audio取樣時,buffer length比getInputBuffer(index)的剩餘長度,還長,沒完全填入,只要分次全部填入就解掉了

影像編碼

public class surface_mediacodec_advance extends  MediaCodec.Callback implements audio_encoder_advance.capture_audio_outpu_buffer{
.........................................
   ///////////////////實作 audio_encoder_advance.capture_audio_outpu_buffer Method///////////////////
    //監聽audio_encoder_advance MediaFormat初始化一次
    @Override
    public void on_capture_mediaformat(MediaFormat mMediaFormat){
        //產生AudioTrackIndex
        mAudioTrackIndex = mMediaMuxer.addTrack(mMediaFormat);
    }
    //時時監聽audio_encoder_advance outpu buffer
    @Override
    public void on_capture_audio_outpu_buffer(MediaCodec codec,ByteBuffer encodedAudioData,MediaCodec.BufferInfo info){
        //audio_encoder_advance輸出,寫入合成器
        mMediaMuxer.writeSampleData(mAudioTrackIndex, encodedAudioData, info);
    }
   ........................................................................................
   //格式發生變化實例化音頻編碼器,set_capture_audio_outpu_buffer_listener,start()後會產生AudioTrackIndex,
   //開始監聽audio_encoder_advance outpu buffer寫入合成器
    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {

        Log.d("TAG_C","output format already changed!");
        //vedio resetOutputFormat
        //MediaFormat newFormat = codec.getOutputFormat();
        //MediaMuxer addtrack
        mVideoTrackIndex = mMediaMuxer.addTrack(format);
        //這要通知其他mediacodec reset
        if(maudio_encoder_advance==null){
             Log.d("TAG"," SAMPLE RATE AUDO :"+maudio_sample_rate);
             //實例化audio_encoder_advance
             maudio_encoder_advance=new audio_encoder_advance(maudio_sample_rate,CH_TYPE);
             //設定傾聽事件
             maudio_encoder_advance.set_capture_audio_outpu_buffer_listener(this);
        }
        //audio_encoder_advance開始編碼
        maudio_encoder_advance.start();

        //若存在,resetOutputFormat,要在這reset其他format
        //if(maudio_encoder_advance!=null){
        //   maudio_encoder_advance.resetOutputFormat();
        //}

        //啟動合成器
        mMediaMuxer.start();


    }
...............................................
}

音頻編碼

audio_encoder_advance class

public class audio_encoder_advance extends  MediaCodec.Callback{
    public void start(){
          preapareaudioencoder
    }
    private void preapareaudioencoder() {
       //設置audio MediaCodec格式
       .............................
       if(mlistener!=null) {
         //設新newAudioFormat給監聽者,以便產生AudioTrackIndex
         mlistener.on_capture_mediaformat(newAudioFormat);
        }
        //設回call
        mAudioEncoder.setCallback(this);
        mAudioEncoder.start();
    }
    ....................................................................................
    //時時監聽輸出buffer可獲取
    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
       ...............................................................................

       if(mlistener!=null) {
                            //設輸出資料給監聽者
                            mlistener.on_capture_audio_outpu_buffer(codec, encodedAudioData, info);
                        }

    }
    /////////////////////////////////界面////////////////////////////////////////////////////////////////
    //interface變數
    private capture_audio_outpu_buffer mlistener=null;
    //設定listener
    public void set_capture_audio_outpu_buffer_listener(capture_audio_outpu_buffer ca){
        mlistener=ca;
    }
    //設計界面
    public interface  capture_audio_outpu_buffer{
        //捕捉輸出buffer資料
        void on_capture_audio_outpu_buffer(MediaCodec codec,ByteBuffer encodedAudioData,MediaCodec.BufferInfo info);
        //audio MediaCodec初始化MediaFormat
        void on_capture_mediaformat(MediaFormat mMediaFormat);
    }
}


read more...

2017年5月1日 星期一

Android 6.0以後一般權限,用一個class輕鬆完成



Android 6.0以後一般權限不只要在AndroidManifest.xml聲明,Activity class還要實作onRequestPermissionsResult(....)給使用者授權
,每個App都有自己管理權限的頁面如影片,給使用者授權,一開始要判斷該項權限是否授權,每授權就詢問是否要求授權,否則應用程式會崩潰
第一個requestCode引數,給各任何數字check既可(如CAPTURE_AUDIO=9999),第2引數不曉得作用,反正沒用到,第3個引數grantResults,是指授權項,若有2個權限要授權會是grantResults[0],grantResults[1],下面用grantResults.length取得項目數用for去逐一判斷,是否全授權,是就運行程式,否就不能運行程式,再去要求授權.
check_permission.newInstance(this)特別用一個Class去處裡授權項

ContextCompat.checkSelfPermission(mActivity, sti)==PackageManager.PERMISSION_DENIED來判斷該項是否有授權,若沒授權加入一個List,sti是指android.Manifest.permission.XXXXX, 

若有沒授權ActivityCompat.requestPermissions(mActivity, get_str, CAPTURE_AUDIO)要求授權,get_str是指沒授權的String List

A.
設定檢查一般權限項目在str_permission設定既可如下,若是只要CAMERA,只要設str_permission={"CAMERA"}要跟AndroidManifest.xml也要加上
    private final String[] str_permission={
            "CAMERA",
            "RECORD_AUDIO",
            "WRITE_EXTERNAL_STORAGE",
            "READ_PHONE_STATE"
    };
B.
若有其他權限要加,根據需要加上需要的一般permission
若知道所有一般permission乾脆全部加上也可,省得麻煩
   public String get_permission_string(String st){
       String s=null;
      switch(st){
          case "RECORD_AUDIO":
                s=android.Manifest.permission.RECORD_AUDIO;
               break;
          case "WRITE_EXTERNAL_STORAGE":
                s=android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
               break;
          case "READ_PHONE_STATE":
               s= Manifest.permission.READ_PHONE_STATE;
               break;
          case "CAMERA":
               s= Manifest.permission.CAMERA;
               break;
          //在這加入一般permission case
       }
       return s;

   }
這個check_permission class可當作API來使用,任何App都可使用
MainActivity只需呼叫check_permission.newInstance(this);就會檢查str_permission設定的項目,


AndroidManifest.xml
    ...........................................................................
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    ............................................................................


MainActivity

public class MainActivity extends AppCompatActivity{
   ..........................................................
   private int CAPTURE_AUDIO=9999;
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        if(Build.VERSION.SDK_INT>=23) {
                //檢查是否有授權沒授權b=true
                check_permission.newInstance(this);
           }

    }

    @Override
    public void onRequestPermissionsResult(int requestCode,String[] permission,int[] grantResults){
        super.onRequestPermissionsResult(requestCode,permission,grantResults);
        if(requestCode==CAPTURE_AUDIO){
            //先預設都通過授權
            boolean b=true;
            for(int i=0;i<grantResults.length;i++){
                //逐一檢查,若一項沒通過,就不通過,
                if(grantResults[i]!= PackageManager.PERMISSION_GRANTED){b=false;}
            }
            if(b){
                Toast.makeText(this,"OK", Toast.LENGTH_LONG).show();
                START_VISUALIZER=true;
            }else{
                START_VISUALIZER=false;
                Toast.makeText(this,"Cancle", Toast.LENGTH_LONG).show();
            }
        }
    }
    ......................................................................
}







check_permission類,下面使用靜態static, newInstance來new class,如check_permission.newInstance(this)
這樣就會實例化一個check_permission,注意方法中會實例化check_permission類,並返回

public static check_permission newInstance(MainActivity obj){
       mActivity=obj;
       check_permission mcheck_permission=new check_permission();
       return mcheck_permission;
}




check_permission 


package com.example.yplin.test_audio;
/**
 * 檢查授權
 * 1.
 * 需要可添加在get_permission_string()權限中
 * 2.
 * String[] str_permission設定檢查一般權限項目
 * 3.
 * Activity呼叫 check_permission.newInstance(myActivity)開始check
 */

import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Created by yplin on 2017/4/26.
 */

public class check_permission {
    private static Activity mActivity=null;
    //設定檢查一般權限項目,若是只要CAMERA,只要設str_permission={"CAMERA"}
    private final String[] str_permission={
            "CAMERA",
            "RECORD_AUDIO",
            "WRITE_EXTERNAL_STORAGE",
            "READ_PHONE_STATE"
    };
    private int CAPTURE_AUDIO=9999;
   public check_permission(){
       run_check(str_permission);
   }
   public static check_permission newInstance(MainActivity obj){
       mActivity=obj;
       check_permission mcheck_permission=new check_permission();
       return mcheck_permission;
   }

   public void run_check(String[] st){
       //不能用null
       List<String> list=new ArrayList();
       int k=0;
       for(int i=0;i<st.length;i++)
       {
           //一個個判斷
           String sti=get_permission_string(st[i]);
           boolean b= ContextCompat.checkSelfPermission(mActivity, sti)==PackageManager.PERMISSION_DENIED;
           //把沒取得的Permission加入list
           if(b){
               Log.d("TAG",sti);
               list.add(k,sti);
               k++;
           }
       }
       //String[]一定要給正確length,根據list.size()給正確length
       String[] get_str=new String[list.size()];
       //重新產生需要求permission String[] get_str
       for(int j=0;j<list.size();j++){
           Log.d("TAG",list.get(j));
           get_str[j]=list.get(j);
       }
       //若沒有授權項目,要求授權,0就已全授權忽略
       if(list.size()>0){
           ActivityCompat.requestPermissions(mActivity, get_str, CAPTURE_AUDIO);
       }

   }
   //根據需要加上需要的一般permission
   public String get_permission_string(String st){
       String s=null;
      switch(st){
          case "RECORD_AUDIO":
                s=android.Manifest.permission.RECORD_AUDIO;
               break;
          case "WRITE_EXTERNAL_STORAGE":
                s=android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
               break;
          case "READ_PHONE_STATE":
               s= Manifest.permission.READ_PHONE_STATE;
               break;
          case "CAMERA":
               s= Manifest.permission.CAMERA;
               break;
          //在這加入一般permission case
       }
       return s;

   }

}


read more...

2017年4月30日 星期日

JAVA界面,如何設計界面含傾聽事件

常常用到實作Java API已寫好的界面,可是都從沒實際設計界面含傾聽事件,現在就來說要如何弄出可時時發送資料的界面,讓其他類監聽,取得資料

下面程式是大致的流程,interface_A類,設計界面data_change_lister被監聽類實作,含有一方法void capture_data_change(int data)要被監聽類實作的方法,其中data是我們要被其他監聽類,監聽的資料,void set_data_change_lister(data_change_lister lister)這個可指定其他監聽類來傾聽
,透過mlister.capture_data_change(i)來發送資料.

當然要達到像下面不同類傳送資料,不用界面也可,但都要在類內實例化其他監聽類,在送出資料,程式碼顯得很亂,透過界面不須實例化監聽類,界面類根本不需要考慮是任何類來監聽資料,變得有規律,透過界面更為周詳,更有彈性,譬如在遊戲中做幾拾個甚至幾百個物體碰撞判別,之前文章python gtk cairo射擊小遊戲,也有類似方法,不過有些時間忘了是如何實現的,要回去看程式碼才知道.

簡單說java 的界面類,不需實例化監聽類,只需指定界面變數,透過界面實例化該監聽類成員,不需要管監聽成員的類別,若不用界面也可以,但實例化必須是該監聽類,才能對該監聽類發送資料,以前我都是用後者來發送.

注意下面紅色粗體字mlister是界面變數,但在set_data_change_lister,卻是可以指定任何類成為這個界面變數,原因在監聽類implements後就可變成該界面型態,而被當引數指成改界面變數mlister,呼叫本身的實作方法mlister.capture_data_change(i),就可取得該i資料了,若界面類是一個Thread,將傳送資料放在run()方法中while(){傳送資料},start()後可持續監聽該資料

主Class 監聽類B:

Class B宣告實作interface_A.data_change_lister,實例化interface_A,加入事件傾聽者minterface_A.set_data_change_lister(this)
實作capture_data_change(int data)監聽了data,start thread結果會在terminal印出0 1 2 3 4 5 6 7 8 9

//Class B宣告實作interface_A.data_change_lister
public class B implements interface_A.data_change_lister{

   public B(){
     //實例化interface_A
     interface_A minterface_A=new interface_A();
     //加入事件傾聽者
    minterface_A.set_data_change_lister(this);
    //start thread
    minterface_Aminterface_A.start()
   }
   @Override
    public void capture_data_change(int data){
        //時時傾聽data變化
        Log.d("TAG_DATA","Data change: "+data);
    }
}

界面類interface_A:注意這裡沒實例化CLass B,是透過界面來實例化任何監聽類
//Class interface_A設計界面data_change_lister
public class interface_A extends Thread{
    //這裡指定界面變數,用來接受監聽類成員
    private data_change_lister mlister;
    //建構子
    public interface_A(){..........}
    @Overrid
    public void run(){
    //舉例傳送0 1 2 3 4 5 6 7 8 9資料
     if(mlister!=null)
         {
                 //透過介面mlister變數給事件傾聽者傳送資料
                 for(int i=0;i < 10;i++){
                    mlister.capture_data_change(i);
                   }
         }
         
    }
 
    //關鍵在這界面變數,可以設定是任何類,設定mlister變數成為其他監聽類實例化
    public void set_data_change_lister(data_change_lister lister){
         mlister=lister;
         Log.d("TAG_DATA","set listener "+mlister);
    }
    //設計data_change_lister界面
    public interface data_change_lister{
        //資料變化時,捕捉資料data
        void capture_data_change(int data);
    }
}



read more...

2017年4月29日 星期六

讓Class View跟一般class Thread一模一樣

如何讓Class View跟一般class Thread一模一樣,當要使用Canvas一定要繼承View才能作畫,就沒法繼承Thread,因為java只能單一繼承
若要View能有Thead功能,只能實作Runnable,但是class實作Runnable是沒有像Thread有start()函式,那要test_vew開始執行續要這樣,
要如下面這要,每次都要new Thread,我覺的這樣太麻煩
//實例化test_view
test_view mtest_view=new test_view(.....);
//mtest_view新的Thread才能start(),
new Thread(mtest_view).start();

Example 1:

public class test_view extends View implements Runnable{

@Override
    public void run(){
     while(true){
       this.update();//update view產生動畫
      }
    }
@Override
    protected void onDraw(Canvas canvas) {
      //處理canvas
   }
}

我想到一個方法,在test_view 加一個start()函式,寫入new Thread(this).start(),那要test_vew開始執行續就跟Class Thread一模一樣
//實例化test_view
test_view mtest_view=new test_view(.....);
//開始Thread
mtest_view.start();

Example 2:

public class test_view extends View implements Runnable{

    public void start(){
        new Thread(this).start();
    }
@Override
    public void run(){
     while(true){
       this.update();//update view產生動畫
      }
    }
@Override
    protected void onDraw(Canvas canvas) {
      //處理canvas
   }
}


若要實例化就開始Thread,建構子加入this.start(),這樣在實例化就會開始Thread了
//實例化test_view且開始Thread
test_view mtest_view=new test_view(.....);

Example 3:

public class test_view extends View implements Runnable{
    public test_view(Context t){
     super(t)
     this.start();
    }
    public void start(){
        new Thread(this).start();
    }
@Override
    public void run(){
     while(true){
       this.update();//update view產生動畫
      }
    }
@Override
    protected void onDraw(Canvas canvas) {
      //處理canvas
   }
}

這樣extends View implements Runnable對用view來作動畫或遊戲就很方便,一個view就是一個Thread,每各View就有個別的生命,可產生不同行為

read more...

2017年4月28日 星期五

Android音頻可視化用Visualizer這個API輕鬆實現






Android音頻可視化,可以用Visualizer這個API輕鬆實現,上面影片,啟動6個Thread取得主Class音頻輸出流作動畫Update,
其實簡單方法的不須用Thread,使用監聽方法onWaveFormDataCapture或onFftDataCapture中去update動畫既可

1.宣告實作implements Visualizer.OnDataCaptureListener界面

2.實作OnDataCaptureListener界面的2個method
 @Override
    public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
       //取得音頻輸出流,給Canvas作後續處理
        mwaveform=waveform;
    }
 @Override
    public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {

    }

3.設定監聽實作界面的2個method,取得音頻輸出流
        //實例化 Visualizer ,0指Audio output
        audioOutput = new Visualizer(0); 
        //加入傾聽事件,時時監聽實作界面的2個method,便可從實作界面的2個method時時取得音頻輸出流
        audioOutput.setDataCaptureListener(this,Visualizer.getMaxCaptureRate(),true,false);

4.對音頻輸出流作可視化處理,運算完用Canvas在View Class繪出圖形,google一下有運算公式
  
把一些不相干的去掉,代碼簡化,比較看得懂
public class MainActivity extends AppCompatActivity implements Visualizer.OnDataCaptureListener{

    private Visualizer audioOutput = null;
    private byte[] mwaveform=null;
    ...................................................
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //實例化 Visualizer ,0指Audio output
        audioOutput = new Visualizer(0); // get output audio stream
        //第2參數true指onWaveFormDataCapture() run第3參數false指onFftDataCapture no run
        audioOutput.setDataCaptureListener(this,Visualizer.getMaxCaptureRate(),true,false);
        start_Visualizer();
        ..................................................
    }
  

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stop_Visualizer();
        //釋放Visualizer
        audioOutput.release();
    }

    /////////////////////////////////////實作OnDataCaptureListener 1////////////////////////////////////
    @Override
    public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
      
        //Log.d("TAG","LENGTH"+waveform.length);
        //print byte waveform[0]=-128~128 waveform[1]=-128~128 ....................
        //可將 waveform[0]... 放入List,再根據List用Canvas畫出來
        //取得音頻輸出流,作後續處理
        mwaveform=waveform;
    }
    ////////////////////////////////////////實作OnDataCaptureListener 2////////////////////////////////////
    @Override
    public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {

    }
    ////////////////////////////////////////////////////////////////////////////////////////////////////////
    //開始Visualizer
    private void star_Visualizert(){
        audioOutput.setEnabled(true);
    }
    //停止Visualizer
    private void stop_Visualizer() { 
        audioOutput.setEnabled(false);
    }
    //CLASS取得音頻輸出流 method,給Class View Canvas作後續處理
    public byte[] get_waveform_value(){
        return mwaveform;
    }

}


read more...