Tuesday 10 June 2014

MediaCodec: Decoding AAC android

Encoding/Decoding for various audio/video formats is now possible in android since
JellyBean. Sample code below shows AAC decoding using MediaCodec API provided in
android JellyBean and above.

Sample InputFile
Sample input file, songwav.mp4 is also created using MediaCodec and MediaMuxer.
Songwav.mp4 file AAC encoded file with following parameters:
 
    MediaFormat outputFormat = MediaFormat.createAudioFormat(
            "audio/mp4a-latm", 44100, 2);
    outputFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
            MediaCodecInfo.CodecProfileLevel.AACObjectLC);
    outputFormat.setInteger(MediaFormat.KEY_BIT_RATE,
            128000);
AAC Decoding
AAC file is decoded using MediaExtractor and MediaCodec. AudioTrack is used to play
the audio while decoding.

String inputfilePath = Environment.getExternalStorageDirectory()
            .getPath() + "/" + "songwav.mp4";
String outputFilePath = Environment.getExternalStorageDirectory()
            .getPath() + "/" + "songwavmp4.pcm";
OutputStream outputStream = new FileOutputStream(outputFilePath);
    MediaCodec codec;
    AudioTrack audioTrack;

// extractor gets information about the stream
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputfilePath);

        MediaFormat format = extractor.getTrackFormat(0);
    String mime = format.getString(MediaFormat.KEY_MIME);

    // the actual decoder
    codec = MediaCodec.createDecoderByType(mime);
    codec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
    codec.start();
    ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
    ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();

    // get the sample rate to configure AudioTrack
    int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);

    // create our AudioTrack instance
    audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
            AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
            AudioTrack.getMinBufferSize(sampleRate,
                    AudioFormat.CHANNEL_OUT_STEREO,
                    AudioFormat.ENCODING_PCM_16BIT), AudioTrack.MODE_STREAM);

    // start playing, we will feed you later
    audioTrack.play();
    extractor.selectTrack(0);

    // start decoding
    final long kTimeOutUs = 10000;
    MediaCodec.BufferInfo BufInfo = new MediaCodec.BufferInfo();
    boolean sawInputEOS = false;
    boolean sawOutputEOS = false;

    int inputBufIndex;

    int counter=0;
while (!sawOutputEOS) {


        counter++;
        if (!sawInputEOS) {
            inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
            // Log.d(LOG_TAG, " bufIndexCheck " + bufIndexCheck);
            if (inputBufIndex >= 0) {
                ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];

                int sampleSize = extractor
                        .readSampleData(dstBuf, 0 /* offset */);

                long presentationTimeUs = 0;

                if (sampleSize < 0) {

                    sawInputEOS = true;
                    sampleSize = 0;
                } else {

                    presentationTimeUs = extractor.getSampleTime();
                }
                // can throw illegal state exception (???)

                codec.queueInputBuffer(inputBufIndex, 0 /* offset */,
                        sampleSize, presentationTimeUs,
                        sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM
                                : 0);

                if (!sawInputEOS) {
                    extractor.advance();
                }
            } else {
                Log.e("sohail", "inputBufIndex " + inputBufIndex);
            }
        }

        int res = codec.dequeueOutputBuffer(BufInfo, kTimeOutUs);

        if (res >= 0) {
            Log.i("sohail","decoding: deqOutputBuffer >=0, counter="+counter);
            // Log.d(LOG_TAG, "got frame, size " + info.size + "/" +
            // info.presentationTimeUs);
            if (BufInfo.size > 0) {
                // noOutputCounter = 0;
            }

            int outputBufIndex = res;
            ByteBuffer buf = codecOutputBuffers[outputBufIndex];

            final byte[] chunk = new byte[BufInfo.size];
            buf.get(chunk);
            buf.clear();

            if (chunk.length > 0) {
                // play
                audioTrack.write(chunk, 0, chunk.length);
                // write to file
                outputStream.write(chunk);

            }
            codec.releaseOutputBuffer(outputBufIndex, false /* render */);
            if ((BufInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                Log.i("sohail", "saw output EOS.");
                sawOutputEOS = true;
            }
        } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            codecOutputBuffers = codec.getOutputBuffers();

            Log.i("sohail", "output buffers have changed.");
        } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            MediaFormat oformat = codec.getOutputFormat();

            Log.i("sohail", "output format has changed to " + oformat);
        } else {
            Log.i("sohail", "dequeueOutputBuffer returned " + res);
        }
    }

    Log.d(LOG_TAG, "stopping...");

    // ////////closing
    if (audioTrack != null) {
        audioTrack.flush();
        audioTrack.release();
        audioTrack = null;
    }

    outputStream.flush();
    outputStream.close();

    codec.stop();

Monday 2 June 2014

Scaling images in onCreate: Android


Android developers mostly face out of memory error when loading images into imageView. Primarily this error is raised because we try to load very large image and there is not much memory available. To prevent this, this is good idea to scale down the image according to imageView height/width. setScaledAvatar() method is doing this scaling.

private void setScaledAvatar(String avatarPath, ImageView imageView) {

        // Get the dimensions of the View
        int targetW = imageView.getWidth();
        int targetH = imageView.getHeight();

        // Get the dimensions of the bitmap
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();

        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(avatarPath, bmOptions);
        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        if (targetW == 0 || targetH == 0) {
            throw new RuntimeException("ImageView height or width is zero");
        }


        // Determine how much to scale down the image
        int scaleFactor = Math.min(photoW / targetW, photoH / targetH);

        // Decode the image file into a Bitmap sized to fill the View
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        bmOptions.inPurgeable = true;

        Bitmap bitmap = BitmapFactory.decodeFile(avatarPath, bmOptions);
        imageView.setImageBitmap(bitmap);

   
    }

