content: /* <!-- +++
   package com.almalence.opencam_plus.ui;
   +++ --> */
  // <!-- -+-
  package com.almalence.opencam.ui;
  //-+- -->
  
  import java.io.IOException;
  import java.nio.ByteBuffer;
  
  import android.annotation.SuppressLint;
  import android.media.AudioFormat;
  import android.media.AudioRecord;
  import android.media.MediaCodec;
  import android.media.MediaCodecInfo;
  import android.media.MediaFormat;
  import android.media.MediaMuxer;
  import android.media.MediaRecorder;
  import android.util.Log;
  
  @SuppressLint("NewApi")
  public class AudioRecorder
  {
  	public static final String		TAG					= "AudioRecorder";
  
  	public static final String		MIME_TYPE_AUDIO		= "audio/mp4a-latm";
  	public static final int			SAMPLE_RATE			= 44100;
  	public static final int			CHANNEL_COUNT		= 1;
  	public static final int			CHANNEL_CONFIG		= AudioFormat.CHANNEL_IN_MONO;
  	public static final int			BIT_RATE_AUDIO		= 128000;
  	public static final int			SAMPLES_PER_FRAME	= 1024;							// AAC
  	public static final int			FRAMES_PER_BUFFER	= 24;
  	public static final int			AUDIO_FORMAT		= AudioFormat.ENCODING_PCM_16BIT;
  	public static final int			AUDIO_SOURCE		= MediaRecorder.AudioSource.MIC;
  	public static final int			TIMEOUT_USEC		= 10000;
  
  	private final MediaMuxer		muxer;
  	private final EncodingThread	encodingThread;
  	private final Object			sync				= new Object();
  	private boolean					started				= false;
  	private boolean					running				= false;
  	private volatile boolean		record				= false;
  	private volatile long			time				= 0;
  	private volatile long			timeOrigin			= 0;
  
  	public AudioRecorder(final MediaMuxer muxer)
  	{
  		this.muxer = muxer;
  
  		this.encodingThread = new EncodingThread();
  	}
  
  	public void updateTime(final long time)
  	{
  		this.time = time;
  		this.timeOrigin = System.nanoTime();
  	}
  
  	public void record(final boolean record)
  	{
  		this.record = record;
  	}
  
  	public void start()
  	{
  		synchronized (this.sync)
  		{
  			if (this.started)
  			{
  				throw new IllegalStateException("Already started");
  			}
  
  			this.started = true;
  			this.running = true;
  
  			synchronized (this.encodingThread.sync)
  			{
  				this.encodingThread.start();
  
  				try
  				{
  					this.encodingThread.sync.wait();
  				} catch (final InterruptedException e)
  				{
  					Thread.currentThread().interrupt();
  				}
  			}
  		}
  	}
  
  	public void stop()
  	{
  		synchronized (this.sync)
  		{
  			if (!this.started)
  			{
  				throw new IllegalStateException("Not started");
  			} else if (!this.running)
  			{
  				throw new IllegalStateException("Already finished");
  			}
  
  			this.running = false;
  
  			this.encodingThread.finish();
  		}
  	}
  
  	private class EncodingThread extends Thread
  	{
  		private final Object		sync		= new Object();
  		private final MediaCodec	encoder;
  
  		private volatile boolean	running		= true;
  		private int					iBufferSize;
  		private long				timeLast	= 0;
  
  		public EncodingThread()
  		{
  			final int iMinBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
  
  			this.iBufferSize = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
  			if (this.iBufferSize < iMinBufferSize)
  			{
  				this.iBufferSize = ((iMinBufferSize / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;
  			}
  
  			final MediaFormat format = MediaFormat.createAudioFormat(MIME_TYPE_AUDIO, SAMPLE_RATE, CHANNEL_COUNT);
  			format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
  			format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384);
  			format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE_AUDIO);
  
  			MediaCodec newEncoder = null;
  			try
  			{
  				newEncoder = MediaCodec.createEncoderByType(MIME_TYPE_AUDIO);
  			}
  			catch (IOException e)
  			{
  				// TODO Auto-generated catch block
  				e.printStackTrace();
  			}
  			this.encoder = newEncoder;
  			this.encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  			this.encoder.start();
  		}
  
  		@Override
  		public void run()
  		{
  			final byte[] recordedBytes = new byte[SAMPLES_PER_FRAME];
  
  			final AudioRecord audioRecorder = new AudioRecord(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT,
  					this.iBufferSize);
  			audioRecorder.startRecording();
  
  			final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
  			int track = 0;
  			ByteBuffer[] buffersInput = null;
  			ByteBuffer[] buffersOutput = this.encoder.getOutputBuffers();
  			ByteBuffer encodedData;
  
  			while (this.running)
  			{
  				final int resultRead = audioRecorder.read(recordedBytes, 0, SAMPLES_PER_FRAME);
  
  				if (resultRead == AudioRecord.ERROR_BAD_VALUE || resultRead == AudioRecord.ERROR_INVALID_OPERATION)
  				{
  					break;
  				}
  
  				if (AudioRecorder.this.record)
  				{
  					try
  					{
  						if (buffersInput == null)
  							buffersInput = this.encoder.getInputBuffers();
  
  						int inputBufferIndex = this.encoder.dequeueInputBuffer(-1);
  						if (inputBufferIndex >= 0)
  						{
  							final ByteBuffer inputBuffer = buffersInput[inputBufferIndex];
  							inputBuffer.clear();
  							inputBuffer.put(recordedBytes);
  
  							this.timeLast = Math
  									.max(this.timeLast + 1,
  											(AudioRecorder.this.time + (System.nanoTime() - AudioRecorder.this.timeOrigin)) / 1000);
  
  							this.encoder.queueInputBuffer(inputBufferIndex, 0, recordedBytes.length, this.timeLast, 0);
  						}
  					} catch (final Throwable t)
  					{
  						Log.e(TAG, "sendFrameToAudioEncoder exception");
  						t.printStackTrace();
  						break;
  					}
  				}
  
  				final int encoderStatus = this.encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
  				if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER)
  				{
  
  				} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
  				{
  					// not expected for an encoder
  					buffersOutput = this.encoder.getOutputBuffers();
  				} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
  				{
  					final MediaFormat newFormat = this.encoder.getOutputFormat();
  
  					track = AudioRecorder.this.muxer.addTrack(newFormat);
  
  					synchronized (this.sync)
  					{
  						sync.notify();
  					}
  
  				} else if (encoderStatus < 0)
  				{
  					Log.w(TAG, "Unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
  				} else
  				{
  					encodedData = buffersOutput[encoderStatus];
  					if (encodedData == null)
  					{
  						throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
  					}
  
  					if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0)
  					{
  						// The codec config data was pulled out and fed
  						// to the muxer when we got
  						// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore
  						// it.
  						bufferInfo.size = 0;
  					}
  
  					if (bufferInfo.size != 0)
  					{
  						// adjust the ByteBuffer values to match
  						// BufferInfo (not needed?)
  						encodedData.position(bufferInfo.offset);
  						encodedData.limit(bufferInfo.offset + bufferInfo.size);
  
  						if (AudioRecorder.this.record)
  						{
  							synchronized (AudioRecorder.this.muxer)
  							{
  								AudioRecorder.this.muxer.writeSampleData(track, encodedData, bufferInfo);
  							}
  						}
  					}
  
  					this.encoder.releaseOutputBuffer(encoderStatus, false);
  
  					if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)
  					{
  						Log.w(TAG, "AudioRecorder: EOS reached.");
  						break;
  					}
  				}
  			}
  
  			audioRecorder.stop();
  			audioRecorder.release();
  
  			this.encoder.stop();
  			this.encoder.release();
  		}
  
  		public void finish()
  		{
  			this.running = false;
  		}
  	}
  
  }
  