All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.webrtc.audio.WebRtcAudioRecord Maven / Gradle / Ivy

There is a newer version: 2.3.3.1
Show newest version
/*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc.audio;

import java.nio.ByteBuffer;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webrtc.CalledByNative;
import org.webrtc.Logging;
import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordErrorCallback;
import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordStartErrorCode;
import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordStateCallback;
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;

import io.antmedia.webrtc.api.IAudioRecordListener;

public class WebRtcAudioRecord {
	
	private static Logger logger = LoggerFactory.getLogger(WebRtcAudioRecord.class);

  private static final String TAG = "WebRtcAudioRecordExternal";

  // Requested size of each recorded buffer provided to the client.
  private static final int CALLBACK_BUFFER_SIZE_MS = 10;

  // Average number of callbacks per second.
  private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS;

  // We ask for a native buffer size of BUFFER_SIZE_FACTOR * (minimum required
  // buffer size). The extra space is allocated to guard against glitches under
  // high load.
  private static final int BUFFER_SIZE_FACTOR = 2;

  // The AudioRecordJavaThread is allowed to wait for successful call to join()
  // but the wait times out afther this amount of time.
  private static final long AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS = 2000;

  public static final int DEFAULT_AUDIO_SOURCE = 0; //AudioSource.VOICE_COMMUNICATION;

  // Default audio data format is PCM 16 bit per sample.
  // Guaranteed to be supported by all devices.
  public static final int DEFAULT_AUDIO_FORMAT = 0; //AudioFormat.ENCODING_PCM_16BIT;

  // Indicates AudioRecord has started recording audio.
  private static final int AUDIO_RECORD_START = 0;

  // Indicates AudioRecord has stopped recording audio.
  private static final int AUDIO_RECORD_STOP = 1;

  // Time to wait before checking recording status after start has been called. Tests have
  // shown that the result can sometimes be invalid (our own status might be missing) if we check
  // directly after start.
  private static final int CHECK_REC_STATUS_DELAY_MS = 100;

  //private final Context context;
  //private final AudioManager audioManager;
  private final int audioSource;
  private final int audioFormat;

  private long nativeAudioRecord;

  //private final WebRtcAudioEffects effects = new WebRtcAudioEffects();

  private @Nullable ByteBuffer byteBuffer;

 // private @Nullable AudioRecord audioRecord;
 // private @Nullable AudioRecordThread audioThread;

  private @Nullable ScheduledExecutorService executor;
  private @Nullable ScheduledFuture future;

  private volatile boolean microphoneMute;
  private boolean audioSourceMatchesRecordingSession;
  private boolean isAudioConfigVerified;
  private byte[] emptyBytes;

  private final @Nullable AudioRecordErrorCallback errorCallback;
  private final @Nullable AudioRecordStateCallback stateCallback;
  private final @Nullable SamplesReadyCallback audioSamplesReadyCallback;
  private final boolean isAcousticEchoCancelerSupported;
  private final boolean isNoiseSuppressorSupported;

  private IAudioRecordListener audioRecordListener;
  
  private ByteBuffer encodedByteBuffer;

  /**
   * Audio thread which keeps calling ByteBuffer.read() waiting for audio
   * to be recorded. Feeds recorded data to the native counterpart as a
   * periodic sequence of callbacks using DataIsRecorded().
   * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority.
   */
  /*
  private class AudioRecordThread extends Thread {
    private volatile boolean keepAlive = true;

    public AudioRecordThread(String name) {
      super(name);
    }

    @Override
    public void run() {
      Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
      Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo());
      assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING);

      // Audio recording has started and the client is informed about it.
      doAudioRecordStateCallback(AUDIO_RECORD_START);

      long lastTime = System.nanoTime();
      while (keepAlive) {
        int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
        if (bytesRead == byteBuffer.capacity()) {
          if (microphoneMute) {
            byteBuffer.clear();
            byteBuffer.put(emptyBytes);
          }
          // It's possible we've been shut down during the read, and stopRecording() tried and
          // failed to join this thread. To be a bit safer, try to avoid calling any native methods
          // in case they've been unregistered after stopRecording() returned.
          if (keepAlive) {
            nativeDataIsRecorded(nativeAudioRecord, bytesRead);
          }
          if (audioSamplesReadyCallback != null) {
            // Copy the entire byte buffer array. The start of the byteBuffer is not necessarily
            // at index 0.
            byte[] data = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.arrayOffset(),
                byteBuffer.capacity() + byteBuffer.arrayOffset());
            audioSamplesReadyCallback.onWebRtcAudioRecordSamplesReady(
                new JavaAudioDeviceModule.AudioSamples(audioRecord.getAudioFormat(),
                    audioRecord.getChannelCount(), audioRecord.getSampleRate(), data));
          }
        } else {
          String errorMessage = "AudioRecord.read failed: " + bytesRead;
          Logging.e(TAG, errorMessage);
          if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
            keepAlive = false;
            reportWebRtcAudioRecordError(errorMessage);
          }
        }
      }

      try {
        if (audioRecord != null) {
          audioRecord.stop();
          doAudioRecordStateCallback(AUDIO_RECORD_STOP);
        }
      } catch (IllegalStateException e) {
        Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage());
      }
    }

    // Stops the inner thread loop and also calls AudioRecord.stop().
    // Does not block the calling thread.
    public void stopThread() {
      Logging.d(TAG, "stopThread");
      keepAlive = false;
    }
  }
  */

  @CalledByNative
  WebRtcAudioRecord(Object context, Object audioManager) {
    this(context, audioManager, DEFAULT_AUDIO_SOURCE, DEFAULT_AUDIO_FORMAT,
        null /* errorCallback */, null /* stateCallback */, null /* audioSamplesReadyCallback */,
        false, false, null);
  }

  public WebRtcAudioRecord(Object context, Object audioManager, int audioSource,
      int audioFormat, @Nullable AudioRecordErrorCallback errorCallback,
      @Nullable AudioRecordStateCallback stateCallback,
      @Nullable SamplesReadyCallback audioSamplesReadyCallback,
      boolean isAcousticEchoCancelerSupported, boolean isNoiseSuppressorSupported, IAudioRecordListener audioRecordListener) {
//    if (isAcousticEchoCancelerSupported && !WebRtcAudioEffects.isAcousticEchoCancelerSupported()) {
//      throw new IllegalArgumentException("HW AEC not supported");
//    }
//    if (isNoiseSuppressorSupported && !WebRtcAudioEffects.isNoiseSuppressorSupported()) {
//      throw new IllegalArgumentException("HW NS not supported");
//    }
//    this.context = context;
//    this.audioManager = audioManager;
    this.audioSource = audioSource;
    this.audioFormat = audioFormat;
    this.errorCallback = errorCallback;
    this.stateCallback = stateCallback;
    this.audioSamplesReadyCallback = audioSamplesReadyCallback;
    this.isAcousticEchoCancelerSupported = isAcousticEchoCancelerSupported;
    this.isNoiseSuppressorSupported = isNoiseSuppressorSupported;
    this.audioRecordListener = audioRecordListener;
 //   Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
  }

  @CalledByNative
  public void setNativeAudioRecord(long nativeAudioRecord) {
    this.nativeAudioRecord = nativeAudioRecord;
  }

  @CalledByNative
  boolean isAcousticEchoCancelerSupported() {
    return isAcousticEchoCancelerSupported;
  }

  @CalledByNative
  boolean isNoiseSuppressorSupported() {
    return isNoiseSuppressorSupported;
  }

  // Returns true if a valid call to verifyAudioConfig() has been done. Should always be
  // checked before using the returned value of isAudioSourceMatchingRecordingSession().
  @CalledByNative
  boolean isAudioConfigVerified() {
    return isAudioConfigVerified;
  }

  // Returns true if verifyAudioConfig() succeeds. This value is set after a specific delay when
  // startRecording() has been called. Hence, should preferably be called in combination with
  // stopRecording() to ensure that it has been set properly. |isAudioConfigVerified| is
  // enabled in WebRtcAudioRecord to ensure that the returned value is valid.
  @CalledByNative
  boolean isAudioSourceMatchingRecordingSession() {
    if (!isAudioConfigVerified) {
      Logging.w(TAG, "Audio configuration has not yet been verified");
      return false;
    }
    return audioSourceMatchesRecordingSession;
  }

  @CalledByNative
  private boolean enableBuiltInAEC(boolean enable) {
    Logging.d(TAG, "enableBuiltInAEC(" + enable + ")");
    return false;
  }

  @CalledByNative
  private boolean enableBuiltInNS(boolean enable) {
    Logging.d(TAG, "enableBuiltInNS(" + enable + ")");
    return false;
  }

  @CalledByNative
  private int initRecording(int sampleRate, int channels) {
	  	System.out.println("initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")");

	  	//TODO: Different Audio format other than PCM_16 may be supported 
	  	//below 2 is BITS_PER_SAMPLE(16)/8 = 2 
		final int bytesPerFrame = channels * 2;
		final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
		byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
		//if (!(byteBuffer.hasArray())) {
		//	reportWebRtcAudioRecordInitError("ByteBuffer does not have backing array.");
		//	return -1;
		//}
		System.out.println("byteBuffer.capacity: " + byteBuffer.capacity());
		emptyBytes = new byte[byteBuffer.capacity()];
		// Rather than passing the ByteBuffer with every callback (requiring
		// the potentially expensive GetDirectBufferAddress) we simply have the
		// the native class cache the address to the memory once.
		nativeCacheDirectBufferAddress(nativeAudioRecord, byteBuffer);

		encodedByteBuffer = ByteBuffer.allocateDirect(byteBuffer.capacity()*10);
		nativeCacheDirectBufferAddressForEncodedAudio(nativeAudioRecord, encodedByteBuffer);

		return framesPerBuffer;
  }

  @CalledByNative
  private boolean startRecording() {
    Logging.d(TAG, "startRecording");
    
    if (audioRecordListener != null) {
		audioRecordListener.audioRecordStarted();
	}
    return true;
  }

  @CalledByNative
  private boolean stopRecording() {
    Logging.d(TAG, "stopRecording");

    if (audioRecordListener != null) {
		audioRecordListener.audioRecordStoppped();
		audioRecordListener = null;
	}
    return true;
  }

  // Helper method which throws an exception  when an assertion has failed.
  private static void assertTrue(boolean condition) {
    if (!condition) {
      throw new AssertionError("Expected condition to be true");
    }
  }

