Permalink

2

Recording audio streams

My most recent app RingDimmer relies heavily on the use of the device microphone. This is not a trivial operation as there are many devices with many specifications, many of which don’t conform to the minimum requirements set out in the Android docs.

As such, I’ve decided to lend my fellow developers a hand and release the class I created to handle audio recording. I’m sure the class will need to improve over time, as new device configurations appear. So far it has been pretty reliable, but I welcome your comments and critiques.

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder.AudioSource;
import android.os.Process;

public class AudioMeter extends Thread {
    /////////////////////////////////////////////////////////////////
    // PUBLIC CONSTANTS

    // Convenience constants
    public static final int AMP_SILENCE = 0;
    public static final int AMP_NORMAL_BREATHING = 10;
    public static final int AMP_MOSQUITO = 20;
    public static final int AMP_WHISPER = 30;
    public static final int AMP_STREAM = 40;
    public static final int AMP_QUIET_OFFICE = 50;
    public static final int AMP_NORMAL_CONVERSATION = 60;
    public static final int AMP_HAIR_DRYER = 70;
    public static final int AMP_GARBAGE_DISPOSAL = 80;

    /////////////////////////////////////////////////////////////////
    // PRIVATE CONSTANTS

    private static final float MAX_REPORTABLE_AMP = 32767f;
    private static final float MAX_REPORTABLE_DB = 90.3087f;

    /////////////////////////////////////////////////////////////////
    // PRIVATE MEMBERS

    private AudioRecord mAudioRecord;
    private int mSampleRate;
    private short mAudioFormat;
    private short mChannelConfig;

    private short[] mBuffer;
    private int mBufferSize = AudioRecord.ERROR_BAD_VALUE;

    private int mLocks = 0;

    /////////////////////////////////////////////////////////////////
    // CONSTRUCTOR

    private AudioMeter() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
        createAudioRecord();
    }

    /////////////////////////////////////////////////////////////////
    // PUBLIC METHODS

    public static AudioMeter getInstance() {
        return InstanceHolder.INSTANCE;
    }

    public float getAmplitude() {
        return (float) (MAX_REPORTABLE_DB + (20 * Math.log10(getRawAmplitude() / MAX_REPORTABLE_AMP)));
    }

    public synchronized void startRecording() {
        if (mAudioRecord == null || mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            throw new IllegalStateException("startRecording() called on an uninitialized AudioRecord.");
        }

        if (mLocks == 0) {
            mAudioRecord.startRecording();
        }

        mLocks++;
    }

    public synchronized void stopRecording() {
        mLocks--;

        if (mLocks == 0) {
            if (mAudioRecord != null) {
                mAudioRecord.stop();
                mAudioRecord.release();
                mAudioRecord = null;
            }
        }
    }

    /////////////////////////////////////////////////////////////////
    // PRIVATE METHODS

    private void createAudioRecord() {
        if (mSampleRate > 0 && mAudioFormat > 0 && mChannelConfig > 0) {
            mAudioRecord = new AudioRecord(AudioSource.MIC, mSampleRate, mChannelConfig, mAudioFormat, mBufferSize);

            return;
        }

        // Find best/compatible AudioRecord
        for (int sampleRate : new int[] { 8000, 11025, 16000, 22050, 32000, 44100, 47250, 48000 }) {
            for (short audioFormat : new short[] { AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_8BIT }) {
                for (short channelConfig : new short[] { AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO,
                        AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.CHANNEL_CONFIGURATION_STEREO }) {

                    // Try to initialize
                    try {
                        mBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

                        if (mBufferSize < 0) {
                            continue;
                        }

                        mBuffer = new short[mBufferSize];
                        mAudioRecord = new AudioRecord(AudioSource.MIC, sampleRate, channelConfig, audioFormat,
                                mBufferSize);

                        if (mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                            mSampleRate = sampleRate;
                            mAudioFormat = audioFormat;
                            mChannelConfig = channelConfig;

                            return;
                        }

                        mAudioRecord.release();
                        mAudioRecord = null;
                    }
                    catch (Exception e) {
                        // Do nothing
                    }
                }
            }
        }
    }

    private int getRawAmplitude() {
        if (mAudioRecord == null) {
            createAudioRecord();
        }

        final int bufferReadSize = mAudioRecord.read(mBuffer, 0, mBufferSize);

        if (bufferReadSize < 0) {
            return 0;
        }

        int sum = 0;
        for (int i = 0; i < bufferReadSize; i++) {
            sum += Math.abs(mBuffer[i]);
        }

        if (bufferReadSize > 0) {
            return sum / bufferReadSize;
        }

        return 0;
    }

    /////////////////////////////////////////////////////////////////
    // PRIVATE CLASSES

    private static class InstanceHolder {
        private static final AudioMeter INSTANCE = new AudioMeter();
    }
}

Basically what this class does is construct a valid AudioRecord Object, wrap AudioRecord methods, and provide conversion to decibels. It also caches the AudioRecord configuration and prevents multiple instances of the recorder.

Feel free to use as you see fit, and be sure to check out RingDimmer!

Permalink

0

