content: /*
   * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
   * 
   * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
   * 
   * Spydroid is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 3 of the License, or
   * (at your option) any later version.
   * 
   * This source code 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 General Public License for more details.
   * 
   * You should have received a copy of the GNU General Public License
   * along with this source code; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package net.majorkernelpanic.streaming.audio;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.RandomAccessFile;
  import java.lang.reflect.Field;
  import java.net.InetAddress;
  import java.nio.ByteBuffer;
  
  import net.majorkernelpanic.streaming.SessionBuilder;
  import net.majorkernelpanic.streaming.rtp.AACADTSPacketizer;
  import net.majorkernelpanic.streaming.rtp.AACLATMPacketizer;
  import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream;
  import android.annotation.SuppressLint;
  import android.content.SharedPreferences;
  import android.content.SharedPreferences.Editor;
  import android.media.AudioFormat;
  import android.media.AudioRecord;
  import android.media.MediaCodec;
  import android.media.MediaCodecInfo;
  import android.media.MediaFormat;
  import android.media.MediaRecorder;
  import android.os.Build;
  import android.os.Environment;
  import android.service.textservice.SpellCheckerService.Session;
  import android.util.Log;
  
  /**
   * A class for streaming AAC from the camera of an android device using RTP.
   * You should use a {@link Session} instantiated with {@link SessionBuilder} instead of using this class directly.
   * Call {@link #setDestinationAddress(InetAddress)}, {@link #setDestinationPorts(int)} and {@link #setAudioQuality(AudioQuality)}
   * to configure the stream. You can then call {@link #start()} to start the RTP stream.
   * Call {@link #stop()} to stop the stream.
   */
  public class AACStream extends AudioStream {
  
  	public final static String TAG = "AACStream";
  
  	/** MPEG-4 Audio Object Types supported by ADTS. **/
  	private static final String[] AUDIO_OBJECT_TYPES = {
  		"NULL",							  // 0
  		"AAC Main",						  // 1
  		"AAC LC (Low Complexity)",		  // 2
  		"AAC SSR (Scalable Sample Rate)", // 3
  		"AAC LTP (Long Term Prediction)"  // 4	
  	};
  
  	/** There are 13 supported frequencies by ADTS. **/
  	public static final int[] AUDIO_SAMPLING_RATES = {
  		96000, // 0
  		88200, // 1
  		64000, // 2
  		48000, // 3
  		44100, // 4
  		32000, // 5
  		24000, // 6
  		22050, // 7
  		16000, // 8
  		12000, // 9
  		11025, // 10
  		8000,  // 11
  		7350,  // 12
  		-1,   // 13
  		-1,   // 14
  		-1,   // 15
  	};
  
  	private String mSessionDescription = null;
  	private int mProfile, mSamplingRateIndex, mChannel, mConfig;
  	private SharedPreferences mSettings = null;
  	private AudioRecord mAudioRecord = null;
  	private Thread mThread = null;
  
  	public AACStream() {
  		super();
  
  		if (!AACStreamingSupported()) {
  			Log.e(TAG,"AAC not supported on this phone");
  			throw new RuntimeException("AAC not supported by this phone !");
  		} else {
  			Log.d(TAG,"AAC supported on this phone");
  		}
  
  	}
  
  	private static boolean AACStreamingSupported() {
  		if (Build.VERSION.SDK_INT<14) return false;
  		try {
  			MediaRecorder.OutputFormat.class.getField("AAC_ADTS");
  			return true;
  		} catch (Exception e) {
  			return false;
  		}
  	}
  
  	/**
  	 * Some data (the actual sampling rate used by the phone and the AAC profile) needs to be stored once {@link #getSessionDescription()} is called.
  	 * @param prefs The SharedPreferences that will be used to store the sampling rate 
  	 */
  	public void setPreferences(SharedPreferences prefs) {
  		mSettings = prefs;
  	}
  
  	@Override
  	public synchronized void start() throws IllegalStateException, IOException {
  		configure();
  		if (!mStreaming) {
  			super.start();
  		}
  	}
  
  	public synchronized void configure() throws IllegalStateException, IOException {
  		super.configure();
  		mQuality = mRequestedQuality.clone();
  
  		// Checks if the user has supplied an exotic sampling rate
  		int i=0;
  		for (;i<AUDIO_SAMPLING_RATES.length;i++) {
  			if (AUDIO_SAMPLING_RATES[i] == mQuality.samplingRate) {
  				mSamplingRateIndex = i;
  				break;
  			}
  		}
  		// If he did, we force a reasonable one: 16 kHz
  		if (i>12) mQuality.samplingRate = 16000;
  
  		if (mMode != mRequestedMode || mPacketizer==null) {
  			mMode = mRequestedMode;
  			if (mMode == MODE_MEDIARECORDER_API) {
  				mPacketizer = new AACADTSPacketizer();
  			} else { 
  				mPacketizer = new AACLATMPacketizer();
  			}		
  		}
  		
  
  		if (mMode == MODE_MEDIARECORDER_API) {
  
  			testADTS();
  
  			// All the MIME types parameters used here are described in RFC 3640
  			// SizeLength: 13 bits will be enough because ADTS uses 13 bits for frame length
  			// config: contains the object type + the sampling rate + the channel number
  
  			// TODO: streamType always 5 ? profile-level-id always 15 ?
  
  			mSessionDescription = "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" +
  					"a=rtpmap:96 mpeg4-generic/"+mQuality.samplingRate+"\r\n"+
  					"a=fmtp:96 streamtype=5; profile-level-id=15; mode=AAC-hbr; config="+Integer.toHexString(mConfig)+"; SizeLength=13; IndexLength=3; IndexDeltaLength=3;\r\n";
  
  		} else {
  
  			mProfile = 2; // AAC LC
  			mChannel = 1;
  			mConfig = mProfile<<11 | mSamplingRateIndex<<7 | mChannel<<3;
  
  			mSessionDescription = "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" +
  					"a=rtpmap:96 mpeg4-generic/"+mQuality.samplingRate+"\r\n"+
  					"a=fmtp:96 streamtype=5; profile-level-id=15; mode=AAC-hbr; config="+Integer.toHexString(mConfig)+"; SizeLength=13; IndexLength=3; IndexDeltaLength=3;\r\n";			
  
  		}
  
  	}
  
  	@Override
  	protected void encodeWithMediaRecorder() throws IOException {
  		testADTS();
  		((AACADTSPacketizer)mPacketizer).setSamplingRate(mQuality.samplingRate);
  		super.encodeWithMediaRecorder();
  	}
  
  	@Override
  	@SuppressLint({ "InlinedApi", "NewApi" })
  	protected void encodeWithMediaCodec() throws IOException {
  
  		final int bufferSize = AudioRecord.getMinBufferSize(mQuality.samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)*2;
  
  		((AACLATMPacketizer)mPacketizer).setSamplingRate(mQuality.samplingRate);
  
  		mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mQuality.samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
  		mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
  		MediaFormat format = new MediaFormat();
  		format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
  		format.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitRate);
  		format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
  		format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mQuality.samplingRate);
  		format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
  		format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize);
  		mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  		mAudioRecord.startRecording();
  		mMediaCodec.start();
  
  		final MediaCodecInputStream inputStream = new MediaCodecInputStream(mMediaCodec);
  		final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
  
  		mThread = new Thread(new Runnable() {
  			@Override
  			public void run() {
  				int len = 0, bufferIndex = 0;
  				try {
  					while (!Thread.interrupted()) {
  						bufferIndex = mMediaCodec.dequeueInputBuffer(10000);
  						if (bufferIndex>=0) {
  							inputBuffers[bufferIndex].clear();
  							len = mAudioRecord.read(inputBuffers[bufferIndex], bufferSize);
  							if (len ==  AudioRecord.ERROR_INVALID_OPERATION || len == AudioRecord.ERROR_BAD_VALUE) {
  								Log.e(TAG,"An error occured with the AudioRecord API !");
  							} else {
  								//Log.v(TAG,"Pushing raw audio to the decoder: len="+len+" bs: "+inputBuffers[bufferIndex].capacity());
  								mMediaCodec.queueInputBuffer(bufferIndex, 0, len, System.nanoTime()/1000, 0);
  							}
  						}
  					}
  				} catch (RuntimeException e) {
  					e.printStackTrace();
  				}
  			}
  		});
  
  		mThread.start();
  
  		// The packetizer encapsulates this stream in an RTP stream and send it over the network
  		mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
  		mPacketizer.setInputStream(inputStream);
  		mPacketizer.start();
  
  		mStreaming = true;
  
  	}
  
  	/** Stops the stream. */
  	public synchronized void stop() {
  		if (mStreaming) {
  			if (mMode==MODE_MEDIACODEC_API) {
  				Log.d(TAG, "Interrupting threads...");
  				mThread.interrupt();
  				mAudioRecord.stop();
  				mAudioRecord.release();
  				mAudioRecord = null;
  			}
  			super.stop();
  		}
  	}
  
  	/**
  	 * Returns a description of the stream using SDP. It can then be included in an SDP file.
  	 * Will fail if called when streaming.
  	 */
  	public String getSessionDescription() throws IllegalStateException {
  		if (mSessionDescription == null) throw new IllegalStateException("You need to call configure() first !");
  		return mSessionDescription;
  	}
  
  	/** 
  	 * Records a short sample of AAC ADTS from the microphone to find out what the sampling rate really is
  	 * On some phone indeed, no error will be reported if the sampling rate used differs from the 
  	 * one selected with setAudioSamplingRate 
  	 * @throws IOException 
  	 * @throws IllegalStateException
  	 */
  	@SuppressLint("InlinedApi")
  	private void testADTS() throws IllegalStateException, IOException {
  		
  		setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
  		try {
  			Field name = MediaRecorder.OutputFormat.class.getField("AAC_ADTS");
  			setOutputFormat(name.getInt(null));
  		}
  		catch (Exception ignore) {
  			setOutputFormat(6);
  		}
  
  		String key = PREF_PREFIX+"aac-"+mQuality.samplingRate;
  
  		if (mSettings!=null) {
  			if (mSettings.contains(key)) {
  				String[] s = mSettings.getString(key, "").split(",");
  				mQuality.samplingRate = Integer.valueOf(s[0]);
  				mConfig = Integer.valueOf(s[1]);
  				mChannel = Integer.valueOf(s[2]);
  				return;
  			}
  		}
  
  		final String TESTFILE = Environment.getExternalStorageDirectory().getPath()+"/spydroid-test.adts";
  
  		if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
  			throw new IllegalStateException("No external storage or external storage not ready !");
  		}
  
  		// The structure of an ADTS packet is described here: http://wiki.multimedia.cx/index.php?title=ADTS
  
  		// ADTS header is 7 or 9 bytes long
  		byte[] buffer = new byte[9];
  
  		mMediaRecorder = new MediaRecorder();
  		mMediaRecorder.setAudioSource(mAudioSource);
  		mMediaRecorder.setOutputFormat(mOutputFormat);
  		mMediaRecorder.setAudioEncoder(mAudioEncoder);
  		mMediaRecorder.setAudioChannels(1);
  		mMediaRecorder.setAudioSamplingRate(mQuality.samplingRate);
  		mMediaRecorder.setAudioEncodingBitRate(mQuality.bitRate);
  		mMediaRecorder.setOutputFile(TESTFILE);
  		mMediaRecorder.setMaxDuration(1000);
  		mMediaRecorder.prepare();
  		mMediaRecorder.start();
  
  		// We record for 1 sec
  		// TODO: use the MediaRecorder.OnInfoListener
  		try {
  			Thread.sleep(2000);
  		} catch (InterruptedException e) {}
  
  		mMediaRecorder.stop();
  		mMediaRecorder.release();
  		mMediaRecorder = null;
  
  		File file = new File(TESTFILE);
  		RandomAccessFile raf = new RandomAccessFile(file, "r");
  
  		// ADTS packets start with a sync word: 12bits set to 1
  		while (true) {
  			if ( (raf.readByte()&0xFF) == 0xFF ) {
  				buffer[0] = raf.readByte();
  				if ( (buffer[0]&0xF0) == 0xF0) break;
  			}
  		}
  
  		raf.read(buffer,1,5);
  
  		mSamplingRateIndex = (buffer[1]&0x3C)>>2 ;
  		mProfile = ( (buffer[1]&0xC0) >> 6 ) + 1 ;
  		mChannel = (buffer[1]&0x01) << 2 | (buffer[2]&0xC0) >> 6 ;
  		mQuality.samplingRate = AUDIO_SAMPLING_RATES[mSamplingRateIndex];
  
  		// 5 bits for the object type / 4 bits for the sampling rate / 4 bits for the channel / padding
  		mConfig = mProfile<<11 | mSamplingRateIndex<<7 | mChannel<<3;
  
  		Log.i(TAG,"MPEG VERSION: " + ( (buffer[0]&0x08) >> 3 ) );
  		Log.i(TAG,"PROTECTION: " + (buffer[0]&0x01) );
  		Log.i(TAG,"PROFILE: " + AUDIO_OBJECT_TYPES[ mProfile ] );
  		Log.i(TAG,"SAMPLING FREQUENCY: " + mQuality.samplingRate );
  		Log.i(TAG,"CHANNEL: " + mChannel );
  
  		raf.close();
  
  		if (mSettings!=null) {
  			Editor editor = mSettings.edit();
  			editor.putString(key, mQuality.samplingRate+","+mConfig+","+mChannel);
  			editor.commit();
  		}
  
  		if (!file.delete()) Log.e(TAG,"Temp file could not be erased");
  
  	}
  
  }
  