/* ** AACDecoder - Freeware Advanced Audio (AAC) Decoder for Android ** Copyright (C) 2011 Spolecne s.r.o., http://www.spoledge.com ** ** This file is a part of AACDecoder. ** ** AACDecoder is free software; you can redistribute it and/or modify ** it under the terms of the GNU Lesser General Public License as published ** by the Free Software Foundation; either version 3 of the License, ** or (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU Lesser General Public License for more details. ** ** You should have received a copy of the GNU Lesser General Public License ** along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.spoledge.aacdecoder; import com.rubika.aotalk.util.Logging; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; /** * This is a PCM Feeder which uses arrays (short[]). * * <pre> * // 44100 Hz, stereo, buffering of 1.5 seconds: * PCMFeed pcmfeed = new PCMFeed( 44100, 2, PCMFeed.msToBytes( 1500 )); * * // start the exectuin thread: * new Thread( pcmfeed ).start(); * * while (...) { * // obtain the PCM data: * short[] samples = ... * * // feed the audio buffer; on error break the loop: * if (!pcmfeed.feed( samples, samples.length )) break; * } * </pre> */ public class PCMFeed implements Runnable, AudioTrack.OnPlaybackPositionUpdateListener { private static final String APP_TAG = "--> The Leet :: PCMFeed"; //////////////////////////////////////////////////////////////////////////// // Attributes //////////////////////////////////////////////////////////////////////////// protected int sampleRate; protected int channels; protected int bufferSizeInMs; protected int bufferSizeInBytes; /** * The callback - may be null. */ protected PlayerCallback playerCallback; /** * True iff the AudioTrack is playing. */ protected boolean isPlaying; protected boolean stopped; /** * The local variable in run() method set by method acquireSamples(). */ protected short[] lsamples; /** * The variable set by feed() method and consumed in run(). */ protected short[] samples; /** * The variable set by feed() method and consumed in run(). */ protected int samplesCount; /** * Total samples written to AudioTrack. */ protected int writtenTotal = 0; //////////////////////////////////////////////////////////////////////////// // Constructors //////////////////////////////////////////////////////////////////////////// /** * Creates a new PCMFeed object. * @param sampleRate the sampling rate in Hz (e.g. 44100) * @param channels the number of channels - only allowed values are 1 (mono) and 2 (stereo). * @param bufferSizeInBytes the size of the audio buffer in bytes * @param playerCallback the callback - may be null */ protected PCMFeed( int sampleRate, int channels, int bufferSizeInBytes, PlayerCallback playerCallback ) { this.sampleRate = sampleRate; this.channels = channels; this.bufferSizeInBytes = bufferSizeInBytes; this.bufferSizeInMs = bytesToMs( bufferSizeInBytes, sampleRate, channels ); this.playerCallback = playerCallback; } //////////////////////////////////////////////////////////////////////////// // Public //////////////////////////////////////////////////////////////////////////// /** * Returns the sampling rate. */ public final int getSampleRate() { return sampleRate; } /** * Returns the number of channels. */ public final int getChannels() { return channels; } /** * Returns the buffer size in bytes. */ public final int getBufferSizeInBytes() { return bufferSizeInBytes; } /** * Returns the buffer size in milliseconds. */ public final int getBufferSizeInMs() { return bufferSizeInMs; } /** * This is called by main thread when a new data are available. * * @param samples the array containing the PCM data * @param n the length of the PCM data * @return true if ok, false if the execution thread is not responding */ public synchronized boolean feed( short[] samples, int n ) { while (this.samples != null && !stopped) { try { wait(); } catch (InterruptedException e) {} } this.samples = samples; this.samplesCount = n; notify(); return !stopped; } /** * Stops the PCM feeder. * This method just asynchronously notifies the execution thread. * This can be called in any state. */ public synchronized void stop() { stopped = true; notify(); } /** * Converts milliseconds to bytes of buffer. * @param ms the time in milliseconds * @return the size of the buffer in bytes */ public static int msToBytes( int ms, int sampleRate, int channels ) { return (int)(((long) ms) * sampleRate * channels / 500); } /** * Converts milliseconds to samples of buffer. * @param ms the time in milliseconds * @return the size of the buffer in samples */ public static int msToSamples( int ms, int sampleRate, int channels ) { return (int)(((long) ms) * sampleRate * channels / 1000); } /** * Converts bytes of buffer to milliseconds. * @param bytes the size of the buffer in bytes * @return the time in milliseconds */ public static int bytesToMs( int bytes, int sampleRate, int channels ) { return (int)(500L * bytes / (sampleRate * channels)); } /** * Converts samples of buffer to milliseconds. * @param samples the size of the buffer in samples (all channels) * @return the time in milliseconds */ public static int samplesToMs( int samples, int sampleRate, int channels ) { return (int)(1000L * samples / (sampleRate * channels)); } //////////////////////////////////////////////////////////////////////////// // OnPlaybackPositionUpdateListener //////////////////////////////////////////////////////////////////////////// /** * Called on the listener to notify it that the previously set marker * has been reached by the playback head. */ public void onMarkerReached( AudioTrack track ) { } /** * Called on the listener to periodically notify it that the playback head * has reached a multiple of the notification period. */ public void onPeriodicNotification( AudioTrack track ) { if (playerCallback != null) { int buffered = 0; try { buffered = writtenTotal - track.getPlaybackHeadPosition()*channels; } catch (IllegalStateException e) { Logging.log(APP_TAG, "onPeriodicNotification(): illegal state=" + track.getPlayState()); return; } int ms = samplesToMs( buffered, sampleRate, channels ); playerCallback.playerPCMFeedBuffer( isPlaying, ms, bufferSizeInMs ); } } //////////////////////////////////////////////////////////////////////////// // Runnable //////////////////////////////////////////////////////////////////////////// /** * The main execution loop which should be executed in its own thread. */ public void run() { Logging.log(APP_TAG, "run(): sampleRate=" + sampleRate + ", channels=" + channels + ", bufferSizeInBytes=" + bufferSizeInBytes + " (" + bufferSizeInMs + " ms)"); AudioTrack atrack = new AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes, AudioTrack.MODE_STREAM ); atrack.setPlaybackPositionUpdateListener( this ); atrack.setPositionNotificationPeriod( msToSamples( 200, sampleRate, channels )); isPlaying = false; while (!stopped) { // fetch the samples into our "local" variable lsamples: int ln = acquireSamples(); if (stopped) { releaseSamples(); break; } // samples written to AudioTrack in this round: int writtenNow = 0; do { if (writtenNow != 0) { Logging.log(APP_TAG, "too fast for playback, sleeping..."); try { Thread.sleep( 50 ); } catch (InterruptedException e) {} } int written = atrack.write( lsamples, writtenNow, ln ); if (written < 0) { Logging.log(APP_TAG, "error in playback feed: " + written); stopped = true; break; } writtenTotal += written; int buffered = writtenTotal - atrack.getPlaybackHeadPosition()*channels; if (!isPlaying) { if (buffered*2 >= bufferSizeInBytes) { Logging.log(APP_TAG, "start of AudioTrack - buffered " + buffered + " samples"); atrack.play(); isPlaying = true; } else { Logging.log(APP_TAG, "start buffer not filled enough - AudioTrack not started yet"); } } writtenNow += written; ln -= written; } while (ln > 0); releaseSamples(); } if (isPlaying) atrack.stop(); atrack.flush(); atrack.release(); Logging.log(APP_TAG, "run() stopped."); } //////////////////////////////////////////////////////////////////////////// // Protected //////////////////////////////////////////////////////////////////////////// /** * Acquires samples into variable lsamples. * @return the actual size (in shorts) of the lsamples */ protected synchronized int acquireSamples() { while (samplesCount == 0 && !stopped) { try { wait(); } catch (InterruptedException e) {} } // copy to local vars lsamples = samples; int ln = samplesCount; // clear the instance vars samples = null; samplesCount = 0; notify(); return ln; } /** * Releases the lsamples variable. * This method is called always after processing the acquired lsamples. */ protected void releaseSamples() { // nothing to be done } }