Falling back on old SDK methods without reflection

As Android progresses as a platform, there will inevitably be methods added to the SDK. You’ll want to add some of these great new features while still supporting the old SDKs.

We can conditionally use these methods by getting the device’s SDK version and using the appropriate method, but this won’t actually work because although it may compile, the older device will throw an exception when it loads the class that uses that unknown method.

One way to fix this is via reflection, which works great. Reflection, however, can be resource intensive, and hard to maintain. You’ll have to declare the method as a string and pass in a method signature… gross.

Another way to do this builds on the first approach. Since a method isn’t loaded until the class it’s in is loaded, we can hide the method in another class. Here’s how it works:

public class MyActivity extends ListActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        // Check to see if this version of Android supports
        // ListView smooth scrolling
        if(Integer.parseInt(Build.VERSION.SDK) >= 8) {
            // Smooth scroll to position
            SmoothScrollMethodHolder.smoothScrollToPosition(getListView(), 5);
        }
        else {
            // Set postion
            getListView().setSelection(5);
        }
    }
   
    private class SmoothScrollMethodHolder {
        // Put method in a static method of a private class. This class
        // won't be loaded until you call this statically method.
        public static void smoothScrollToPosition(ListView listView, int position) {
            listView.smoothScrollToPosition(position);
        }
    }
}

This code is easy to read, easy to maintain, and comes with all the benefits that reflection strips away, like code completion. Of course, you don’t want to litter your code with the above, so you’ll probably want to put it into a utility class.

public static class CompatibilityUtils {
    private static final int SDK_VERSION = Integer.parseInt(Build.VERSION.SDK);

    public static void smoothScrollToPosition(ListView listView, int position) {
        if(SDK_VERSION >= 8) {
            API9.smoothScrollToPosition(listView, position);
        }
        else {
            listView.setPosition(position);
        }
    }
   
    private static class API6 {
        ...
    }

    private static class API8 {
        public static void smoothScrollToPosition(ListView listView, int position) {
            listView.smoothScrollToPosition(position);
        }
    }
   
    private static class API9 {
        ...
    }
   
    private static class API10 {
        ...
    }
   
    private static class API14 {
        ...
    }
}

You can add all your compatibility methods to this class. The original code is now just:

CompatibilityUtils.smoothScrollToPosition(getListView(), 5);
Permalink

2

Inset TextView shadows

If you’re used to using Apple products, you’re probably familiar with the heavy use of inset shadows. It adds a bit of depth to the UI and can really make the screen look beautiful. Notice the white drop shadow in the title of the window.

Finder

I tend to do the same thing a lot for my Android apps. It’s incredibly simple to do. Here’s an example of a TextView with the same inset shadow.

<!-- Semi-opaque white inset shadow beneath the text -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    ...
    android:shadowColor="#88FFFFFF"
    android:shadowRadius="0.1"
    android:shadowDx="0"
    android:shadowDy="1" />
       
<!-- Semi-opaque black inset shadow above the text -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    ...
    android:shadowColor="#88000000"
    android:shadowRadius="0.1"
    android:shadowDx="0"
    android:shadowDy="-1" />

And here’s how it looks:

Inset shadows

It should look great on all screen sizes. Don’t use the above code strictly, however. Mess around with the color values and shadowRadius value to get the exact effect you’re looking for.

Permalink

0

Repeating Bitmaps inside LayerLists

I’ve run into this particular issue several times now. It’s a bug in the Android SDK, which apparently has been fixed as of ICS (Ice Cream Sandwich). When you place a <bitmap> inside a <layer-list>, it tends to do whatever it feels like doing in regards to repeating the bitmap. Sometimes it will follow your instructions, and sometimes it won’t.

To fix this, just set the repeat mode in code. Here’s a snippet that will set all your Bitmaps repeating in a LayerDrawable.

private void setLayerDrawableBitmapsRepeating(LayerDrawable layerDrawable) {
    final int size = layerDrawable.getNumberOfLayers();
    for(int i = 0; i < size; i++) {
        Drawable drawable = layerDrawable.getDrawable(i);
        if(drawable instanceof BitmapDrawable) {
            ((BitmapDrawable) drawable).setTileModeXY(TileMode.REPEAT, TileMode.REPEAT);
        }
    }
}
Permalink

0

Getting visible bounds from a MapView

MapView doesn’t have a getBounds() method, but using a couple of MapView’s other methods, it’s actually very easy to find the visible bounds. MapView has the methods getCenter(), getLongitudeSpan(), and getLatitudeSpan(). By combining these methods we can get the visible bounds.

private Rect getMapBounds(MapView mapView) {
    final GeoPoint mapCenter = mapView.getMapCenter();
    final int lngHalfSpan = mapView.getLongitudeSpan() / 2;
    final int latHalfSpan = mapView.getLatitudeSpan() / 2;

    return new Rect(mapCenter.getLongitudeE6() - lngHalfSpan, mapCenter.getLatitudeE6() - latHalfSpan,
            mapCenter.getLongitudeE6() + lngHalfSpan, mapCenter.getLatitudeE6() + latHalfSpan);
}