This paper first analyzes the whole audio processing process conceptually: sampling – quantization – coding – compression coding. Then through the example code, analyze how to use AudioRecord & MediaRecorder in Android phone recording audio detailed process.
Basic knowledge of
Analog signal
Audio carries sound information, and sound is continuously changing information. In physics, the carrier carrying information is called signal, and the continuously changing information is called analog signal, which is shown in the following forms in the coordinate axis:
Computers can only deal with zeros and ones, or discrete values. Audio is an analog signal that has to be converted into discrete values before it can be processed by a computer. This conversion process, called analog signal digitization, is divided into three steps:
1. The sampling
Sampling is to discretize the continuous signal in time, that is, to collect instantaneous values point by point on the original analog signal at a specific time interval. The visual effect of sampling is as follows:
Continuous curves are replaced by discrete vertical lines. The denser these lines are, the closer they resemble the original analog signal.
In physics, sampling frequency is used to indicate the intensity of sampling, that is, the sampling times per second (sampling number/second), which is expressed in Hertz (Hz)
2. The quantitative
Although continuous values have been sampled into discrete values, each discrete value may have an infinite number of values. To give each discrete value a numeric code, an infinite number of values must be converted to a finite number of values (for a computer that can only handle binary, the possible number of values should be a multiple of 2). In physics, this method of rounding is called quantization. The quantized digital signal is shown in the figure below:
The quantized audio becomes rigid and angular, like the difference between a human and a robot.
3. The coding
Analog signals are sampled into discrete values, and each discrete value corresponds to a binary after quantization. The combination of these binary values according to time series is called coding.
The original data of audio is formed after sampling quantization coding. This original data format is called PCM (Pulse Code Modulation), which is the English expression of sampling quantization coding.
Files with the.pcm suffix are very, very large, which increases the cost of storage and network transmission. So the original lossless audio like PCM has to go through a compression code.
Audio can only be compressed if there is redundant information. For example, the range of sound frequency that can be recognized by human ear is 20Hz ~ 20KHz, and the sound beyond this frequency is redundant information. Another example is that strong and weak signals appear at the same time, and the gap between strong and weak signals is so large that weak signals are completely covered up. Weak signals are redundant information.
There are many formats for audio compression and encoding. Here are some of the formats supported by Android:
The most commonly used format on mobile is AAC (Advanced Audio Coding), which is a file compression format designed for Audio data. It uses a more efficient encoding method, which makes it have the same sound quality as MP3 and smaller size.
Compression coding is executed by GPU or CPU in two ways. The former is called hard coding and the latter is called soft coding. Hard coding is fast but not compatible, and there will be coding failure. Soft coding speed is slow, but compatibility is good.
Record PCM audio
Android provides two ways to record audio: 1. MediaRecorder 2. AudioRecord
If there is no need to optimize audio, it is perfectly possible to use MediaRecorder to directly output audio in AAC format.
Audio optimization, such as noise reduction, gain algorithms are based on PCM format. This makes it necessary to use an AudioRecord to record audio.
Build an AudioRecord object
The AudioRecord constructor takes six arguments:
- Audio source: Indicates where audio is collected, usually a microphone.
- Sampling frequency: that is, the number of applications per second. 44100 Hz is the sampling frequency supported by all Android devices.
- Number of channels: Indicates how many channels a sound consists of. Mono is the number of channels supported by all Android devices.
- Quantization precision: Indicates how many bits of binary are used to express the discrete value of a quantization, usually 16 bits.
- Buffer size: Indicates the size of the memory buffer used to store audio data collected by the hardware.
The template code for building AudioRecord is as follows:
const val SOURCE = MediaRecorder.AudioSource.MIC // Use the microphone to collect audio
const val SAMPLE_RATE = 44100 // The sampling frequency is 44100 Hz
const val CHANNEL_IN_MONO = AudioFormat.CHANNEL_IN_MONO / / mono
const val ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT // The quantization accuracy is 16 bits
var bufferSize: Int = 0 // Audio buffer size
val audioRecord by lazy {
// Calculate the buffer size
bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
// Build an AudioRecord instance
AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize)
}
Copy the code
The parameters that build the AudioRecord are quantified so that they can be referenced elsewhere. The buffer size is through the AudioRecord. GetMinBufferSize () dynamic calculation, calculation is based on the sampling rate, channel number and quantitative accuracy.
Read audio data and write files
With an AudioRecord instance, you can call its methods to read audio data from the hardware device. It provides three methods to control the reading of audio data: startRecording(), read a batch of audio data, and stop recording stop(). These three methods are usually combined with the following template:
audioRecord.startRecording()
whileAudiorecord.read ()} AudioRecord.stop ()Copy the code
The size of the audio data is in bytes, and the audio data is read in batches, so a while loop keeps reading, and the number of bytes per read is determined by the size of the requested buffer.
A PCM file is formed by storing audio bytes read from a hardware device in a byte array and then writing the byte array to a file:
var bufferSize: Int = 0 // Audio buffer size
val outputFile:File / / PCM file
val audioRecord: AudioRecord
// Build the PCM file output stream
outputFile.outputStream().use { outputStream ->
// Start recording
audioRecord.startRecording()
// Build an array of bytes to hold audio data
val audioData = ByteArray(bufferSize)// Correspond to Java byte[]
// Keep reading audio data
while (continueRecord()) {
// Read a batch of audio data into a byte array
audioRecord.read(audioData, 0, audioData.size)
// Write the byte array to the PCM file through the output stream
outputStream.write(audioData)
}
// Stop recording
audioRecord.stop()
}
Copy the code
- Among them
outputStream()
Is an extension of File that makes code speech clearer and more continuous:
public inline fun File.outputStream(a): FileOutputStream {
return FileOutputStream(this)}Copy the code
use()
Is a Closeable extension method, no matter what happens in the enduse()
Will be calledclose()
To close the resource. This avoids template code for stream operations and reduces code complexity:
public inline fun
T.use(block: (T) - >R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
var exception: Throwable? = null
try {
// Execute the incoming lambda in the try block
return block(this)}catch (e: Throwable) {
exception = e
throw e
} finally {
// Execute close() in finally
when {
apiVersionIsAtLeast(1.1.0) - >this.closeFinally(exception)
this= =null -> {}
exception == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {}
}
}
}
Copy the code
- While IO operations are time-consuming, the code that reads and writes audio data should be executed in a non-UI thread. Whether to continue recording should be triggered by the user action, the UI thread. There is a multithreaded safety issue here, which requires a thread-safe Boolean to control audio recording:
var isRecording = AtomicBoolean(false) // Thread-safe Boolean variables
val audioRecord: AudioRecord
// Whether to continue recording
fun continueRecord(a): Boolean {
return isRecording.get() &&
audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING
}
// Stop recording audio (while loop called by the business layer to stop recording)
fun stop(a) {
isRecording.set(false)}Copy the code
Decoupling the abstract
Abstract all operations on an AudioRecord into an interface:
interface Recorder {
var outputFormat: String // Output audio format
fun isRecording(a): Boolean // Whether to record
fun getDuration(a): Long // Get the audio duration
fun start(outputFile: File, maxDuration: Int) // Start recording
fun stop(a) // Stop recording
fun release(a) // Release recording resources
}
Copy the code
This interface provides abstraction for recording audio. When upper-level classes work with this set of interfaces, they don’t need to worry about the implementation details of recording audio, that is, not coupling to an AudioRecord.
Why an extra layer of abstraction? Because the implementation is always volatile, in case the business layer needs to directly generate AAC files one day, you can easily replace the original implementation by adding a Recorder instance.
Give the implementation of AudioRecord for the Recorder interface:
class AudioRecorder(override var outputFormat: String) : Recorder {
private var bufferSize: Int = 0 // The size of the audio byte buffer
private var isRecording = AtomicBoolean(false) // Thread-safe Boolean value used to control audio recording
private var startTime = 0L // Record the start time of audio recording
private var duration = 0L // Audio duration
/ / AudioRecord instance
private val audioRecord by lazy {
bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize)
}
// Whether to record
override fun isRecording(a): Boolean = isRecording.get(a)// Get the audio duration
override fun getDuration(a): Long = duration
// Start audio recording
override fun start(outputFile: File, maxDuration: Int) {
if (audioRecord.state == AudioRecord.STATE_UNINITIALIZED) return
isRecording.set(true) // Mark the start of recording in the asynchronous thread
startTime.set(SystemClock.elapsedRealtime()) // Record the start time in the asynchronous thread
// Create a file output stream
outputFile.outputStream().use { outputStream ->
// Start recording
audioRecord.startRecording()
val audioData = ByteArray(bufferSize)
// Keep reading audio data into a byte array, and then write the byte array to a file
while (continueRecord(maxDuration)) {
audioRecord.read(audioData, 0, audioData.size)
outputStream.write(audioData)
}
// After the loop ends, notify the bottom layer of the end of recording
if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop()
}
// If the recording length exceeds the maximum length, it is called back to the upper layer
if (duration >= maxDuration) handleRecordEnd(isSuccess = true, isReachMaxTime = true)}}// Check whether the recording can continue
private fun continueRecord(maxDuration: Int): Boolean {
// Calculate the recording duration in real time
duration = SystemClock.elapsedRealtime() - startTime.get(a)return isRecording.get() &&
audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING &&
duration < maxDuration
}
// Stop recording (called in the UI thread)
override fun stop(a) {
isRecording.set(false)}// Release recording resources
override fun release(a) {
audioRecord.release()
}
}
Copy the code
The following is the implementation of MediaRecorder interface to Recorder:
inner class MediaRecord(override var outputFormat: String) : Recorder {
private var starTime = AtomicLong() // Start time of audio recording
// Listen for a callback to see if recording times out
private val listener = MediaRecorder.OnInfoListener { _, what, _ ->
when (what) {
MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED -> {
// If recording times out, stopping recording is called back to the upper layer
stop()
handleRecordEnd(isSuccess = true, isReachMaxTime = true)}else -> {
handleRecordEnd(isSuccess = false, isReachMaxTime = false)}}}// Record the error listener
private val errorListener = MediaRecorder.OnErrorListener { _, _, _ ->
handleRecordEnd(isSuccess = false, isReachMaxTime = false)}private val recorder = MediaRecorder()
private var isRecording = AtomicBoolean(false) // Thread-safe Boolean value used to control audio recording
private var duration = 0L // Audio duration
// Determine whether audio is being recorded
override fun isRecording(a): Boolean = isRecording.get(a)// Record the duration of the audio
override fun getDuration(a): Long = duration
// Start recording audio
override fun start(outputFile: File, maxDuration: Int) {
// Enumerates audio output formats
val format = when (outputFormat) {
AMR -> MediaRecorder.OutputFormat.AMR_NB
else -> MediaRecorder.OutputFormat.AAC_ADTS
}
// Enumerates audio encoding formats
val encoder = when (outputFormat) {
AMR -> MediaRecorder.AudioEncoder.AMR_NB
else -> MediaRecorder.AudioEncoder.AAC
}
// Start recording
starTime.set(SystemClock.elapsedRealtime())
isRecording.set(true)
recorder.apply {
reset()
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(format)
setOutputFile(outputFile.absolutePath)
setAudioEncoder(encoder)
setOnInfoListener(listener)
setOnErrorListener(errorListener)
setMaxDuration(maxDuration)
prepare()
start()
}
}
// Stop recording
override fun stop(a) {
recorder.stop()
isRecording.set(false)
duration = SystemClock.elapsedRealtime() - starTime.get()}// Release recording resources
override fun release(a) {
recorder.release()
}
}
Copy the code
The upper class that deals with the Recorder interface is defined as AudioManager, which is the entry point for the business layer to access the audio capability and provides a set of access interfaces:
// The context and audio output format are passed in to construct an AudioManager
class AudioManager(val context: Context, val type: String = AAC) {
companion object {
const val AAC = "aac"
const val AMR = "amr"
const val PCM = "pcm"
}
private var maxDuration = 120 * 1000 // The default maximum audio duration is 120 s
// Instantiate the corresponding Recorder instance according to the output format
private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
// Start recording
fun start(maxDuration: Int = 120) {
this.maxDuration = maxDuration * 1000
startRecord()
}
// Stop recording
fun stop(cancel: Boolean = false) {
stopRecord(cancel)
}
// Release resources
fun release(a) {
recorder.release()
}
// Whether to record
fun isRecording(a) = recorder.isRecording()
}
Copy the code
StartRecord () and stopRecord() contain the logic of AudioManager layer to control playback:
class AudioManager(val context: Context, val type: String = AAC) :
// To facilitate coroutine recording, directly inherit the CoroutineScope and dispatch the coroutine to a single thread pool corresponding to the Dispatcher
CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
// Start recording
private fun startRecord(a) {
// Request audio focus
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
// Returns if audio is being recorded
if (recorder.isRecording()) {
setState(STATE_FAILED) // Set the status to failed
return
}
// Returns if there are not enough memory card controls
if (getFreeSpace() <= 0) {
setState(STATE_FAILED) // Set the status to failed
return
}
// Create an audio file
audioFile = getAudioFile()
// If the creation fails, return
if (audioFile == null) setState(STATE_FAILED) // Set the status to failed
cancelRecord.set(false)
try {
if (! cancelRecord.get()) {
setState(STATE_READY) // Set the state to ready
if (hasPermission()) { // Have recording and storage permissions
// Start coroutine to start recordinglaunch { recorder.start(audioFile !! , maxDuration) } setState(STATE_START)// Set the state to start
} else {
stopRecord(false) // Stop recording if you do not have permission}}}catch (e: Exception) {
e.printStackTrace()
stopRecord(false) // Stop recording when an exception occurs}}// To stop recording, check whether the user actively cancels recording
private fun stopRecord(cancel: Boolean) {
// If not in recording, return
if (! recorder.isRecording()) {
return
}
cancelRecord.set(cancel)
// Drop the audio focus
audioManager.abandonAudioFocus(null)
try {
// Stop recording
recorder.stop()
} catch (e: Exception) {
e.printStackTrace()
} finally {
// Callback status after recording ends
handleRecordEnd(isSuccess = true, isReachMaxTime = false)}}}Copy the code
Since AudioManager is a class that works with the business layer, this layer has a lot of control logic, including the capture of audio focus, storage and recording permissions, creation of sound files, and recording state callbacks.
The recording state callback is defined as a number of lambdas:
class AudioManager(val context: Context, val type: String = AAC) :
CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
// State constant
private val STATE_FAILED = 1
private val STATE_READY = 2
private val STATE_START = 3
private val STATE_SUCCESS = 4
private val STATE_CANCELED = 5
private val STATE_REACH_MAX_TIME = 6
// Main thread Handler, used to call back the state in the main thread
private val callbackHandler = Handler(Looper.getMainLooper())
// Call back the recording status to the business layer's lambda
var onRecordReady: (() -> Unit)? = null
var onRecordStart: ((File) -> Unit)? = null
var onRecordSuccess: ((File, Long) - >Unit)? = null
var onRecordFail: (() -> Unit)? = null
var onRecordCancel: (() -> Unit)? = null
var onRecordReachedMaxTime: ((Int) - >Unit)? = null
// Status changes
private fun setState(state: Int) {
callbackHandler.post {
when(state) { STATE_FAILED -> onRecordFail? .invoke() STATE_READY -> onRecordReady? .invoke() STATE_START -> audioFile? .let { onRecordStart? .invoke(it) } STATE_CANCELED -> onRecordCancel? .invoke() STATE_SUCCESS -> audioFile? .let { onRecordSuccess? .invoke(it, recorder.getDuration()) } STATE_REACH_MAX_TIME -> onRecordReachedMaxTime? .invoke(maxDuration) } } } }Copy the code
Encapsulate the details of the state distribution callback into the setState() method to reduce the complexity of the recording process control code.
The complete AudioManager code is as follows:
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioFormat.CHANNEL_IN_MONO
import android.media.AudioFormat.ENCODING_PCM_16BIT
import android.media.AudioManager
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import java.io.File
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
/** * provide the ability to record audio in file. * [AudioManager] exists for the sake of the following: * 1. launch a thread to record audio in file. * 2. control the state of recording and invoke according callbacks in main thread. * 3. provide interface for the business layer to control audio recording */
class AudioManager(val context: Context, val type: String = AAC) :
CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
companion object {
const val AAC = "aac"
const val AMR = "amr"
const val PCM = "pcm"
const val SOURCE = MediaRecorder.AudioSource.MIC
const val SAMPLE_RATE = 44100
const val CHANNEL = 1
}
private val STATE_FAILED = 1
private val STATE_READY = 2
private val STATE_START = 3
private val STATE_SUCCESS = 4
private val STATE_CANCELED = 5
private val STATE_REACH_MAX_TIME = 6
/** * the callback business layer cares about */
var onRecordReady: (() -> Unit)? = null
var onRecordStart: ((File) -> Unit)? = null
var onRecordSuccess: ((File, Long) - >Unit)? = null// deliver audio file and duration to business layer
var onRecordFail: (() -> Unit)? = null
var onRecordCancel: (() -> Unit)? = null
var onRecordReachedMaxTime: ((Int) - >Unit)? = null
/** * deliver recording state to business layer */
private val callbackHandler = Handler(Looper.getMainLooper())
private var maxDuration = 120 * 1000
private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
private var audioFile: File? = null
private var cancelRecord: AtomicBoolean = AtomicBoolean(false)
private val audioManager: AudioManager = context.applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
fun start(maxDuration: Int = 120) {
this.maxDuration = maxDuration * 1000
startRecord()
}
fun stop(cancel: Boolean = false) {
stopRecord(cancel)
}
fun release(a) {
recorder.release()
}
fun isRecording(a) = recorder.isRecording()
private fun startRecord(a) {
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
if (recorder.isRecording()) {
setState(STATE_FAILED)
return
}
if (getFreeSpace() <= 0) {
setState(STATE_FAILED)
return
}
audioFile = getAudioFile()
if (audioFile == null) setState(STATE_FAILED)
cancelRecord.set(false)
try {
if (! cancelRecord.get()) {
setState(STATE_READY)
if(hasPermission()) { launch { recorder.start(audioFile !! , maxDuration) } setState(STATE_START) }else {
stopRecord(false)}}}catch (e: Exception) {
e.printStackTrace()
stopRecord(false)}}private fun hasPermission(a): Boolean {
return context.checkCallingOrSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
&& context.checkCallingOrSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}
private fun stopRecord(cancel: Boolean) {
if (! recorder.isRecording()) {
return
}
cancelRecord.set(cancel)
audioManager.abandonAudioFocus(null)
try {
recorder.stop()
} catch (e: Exception) {
e.printStackTrace()
} finally {
handleRecordEnd(isSuccess = true, isReachMaxTime = false)}}private fun handleRecordEnd(isSuccess: Boolean, isReachMaxTime: Boolean) {
if (cancelRecord.get()) { audioFile? .deleteOnExit() setState(STATE_CANCELED) }else if(! isSuccess) { audioFile? .deleteOnExit() setState(STATE_FAILED) }else {
if (isAudioFileInvalid()) {
setState(STATE_FAILED)
if (isReachMaxTime) {
setState(STATE_REACH_MAX_TIME)
}
} else {
setState(STATE_SUCCESS)
}
}
}
private fun isAudioFileInvalid(a) = audioFile == null| |! audioFile !! .exists() || audioFile !! .length() <=0
/** * change recording state and invoke according callback to main thread */
private fun setState(state: Int) {
callbackHandler.post {
when(state) { STATE_FAILED -> onRecordFail? .invoke() STATE_READY -> onRecordReady? .invoke() STATE_START -> audioFile? .let { onRecordStart? .invoke(it) } STATE_CANCELED -> onRecordCancel? .invoke() STATE_SUCCESS -> audioFile? .let { onRecordSuccess? .invoke(it, recorder.getDuration()) } STATE_REACH_MAX_TIME -> onRecordReachedMaxTime? .invoke(maxDuration) } } }private fun getFreeSpace(a): Long {
if(Environment.MEDIA_MOUNTED ! = Environment.getExternalStorageState()) {return 0L
}
return try {
valstat = StatFs(context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)? .absolutePath) stat.run { blockSizeLong * availableBlocksLong } }catch (e: Exception) {
0L}}private fun getAudioFile(a): File? {
valaudioFilePath = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)? .absolutePathif (audioFilePath.isNullOrEmpty()) return null
return File("$audioFilePath${File.separator}${UUID.randomUUID()}.$type")}/** * the implementation of [Recorder] define the detail of how to record audio. * [AudioManager] works with [Recorder] and dont care about the recording details */
interface Recorder {
/** * audio output format */
var outputFormat: String
/** * whether audio is recording */
fun isRecording(a): Boolean
/** * the length of audio */
fun getDuration(a): Long
/** * start audio recording, it is time-consuming */
fun start(outputFile: File, maxDuration: Int)
/** * stop audio recording */
fun stop(a)
/** * release the resource of audio recording */
fun release(a)
}
/** * record audio by [android.media.MediaRecorder] */
inner class MediaRecord(override var outputFormat: String) : Recorder {
private var starTime = AtomicLong()
private val listener = MediaRecorder.OnInfoListener { _, what, _ ->
when (what) {
MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED -> {
stop()
handleRecordEnd(isSuccess = true, isReachMaxTime = true)}else -> {
handleRecordEnd(isSuccess = false, isReachMaxTime = false)}}}private val errorListener = MediaRecorder.OnErrorListener { _, _, _ ->
handleRecordEnd(isSuccess = false, isReachMaxTime = false)}private val recorder = MediaRecorder()
private var isRecording = AtomicBoolean(false)
private var duration = 0L
override fun isRecording(a): Boolean = isRecording.get(a)override fun getDuration(a): Long = duration
override fun start(outputFile: File, maxDuration: Int) {
val format = when (outputFormat) {
AMR -> MediaRecorder.OutputFormat.AMR_NB
else -> MediaRecorder.OutputFormat.AAC_ADTS
}
val encoder = when (outputFormat) {
AMR -> MediaRecorder.AudioEncoder.AMR_NB
else -> MediaRecorder.AudioEncoder.AAC
}
starTime.set(SystemClock.elapsedRealtime())
isRecording.set(true)
recorder.apply {
reset()
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(format)
setOutputFile(outputFile.absolutePath)
setAudioEncoder(encoder)
if (outputFormat == AAC) {
setAudioSamplingRate(22050)
setAudioEncodingBitRate(32000)
}
setOnInfoListener(listener)
setOnErrorListener(errorListener)
setMaxDuration(maxDuration)
prepare()
start()
}
}
override fun stop(a) {
recorder.stop()
isRecording.set(false)
duration = SystemClock.elapsedRealtime() - starTime.get()}override fun release(a) {
recorder.release()
}
}
/** * record audio by [android.media.AudioRecord] */
inner class AudioRecorder(override var outputFormat: String) : Recorder {
private var bufferSize: Int = 0
private var isRecording = AtomicBoolean(false)
private var startTime = AtomicLong()
private var duration = 0L
private val audioRecord by lazy {
bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize)
}
override fun isRecording(a): Boolean = isRecording.get(a)override fun getDuration(a): Long = duration
override fun start(outputFile: File, maxDuration: Int) {
if (audioRecord.state == AudioRecord.STATE_UNINITIALIZED) return
isRecording.set(true)
startTime.set(SystemClock.elapsedRealtime())
outputFile.outputStream().use { outputStream ->
audioRecord.startRecording()
val audioData = ByteArray(bufferSize)
while (continueRecord(maxDuration)) {
audioRecord.read(audioData, 0, audioData.size)
outputStream.write(audioData)
}
if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop()
}
if (duration >= maxDuration) handleRecordEnd(isSuccess = true, isReachMaxTime = true)}}private fun continueRecord(maxDuration: Int): Boolean {
duration = SystemClock.elapsedRealtime() - startTime.get(a)return isRecording.get() && audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING && duration < maxDuration
}
override fun stop(a) {
isRecording.set(false)}override fun release(a) {
audioRecord.release()
}
}
}
Copy the code
The next article will pick up on this topic and look at how to hardcode PCM files into AAC files using MediaCodec.
talk is cheap, show me the code
The complete code is in the AudioManager class in the REPo