/* * Copyright (C) 2021 pedroSG94. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pedro.encoder.input.audio; import android.annotation.SuppressLint; import android.media.AudioFormat; import android.media.AudioPlaybackCaptureConfiguration; import android.media.AudioRecord; import android.media.MediaRecorder; import android.media.projection.MediaProjection; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import com.pedro.encoder.Frame; import java.nio.ByteBuffer; /** * Created by pedro on 19/01/17. */ @SuppressLint("MissingPermission") public class MicrophoneManager { private final String TAG = "MicrophoneManager"; private int BUFFER_SIZE = 0; protected AudioRecord audioRecord; private final GetMicrophoneData getMicrophoneData; protected byte[] pcmBuffer = new byte[BUFFER_SIZE]; protected byte[] pcmBufferMuted = new byte[BUFFER_SIZE]; protected boolean running = false; private boolean created = false; //default parameters for microphone private int sampleRate = 32000; //hz private final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; private int channel = AudioFormat.CHANNEL_IN_STEREO; protected boolean muted = false; private AudioPostProcessEffect audioPostProcessEffect; protected HandlerThread handlerThread; protected CustomAudioEffect customAudioEffect = new NoAudioEffect(); public MicrophoneManager(GetMicrophoneData getMicrophoneData) { this.getMicrophoneData = getMicrophoneData; getPcmBufferSize(); } public void setCustomAudioEffect(CustomAudioEffect customAudioEffect) { this.customAudioEffect = customAudioEffect; } /** * Create audio record */ public void createMicrophone() { createMicrophone(sampleRate, true, false, false); Log.i(TAG, "Microphone created, " + sampleRate + "hz, Stereo"); } /** * Create audio record with params and default audio source */ public boolean createMicrophone(int sampleRate, boolean isStereo, boolean echoCanceler, boolean noiseSuppressor) { return createMicrophone(MediaRecorder.AudioSource.DEFAULT, sampleRate, isStereo, echoCanceler, noiseSuppressor); } /** * Create audio record with params and selected audio source * * @param audioSource - the recording source. See {@link MediaRecorder.AudioSource} for the * recording source definitions. */ public boolean createMicrophone(int audioSource, int sampleRate, boolean isStereo, boolean echoCanceler, boolean noiseSuppressor) { try { this.sampleRate = sampleRate; channel = isStereo ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO; audioRecord = new AudioRecord(audioSource, sampleRate, channel, audioFormat, getMaxInputSize() * 5); audioPostProcessEffect = new AudioPostProcessEffect(audioRecord.getAudioSessionId()); if (echoCanceler) audioPostProcessEffect.enableEchoCanceler(); if (noiseSuppressor) audioPostProcessEffect.enableNoiseSuppressor(); String chl = (isStereo) ? "Stereo" : "Mono"; if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { throw new IllegalArgumentException("Some parameters specified is not valid"); } Log.i(TAG, "Microphone created, " + sampleRate + "hz, " + chl); created = true; } catch (IllegalArgumentException e) { Log.e(TAG, "create microphone error", e); } return created; } /** * Create audio record with params and AudioPlaybackCaptureConfig used for capturing internal * audio * Notice that you should granted {@link android.Manifest.permission#RECORD_AUDIO} before calling * this! * * @param config - AudioPlaybackCaptureConfiguration received from {@link * android.media.projection.MediaProjection} * @see AudioPlaybackCaptureConfiguration.Builder#Builder(MediaProjection) * @see "https://developer.android.com/guide/topics/media/playback-capture" * @see "https://medium.com/@debuggingisfun/android-10-audio-capture-77dd8e9070f9" */ public boolean createInternalMicrophone(AudioPlaybackCaptureConfiguration config, int sampleRate, boolean isStereo, boolean echoCanceler, boolean noiseSuppressor) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.sampleRate = sampleRate; channel = isStereo ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO; audioRecord = new AudioRecord.Builder().setAudioPlaybackCaptureConfig(config) .setAudioFormat(new AudioFormat.Builder().setEncoding(audioFormat) .setSampleRate(sampleRate) .setChannelMask(channel) .build()) .setBufferSizeInBytes(getMaxInputSize() * 5) .build(); audioPostProcessEffect = new AudioPostProcessEffect(audioRecord.getAudioSessionId()); if (echoCanceler) audioPostProcessEffect.enableEchoCanceler(); if (noiseSuppressor) audioPostProcessEffect.enableNoiseSuppressor(); String chl = (isStereo) ? "Stereo" : "Mono"; if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { throw new IllegalArgumentException("Some parameters specified is not valid"); } Log.i(TAG, "Internal microphone created, " + sampleRate + "hz, " + chl); created = true; } else { return createMicrophone(sampleRate, isStereo, echoCanceler, noiseSuppressor); } } catch (IllegalArgumentException e) { Log.e(TAG, "create microphone error", e); } return created; } public boolean createInternalMicrophone(AudioPlaybackCaptureConfiguration config, int sampleRate, boolean isStereo) { return createInternalMicrophone(config, sampleRate, isStereo, false, false); } /** * Start record and get data */ public synchronized void start() { init(); handlerThread = new HandlerThread(TAG); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()); handler.post(new Runnable() { @Override public void run() { while (running) { Frame frame = read(); if (frame != null) { getMicrophoneData.inputPCMData(frame); } } } }); } private void init() { if (audioRecord != null) { audioRecord.startRecording(); running = true; Log.i(TAG, "Microphone started"); } else { Log.e(TAG, "Error starting, microphone was stopped or not created, " + "use createMicrophone() before start()"); } } public void mute() { muted = true; } public void unMute() { muted = false; } public boolean isMuted() { return muted; } /** * @return Object with size and PCM buffer data */ protected Frame read() { int size = audioRecord.read(pcmBuffer, 0, pcmBuffer.length); if (size < 0) return null; return new Frame(muted ? pcmBufferMuted : customAudioEffect.process(pcmBuffer), 0, size); } /** * Stop and release microphone */ public synchronized void stop() { running = false; created = false; if (handlerThread != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { handlerThread.quitSafely(); } else { handlerThread.quit(); } } if (audioRecord != null) { audioRecord.setRecordPositionUpdateListener(null); audioRecord.stop(); audioRecord.release(); audioRecord = null; } if (audioPostProcessEffect != null) { audioPostProcessEffect.release(); } Log.i(TAG, "Microphone stopped"); } /** * Get PCM buffer size */ private void getPcmBufferSize() { BUFFER_SIZE = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat); pcmBuffer = new byte[BUFFER_SIZE]; pcmBufferMuted = new byte[BUFFER_SIZE]; } public int getMaxInputSize() { return BUFFER_SIZE; } public int getSampleRate() { return sampleRate; } public void setSampleRate(int sampleRate) { this.sampleRate = sampleRate; } public int getAudioFormat() { return audioFormat; } public int getChannel() { return channel; } public boolean isRunning() { return running; } public boolean isCreated() { return created; } }