//  private int channelCountToConfiguration(int channels) {
//    return (channels == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO);
//  }

  private native void nativeCacheDirectBufferAddress(
      long nativeAudioRecordJni, ByteBuffer byteBuffer);
  private native void nativeDataIsRecorded(long nativeAudioRecordJni, int bytes);
  
  private native void nativeCacheDirectBufferAddressForEncodedAudio(
			long nativeAudioRecordJni, ByteBuffer byteBuffer);

  private native void nativeEncodedDataIsReady(long nativeAudioRecordJni, int bytes);


  // Sets all recorded samples to zero if |mute| is true, i.e., ensures that
  // the microphone is muted.
  public void setMicrophoneMute(boolean mute) {
    Logging.w(TAG, "setMicrophoneMute(" + mute + ")");
    microphoneMute = mute;
  }


  private void reportWebRtcAudioRecordInitError(String errorMessage) {
    Logging.e(TAG, "Init recording error: " + errorMessage);
    //WebRtcAudioUtils.logAudioState(TAG, context, audioManager);
    //logRecordingConfigurations(false /* verifyAudioConfig */);
    if (errorCallback != null) {
      errorCallback.onWebRtcAudioRecordInitError(errorMessage);
    }
  }

  private void reportWebRtcAudioRecordStartError(
      AudioRecordStartErrorCode errorCode, String errorMessage) {
    Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage);
    //WebRtcAudioUtils.logAudioState(TAG, context, audioManager);
    //logRecordingConfigurations(false /* verifyAudioConfig */);
    if (errorCallback != null) {
      errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage);
    }
  }

  private void reportWebRtcAudioRecordError(String errorMessage) {
    Logging.e(TAG, "Run-time recording error: " + errorMessage);
    //WebRtcAudioUtils.logAudioState(TAG, context, audioManager);
    if (errorCallback != null) {
      errorCallback.onWebRtcAudioRecordError(errorMessage);
    }
  }

  private void doAudioRecordStateCallback(int audioState) {
    Logging.d(TAG, "doAudioRecordStateCallback: " + audioStateToString(audioState));
    if (stateCallback != null) {
      if (audioState == WebRtcAudioRecord.AUDIO_RECORD_START) {
        stateCallback.onWebRtcAudioRecordStart();
      } else if (audioState == WebRtcAudioRecord.AUDIO_RECORD_STOP) {
        stateCallback.onWebRtcAudioRecordStop();
      } else {
        Logging.e(TAG, "Invalid audio state");
      }
    }
  }

  // Reference from Android code, AudioFormat.getBytesPerSample. BitPerSample / 8
  // Default audio data format is PCM 16 bits per sample.
  // Guaranteed to be supported by all devices
  /*
  private static int getBytesPerSample(int audioFormat) {
    switch (audioFormat) {
      case AudioFormat.ENCODING_PCM_8BIT:
        return 1;
      case AudioFormat.ENCODING_PCM_16BIT:
      case AudioFormat.ENCODING_IEC61937:
      case AudioFormat.ENCODING_DEFAULT:
        return 2;
      case AudioFormat.ENCODING_PCM_FLOAT:
        return 4;
      case AudioFormat.ENCODING_INVALID:
      default:
        throw new IllegalArgumentException("Bad audio format " + audioFormat);
    }
  }
  */
  

  // Use an ExecutorService to schedule a task after a given delay where the task consists of
  // checking (by logging) the current status of active recording sessions.
  
