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:

  1. Audio source: Indicates where audio is collected, usually a microphone.
  2. Sampling frequency: that is, the number of applications per second. 44100 Hz is the sampling frequency supported by all Android devices.
  3. Number of channels: Indicates how many channels a sound consists of. Mono is the number of channels supported by all Android devices.
  4. Quantization precision: Indicates how many bits of binary are used to express the discrete value of a quantization, usually 16 bits.
  5. 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 themoutputStream()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