/* ** 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 java.io.FileInputStream; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import com.rubika.aotalk.util.Logging; /** * This is the AAC Stream player class. * It uses Decoder to decode AAC stream into PCM samples. * This class is not thread safe. * <pre> * AACPlayer player = new AACPlayer(); * * String url = ...; * player.playAsync( url ); * </pre> */ public class AACPlayer { private static final String APP_TAG = "--> The Leet :: AACPlayer"; /** * The default expected bitrate. * Used only if not specified in play() methods. */ public static final int DEFAULT_EXPECTED_KBITSEC_RATE = 64; /** * The default capacity of the audio buffer (AudioTrack) in ms. * @see setAudioBufferCapacityMs(int) */ public static final int DEFAULT_AUDIO_BUFFER_CAPACITY_MS = 1500; /** * The default capacity of the output buffer used for decoding in ms. * @see setDecodeBufferCapacityMs(int) */ public static final int DEFAULT_DECODE_BUFFER_CAPACITY_MS = 700; //////////////////////////////////////////////////////////////////////////// // Attributes //////////////////////////////////////////////////////////////////////////// protected boolean stopped; protected boolean metadataEnabled = true; protected int audioBufferCapacityMs; protected int decodeBufferCapacityMs; protected PlayerCallback playerCallback; protected Decoder decoder; // variables used for computing average bitrate private int sumKBitSecRate = 0; private int countKBitSecRate = 0; private int avgKBitSecRate = 0; //////////////////////////////////////////////////////////////////////////// // Constructors //////////////////////////////////////////////////////////////////////////// /** * Creates a new player. */ public AACPlayer() { this( null ); } /** * Creates a new player. * @param playerCallback the callback, can be null */ public AACPlayer( PlayerCallback playerCallback ) { this( playerCallback, DEFAULT_AUDIO_BUFFER_CAPACITY_MS, DEFAULT_DECODE_BUFFER_CAPACITY_MS ); } /** * Creates a new player. * @param playerCallback the callback, can be null * @param audioBufferCapacityMs the capacity of the audio buffer (AudioTrack) in ms * @param decodeBufferCapacityMs the capacity of the buffer used for decoding in ms * @see setAudioBufferCapacityMs(int) * @see setDecodeBufferCapacityMs(int) */ public AACPlayer( PlayerCallback playerCallback, int audioBufferCapacityMs, int decodeBufferCapacityMs ) { setPlayerCallback( playerCallback ); setAudioBufferCapacityMs( audioBufferCapacityMs ); setDecodeBufferCapacityMs( decodeBufferCapacityMs ); decoder = createDecoder(); } //////////////////////////////////////////////////////////////////////////// // Public //////////////////////////////////////////////////////////////////////////// /** * Returns the underlying decoder. */ public Decoder getDecoder() { return decoder; } /** * Sets the custom decoder. */ public void setDecoder( Decoder decoder ) { this.decoder = decoder; } /** * Sets the audio buffer (AudioTrack) capacity. * The capacity can be expressed in time of audio playing of such buffer. * For example 1 second buffer capacity is 88100 samples for 44kHz stereo. * By setting this the audio will start playing after the audio buffer is first filled. * * NOTE: this should be set BEFORE any of the play methods are called. * * @param audioBufferCapacityMs the capacity of the buffer in milliseconds */ public void setAudioBufferCapacityMs( int audioBufferCapacityMs ) { this.audioBufferCapacityMs = audioBufferCapacityMs; } /** * Gets the audio buffer capacity as the audio playing time. * @return the capacity of the audio buffer in milliseconds */ public int getAudioBufferCapacityMs() { return audioBufferCapacityMs; } /** * Sets the capacity of the output buffer used for decoding. * The capacity can be expressed in time of audio playing of such buffer. * For example 1 second buffer capacity is 88100 samples for 44kHz stereo. * Decoder tries to fill out the whole buffer in each round. * * NOTE: this should be set BEFORE any of the play methods are called. * * @param decodeBufferCapacityMs the capacity of the buffer in milliseconds */ public void setDecodeBufferCapacityMs( int decodeBufferCapacityMs ) { this.decodeBufferCapacityMs = decodeBufferCapacityMs; } /** * Gets the capacity of the output buffer used for decoding as the audio playing time. * @return the capacity of the decoding buffer in milliseconds */ public int getDecodeBufferCapacityMs() { return decodeBufferCapacityMs; } /** * Sets the PlayerCallback. * NOTE: this should be set BEFORE any of the play methods are called. */ public void setPlayerCallback( PlayerCallback playerCallback ) { this.playerCallback = playerCallback; } /** * Returns the PlayerCallback or null if no PlayerCallback was set. */ public PlayerCallback getPlayerCallback() { return playerCallback; } /** * Returns the flag if metadata information is enabeld / sent to PlayerCallback. */ public boolean getMetadataEnabled() { return metadataEnabled; } /** * Sets the flag if metadata information is enabeld / sent to PlayerCallback. * This is enabled by default. */ public void setMetadataEnabled( boolean metadataEnabled ) { this.metadataEnabled = metadataEnabled; } /** * Plays a stream asynchronously. * This method starts a new thread. * @param url the URL of the stream or file */ public void playAsync( final String url ) { playAsync( url, -1 ); } /** * Plays a stream asynchronously. * This method starts a new thread. * @param url the URL of the stream or file * @param expectedKBitSecRate the expected average bitrate in kbit/sec; -1 means unknown */ public void playAsync( final String url, final int expectedKBitSecRate ) { new Thread(new Runnable() { public void run() { try { play( url, expectedKBitSecRate ); } catch (Exception e) { Logging.log(APP_TAG, e.getMessage()); if (playerCallback != null) playerCallback.playerException( e ); } } }).start(); } /** * Plays a stream synchronously. * @param url the URL of the stream or file */ public void play( String url ) throws Exception { play( url, -1 ); } /** * Plays a stream synchronously. * @param url the URL of the stream or file * @param expectedKBitSecRate the expected average bitrate in kbit/sec; -1 means unknown */ public void play( String url, int expectedKBitSecRate ) throws Exception { if (url.indexOf( ':' ) > 0) { URLConnection cn = new URL( url ).openConnection(); prepareConnection( cn ); cn.connect(); processHeaders( cn ); // TODO: try to get the expectedKBitSecRate from headers play( getInputStream( cn ), expectedKBitSecRate); } else play( new FileInputStream( url ), expectedKBitSecRate ); } /** * Plays a stream synchronously. * @param is the input stream */ public void play( InputStream is ) throws Exception { play( is, -1 ); } /** * Plays a stream synchronously. * @param is the input stream * @param expectedKBitSecRate the expected average bitrate in kbit/sec; -1 means unknown */ public final void play( InputStream is, int expectedKBitSecRate ) throws Exception { stopped = false; if (playerCallback != null) playerCallback.playerStarted(); if (expectedKBitSecRate <= 0) expectedKBitSecRate = DEFAULT_EXPECTED_KBITSEC_RATE; sumKBitSecRate = 0; countKBitSecRate = 0; playImpl( is, expectedKBitSecRate ); } /** * Stops the execution thread. */ public void stop() { stopped = true; } //////////////////////////////////////////////////////////////////////////// // Protected //////////////////////////////////////////////////////////////////////////// /** * Plays a stream synchronously. * This is the implementation method called by every play() and playAsync() methods. * @param is the input stream * @param expectedKBitSecRate the expected average bitrate in kbit/sec */ protected void playImpl( InputStream is, int expectedKBitSecRate ) throws Exception { BufferReader reader = new BufferReader(computeInputBufferSize( expectedKBitSecRate, decodeBufferCapacityMs ), is ); new Thread( reader ).start(); PCMFeed pcmfeed = null; Thread pcmfeedThread = null; // profiling info long profMs = 0; long profSamples = 0; long profSampleRate = 0; int profCount = 0; try { Decoder.Info info = decoder.start( reader ); Logging.log(APP_TAG, "play(): samplerate=" + info.getSampleRate() + ", channels=" + info.getChannels()); profSampleRate = info.getSampleRate() * info.getChannels(); if (info.getChannels() > 2) { throw new RuntimeException("Too many channels detected: " + info.getChannels()); } // 3 buffers for result samples: // - one is used by decoder // - one is used by the PCMFeeder // - one is enqueued / passed to PCMFeeder - non-blocking op short[][] decodeBuffers = createDecodeBuffers( 3, info ); short[] decodeBuffer = decodeBuffers[0]; int decodeBufferIndex = 0; pcmfeed = createPCMFeed( info ); pcmfeedThread = new Thread( pcmfeed ); pcmfeedThread.start(); do { long tsStart = System.currentTimeMillis(); info = decoder.decode( decodeBuffer, decodeBuffer.length ); int nsamp = info.getRoundSamples(); profMs += System.currentTimeMillis() - tsStart; profSamples += nsamp; profCount++; //Logging.log(APP_TAG, "play(): decoded " + nsamp + " samples"); if (nsamp == 0 || stopped) break; if (!pcmfeed.feed( decodeBuffer, nsamp ) || stopped) break; int kBitSecRate = computeAvgKBitSecRate( info ); if (Math.abs(expectedKBitSecRate - kBitSecRate) > 1) { Logging.log(APP_TAG, "play(): changing kBitSecRate: " + expectedKBitSecRate + " -> " + kBitSecRate); reader.setCapacity( computeInputBufferSize( kBitSecRate, decodeBufferCapacityMs )); expectedKBitSecRate = kBitSecRate; } decodeBuffer = decodeBuffers[ ++decodeBufferIndex % 3 ]; } while (!stopped); } finally { stopped = true; if (pcmfeed != null) pcmfeed.stop(); decoder.stop(); reader.stop(); int perf = 0; if (profCount > 0) { Logging.log(APP_TAG, "play(): average decoding time: " + profMs / profCount + " ms"); } if (profMs > 0) { perf = (int)((1000*profSamples / profMs - profSampleRate) * 100 / profSampleRate); Logging.log(APP_TAG, "play(): average rate (samples/sec): audio=" + profSampleRate + ", decoding=" + (1000*profSamples / profMs) + ", audio/decoding= " + perf + " % (the higher, the better; negative means that decoding is slower than needed by audio)"); } if (pcmfeedThread != null) pcmfeedThread.join(); if (playerCallback != null) playerCallback.playerStopped( perf ); } } protected Decoder createDecoder() { return Decoder.create(); } protected short[][] createDecodeBuffers( int count, Decoder.Info info ) { int size = PCMFeed.msToSamples( decodeBufferCapacityMs, info.getSampleRate(), info.getChannels()); short[][] ret = new short[ count ][]; for (int i=0; i < ret.length; i++) { ret[i] = new short[ size ]; } return ret; } protected PCMFeed createPCMFeed( Decoder.Info info ) { int size = PCMFeed.msToBytes( audioBufferCapacityMs, info.getSampleRate(), info.getChannels()); return new PCMFeed( info.getSampleRate(), info.getChannels(), size, playerCallback ); } /** * Prepares the connection. * This method is called before a connection is opened. * Actually sets "Icy-MetaData" header to "1" if metadata are enabled. */ protected void prepareConnection( URLConnection conn ) { // request for dynamic metadata: if (metadataEnabled) conn.setRequestProperty("Icy-MetaData", "1"); } /** * Gets the input stream from the connection. * Actually returns the underlying stream or IcyInputStream. */ protected InputStream getInputStream( URLConnection conn ) throws Exception { String smetaint = conn.getHeaderField( "icy-metaint" ); InputStream ret = conn.getInputStream(); if (!metadataEnabled) { Logging.log(APP_TAG, "Metadata not enabled"); } else if (smetaint != null) { int period = -1; try { period = Integer.parseInt( smetaint ); } catch (Exception e) { Logging.log(APP_TAG, "The icy-metaint '" + smetaint + "' cannot be parsed: '" + e); } if (period > 0) { Logging.log(APP_TAG, "The dynamic metainfo is sent every " + period + " bytes"); ret = new IcyInputStream( ret, period, playerCallback ); } } else { Logging.log(APP_TAG, "This stream does not provide dynamic metainfo"); } return ret; } /** * This method is called after the connection is established. */ protected void processHeaders( URLConnection cn ) { dumpHeaders( cn ); if (playerCallback != null) { for (java.util.Map.Entry<String, java.util.List<String>> me : cn.getHeaderFields().entrySet()) { for (String s : me.getValue()) { playerCallback.playerMetadata( me.getKey(), s ); } } } } protected void dumpHeaders( URLConnection cn ) { for (java.util.Map.Entry<String, java.util.List<String>> me : cn.getHeaderFields().entrySet()) { for (String s : me.getValue()) { Logging.log(APP_TAG, "header: key=" + me.getKey() + ", val=" + s); } } } protected int computeAvgKBitSecRate( Decoder.Info info ) { // do not change the value after a while - avoid changing of the out buffer: if (countKBitSecRate < 64) { int kBitSecRate = computeKBitSecRate( info ); int frames = info.getRoundFrames(); sumKBitSecRate += kBitSecRate * frames; countKBitSecRate += frames; avgKBitSecRate = sumKBitSecRate / countKBitSecRate; } return avgKBitSecRate; } protected static int computeKBitSecRate( Decoder.Info info ) { if (info.getRoundSamples() <= 0) return -1; return computeKBitSecRate( info.getRoundBytesConsumed(), info.getRoundSamples(), info.getSampleRate(), info.getChannels()); } protected static int computeKBitSecRate( int bytesconsumed, int samples, int sampleRate, int channels ) { long ret = 8L * bytesconsumed * channels * sampleRate / samples; return (((int)ret) + 500) / 1000; } protected static int computeInputBufferSize( int kbitSec, int durationMs ) { return kbitSec * durationMs / 8; } protected static int computeInputBufferSize( Decoder.Info info, int durationMs ) { return computeInputBufferSize( info.getRoundBytesConsumed(), info.getRoundSamples(), info.getSampleRate(), info.getChannels(), durationMs ); } protected static int computeInputBufferSize( int bytesconsumed, int samples, int sampleRate, int channels, int durationMs ) { return (int)(((long) bytesconsumed) * channels * sampleRate * durationMs / (1000L * samples)); } }