//  private void scheduleLogRecordingConfigurationsTask() {
//    Logging.d(TAG, "scheduleLogRecordingConfigurationsTask");
//    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
//      return;
//    }
//    if (executor != null) {
//      executor.shutdownNow();
//    }
//    executor = Executors.newSingleThreadScheduledExecutor();
//
//    Callable callable = () -> {
//      logRecordingConfigurations(true /* verifyAudioConfig */);
//      return "Scheduled task is done";
//    };
//
//    if (future != null && !future.isDone()) {
//      future.cancel(true /* mayInterruptIfRunning */);
//    }
//    // Schedule call to logRecordingConfigurations() from executor thread after fixed delay.
//    future = executor.schedule(callable, CHECK_REC_STATUS_DELAY_MS, TimeUnit.MILLISECONDS);
//  };
//
//  @TargetApi(Build.VERSION_CODES.N)
//  private static boolean logActiveRecordingConfigs(
//      int session, List configs) {
//    assertTrue(!configs.isEmpty());
//    final Iterator it = configs.iterator();
//    Logging.d(TAG, "AudioRecordingConfigurations: ");
//    while (it.hasNext()) {
//      final AudioRecordingConfiguration config = it.next();
//      StringBuilder conf = new StringBuilder();
//      // The audio source selected by the client.
//      final int audioSource = config.getClientAudioSource();
//      conf.append("  client audio source=")
//          .append(WebRtcAudioUtils.audioSourceToString(audioSource))
//          .append(", client session id=")
//          .append(config.getClientAudioSessionId())
//          // Compare with our own id (based on AudioRecord#getAudioSessionId()).
//          .append(" (")
//          .append(session)
//          .append(")")
//          .append("\n");
//      // Audio format at which audio is recorded on this Android device. Note that it may differ
//      // from the client application recording format (see getClientFormat()).
//      AudioFormat format = config.getFormat();
//      conf.append("  Device AudioFormat: ")
//          .append("channel count=")
//          .append(format.getChannelCount())
//          .append(", channel index mask=")
//          .append(format.getChannelIndexMask())
//          // Only AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all devices.
//          .append(", channel mask=")
//          .append(WebRtcAudioUtils.channelMaskToString(format.getChannelMask()))
//          .append(", encoding=")
//          .append(WebRtcAudioUtils.audioEncodingToString(format.getEncoding()))
//          .append(", sample rate=")
//          .append(format.getSampleRate())
//          .append("\n");
//      // Audio format at which the client application is recording audio.
//      format = config.getClientFormat();
//      conf.append("  Client AudioFormat: ")
//          .append("channel count=")
//          .append(format.getChannelCount())
//          .append(", channel index mask=")
//          .append(format.getChannelIndexMask())
//          // Only AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all devices.
//          .append(", channel mask=")
//          .append(WebRtcAudioUtils.channelMaskToString(format.getChannelMask()))
//          .append(", encoding=")
//          .append(WebRtcAudioUtils.audioEncodingToString(format.getEncoding()))
//          .append(", sample rate=")
//          .append(format.getSampleRate())
//          .append("\n");
//      // Audio input device used for this recording session.
//      final AudioDeviceInfo device = config.getAudioDevice();
//      if (device != null) {
//        assertTrue(device.isSource());
//        conf.append("  AudioDevice: ")
//            .append("type=")
//            .append(WebRtcAudioUtils.deviceTypeToString(device.getType()))
//            .append(", id=")
//            .append(device.getId());
//      }
//      Logging.d(TAG, conf.toString());
//    }
//    return true;
//  }
//

  

  private static String audioStateToString(int state) {
    switch (state) {
      case WebRtcAudioRecord.AUDIO_RECORD_START:
        return "START";
      case WebRtcAudioRecord.AUDIO_RECORD_STOP:
        return "STOP";
      default:
        return "INVALID";
    }
  }
  
  /**
	 * @param audio => 20ms of encoded audio data
	 */
	public void notifyEncodedData(ByteBuffer audio) {
		if (audio.limit() <= encodedByteBuffer.capacity()) {
			encodedByteBuffer.clear();
			audio.rewind();
			encodedByteBuffer.put(audio);
			nativeEncodedDataIsReady(nativeAudioRecord, audio.limit());
		}
		else {
			 logger.warn("Discarding audio packet because audio packet size({}) is bigger than buffer capacity{} and limit {}", audio.limit(), encodedByteBuffer.capacity(), encodedByteBuffer.limit());
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy