Pages

Thursday, February 6, 2014

My adventures on playing and looping sounds on android, part 2

AudioTrack

This entry will be about to playing sounds and writing loops with AudioTrack on Android for a single channel, 44.1kHz sample rate (number of samples per second), 16-bit depth per sample (2 bytes per sample, i.e. the fidelity of a sound can be expressed in two bytes per sample), sound file. Let's call this set of parameters, for this specific sound file: S.

I prepared a sample with Audacity, by exporting a wav file (microsoft's uncompressed pcm file) with these parameters S.

Feeding data

There are two ways to feed data to AudioTrack's audio buffer:
  1. Set the data feed type to stream, i.e. feed it in increments:
    private AudioTrack audioTrack;
    
    // ...
    
    InputStream is = getResources().openRawResource(R.raw.click); // res/raw/click.wav
    
    audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
      AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT,
      minBufferSize, AudioTrack.MODE_STREAM);
    
    int i = 0;
    byte[] music = null; // feed in increments of 512 bytes
    try{
        music = new byte[512];
    
        audioTrack.play();
        while((i = is.read(music)) != -1)
        {
            audioTrack.write(music, 0, i);
            Log.d(getClass().getName(), "samples: " + Arrays.toString(music));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  2. Set the data feed type to static, i.e. feed it in one go:
    InputStream is = getResources().openRawResource(R.raw.click);
    
    final int WAV_FILE_BYTE_SIZE = 6878; // command line: wc -c click.wav
     
    assert WAV_FILE_BYTE_SIZE > minBufferSize;
      
    audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
     AudioFormat.CHANNEL_CONFIGURATION_MONO,
     AudioFormat.ENCODING_PCM_16BIT, WAV_FILE_BYTE_SIZE,
     AudioTrack.MODE_STATIC);
    
    byte[] music = new byte[WAV_FILE_BYTE_SIZE];
    try {
     is.read(music);
    } catch (IOException e) {
     e.printStackTrace();
    }finally
    {
     try {
      is.close();
     } catch (IOException e) {
      e.printStackTrace();
            }
    }
    
    audioTrack.write(music, 0, music.length);
    audioTrack.play();
    // ...
    

In either case, it would be wise to know the minimum buffer size, for the setting S. The minimum buffer size for a sample like this can be determined like this:
int minBufferSize = AudioTrack.getMinBufferSize(44100,
    AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT);

Log.d(getClass().getName(), "minBufferSize: " + minBufferSize); // minBufferSize: 4096


Caveat callbacks

Please bear in mind, that AudioTrack does not have any convenient callbacks (interfaces) to tell you if a sample has been loaded correctly or when a sample has finished playing. If you're doing it wrong, then it will simply blow up in your face.

Although, it has a single very versatile AudioTrack.OnPlaybackPositionUpdateListener callback interface and AudioTrack#setNotificationMarkerPosition/(1 or 2), to do stuff when AudioTrack has reached a certain marker position.

Some examples:
  • mark the end of the sound file to trigger the callback, to set the head to the start then restart itself (i.e. a loop);
  • mark the end of the sound file to clean up resources, e.g. stop, flush, release;
  • feed a few measures of music and mark the time needed for a single beat and use the #onPeriodicNotification/1 to signify the beats, with another sound, i.e. add a metronome function;
  • mark certain points in the sound file to trigger other events, e.g. start other sounds, CRUD new game characters, etc.

WAV files

WAV files always contain a header section of 44 bytes (11 frames), before you get to the actual sonic payload. You can either feed these bytes as if they were sounds or you can choose not to feed them to the sound buffer. These initial bytes will probably sound like noise. In my examples above, I did not skip over these bytes.

This is one way to skip over the header:

InputStream is = getResources().openRawResource(R.raw.click);

// ...

final int WAV_FILE_BYTE_SIZE = 6878;

// ...
final int WAV_HEADER_BYTE_SIZE = 44;
byte[] music = new byte[WAV_FILE_BYTE_SIZE - WAV_HEADER_BYTE_SIZE];
try {
 is.read(music, WAV_HEADER_BYTE_SIZE, WAV_FILE_BYTE_SIZE);
} catch (IOException e) {
// ...


Leak prevention

To prevent any memory leaks, do the following:

  • Release your AudioTrack resources:
     @Override
     protected void onDestroy() {
      if (audioTrack != null) {
       audioTrack.flush();
       audioTrack.release();
      }
      super.onDestroy();
     }
    
  • Close all your streams:
    InputStream is = getResources().openRawResource(R.raw.click);
    
    // ...
     byte[] music = new byte[WAV_FILE_BYTE_SIZE];
     try {
      is.read(music);
     } catch (IOException e) {
      e.printStackTrace();
     }finally
     {
      try {
       is.close();
      } catch (IOException e) {
       e.printStackTrace();
      }
     }
    // ...
    


Loops for fun and profit

There are two general ways to loop with AudioTrack:
  • For incremental feeds, i.e. using the AudioTrack.MODE_STREAM parameter, you can simply achieve a loop by writing to the buffer repeatedly with a while/for loop, while the stream is playing;
  • For a static feed, i.e. one time feed, using the AudioTrack.MODE_STATIC parameter, use #setLoopPoints/3:
    // ...
    
    audioTrack.write(music, 0, music.length);
    final int WAV_FILE_FLOORED_FRAME_SIZE = 1719; // math.floor (6878.0 / 4) == 1719 
    final int WAV_HEADER_FRAME_SIZE = 11;
    final int INFINITE_LOOP = -1;
    audioTrack.setLoopPoints(WAV_HEADER_FRAME_SIZE, WAV_FILE_FLOORED_FRAME_SIZE, INFINITE_LOOP);
    audioTrack.play();
    
    // ...
    
If you're doing something that requires (almost) perfect timing, then use the static approach with #setLoopPoint/3, because it has the least latency issues.

When this starts happening, in the static case:

...:E/AndroidRuntime(3110): Caused by: java.lang.IllegalArgumentException: Invalid audio buffer size
...:E/AndroidRuntime(3110): at android.media.AudioTrack.audioBuffSizeCheck(AudioTrack.java:437)

you probably exceeded the allowed buffer size. That's it, just use your imagination.

No comments:

Post a Comment

Please help to keep this blog clean. Don't litter with spam.