However if you want to use this method on activity onCreate, this will throw divided by zero exception. why? because in onCreate, view are not yet actually drawn and imageView height and width are zero. To handle this situation,we can getViewTree observer and override addGlobalLayout method to for scalling image according to imageView height/width.
 

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.main_fragment);

       String path = Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES).getAbsolutePath()
                + "/" + "sample.png";
        ImageView avatar = (ImageView) findViewById(R.id.ivAvatar);

      
        avatar.getViewTreeObserver().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {

                    @Override
                    public void onGlobalLayout() {
                        if (path != null && avatar != null) {

                            setScaledAvatar(path, avatar);
                        }

                    }
                });

    }


Sunday 1 June 2014

Making three state custom button in android


Today I'll discuss how can we make three state button like repeat song button in android music app.
First of all we need to define xml custom attributes for our custom button. Our repeat button has three states:
  1. Repeat One.
  2. Repeat All.
  3. Repeat Off.
So we need to define xml attributes for:
  1. src_repeat_one => Source drawable to be shown for Repeat One.
  2. src_repeat_all   => Source drawable to be shown for Repeat All.
  3. src_repeat_off  => Source drawable to be shown for Repeat Off.
plus, we also need another xml attribute to set the current state:
     repeat_state => which can be 0, 1 or 2 (off, on , all).

So lets create attr.xml in values folder of our android project.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- repeat state -->
    <attr name="repeat_state">
        <enum name="off" value="0" />
        <enum name="one" value="1" />
        <enum name="all" value="2" />
    </attr>

    <!-- custom repeat button -->
    <declare-styleable name="RepeatButton">
        <attr name="repeat_state" />
        <attr name="src_repeat_off" format="integer" />
        <attr name="src_repeat_one" format="integer" />
        <attr name="src_repeat_all" format="integer" />
    </declare-styleable>

</resources> 


Now, we will make Repeat button by extending the imageButton:

RepeatButton.xml:

public class RepeatButton extends ImageButton {

    private final int MAX_STATES=3;
    int state;
    Drawable srcRepeatOff;
    Drawable srcRepeatOne;
    Drawable srcRepeatAll;
    int repeatState;
    Context context;
    

    public RepeatButton(Context context) {
        super(context);
        this.context=context;

    }

    public RepeatButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context=context;

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.RepeatButton);

        try {

            repeatState = a
                    .getInteger(R.styleable.RepeatButton_repeat_state, 0);
            srcRepeatOff = a
                    .getDrawable(R.styleable.RepeatButton_src_repeat_off);
            srcRepeatOne = a
                    .getDrawable(R.styleable.RepeatButton_src_repeat_one);
            srcRepeatAll = a
                    .getDrawable(R.styleable.RepeatButton_src_repeat_all);

        } catch (Exception e) {

        } finally {
            a.recycle();
        }

        switch (repeatState) {
        case 0:
            this.setBackground(srcRepeatOff);
            break;
        case 1:
            this.setBackground(srcRepeatOne);
            break;
        case 2:
            this.setBackground(srcRepeatAll);
            break;
        default:
            break;

        }
    }

    @Override
    public boolean performClick() {
        super.performClick();
        nextState();
        setStateBackground();
        return true;

    }
    
    private void nextState() {
        state++;

        if (state == MAX_STATES) {
            state = 0;
        }
    }

    private void setStateBackground() {

        switch (state) {
        case 0:
            this.setBackground(srcRepeatOff);
            showButtonText("Repeat Off");
            break;
        case 1:
            this.setBackground(srcRepeatOne);
            showButtonText("Repeat One");
            break;
        case 2:
            this.setBackground(srcRepeatAll);
            showButtonText("Repeat All");
            
            break;
        default:
            break;

        }
    }
    public void showButtonText(String text) {

        Toast.makeText(context, text, Toast.LENGTH_SHORT).show();

    }
    public REPEAT getRepeatState() {

        switch (state) {
        case 0:
            return REPEAT.OFF;
        case 1:
            return REPEAT.ONE;
        case 2:
            return REPEAT.ALL;
        default:
            return REPEAT.OFF;

        }
    }

    public void setRepeatState(REPEAT repeatState) {

        switch (repeatState) {
        case OFF:
            state=0;

            break;
        case ONE:
            state=1;
            break;
        case ALL:
            state=2;
            break;
        default:
            break;
        }

        setStateBackground();
    }
}


We retrieve xml attributes and set the imageButton values accordingly. PerformClick is called whenever user press the button, so we change the button state here. REPEAT is an enum define like this:

public enum REPEAT {
     OFF,ONE,ALL;
}

getRepeatState and setRepeatState methods are not necessary and  provided only to change button state programatically.

We have created all the necessary components of our custom button. Now lets use it in our layout, activity_main.xml


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <sohail.aziz.samplebutton.RepeatButton
        android:id="@+id/btRepeat"
        android:layout_width="60dp"
        android:layout_height="40dp"
        android:layout_marginRight="100dp"
        android:padding="5dp"
        app:repeat_state="off"
        app:src_repeat_all="@drawable/repeat_enable"
        app:src_repeat_off="@drawable/repeat_disable"
        app:src_repeat_one="@drawable/repeat_enable_1" />

 . . . . . 

</LinearLayout>


Pay attention to xmlns:app="http://schemas.android.com/apk/res-auto" this is necessary for using custom attributes in your layouts. The layout (with three buttons, different states) will look like this:




Complete source code can be found here three-state-button .