Android 音频(一) | 采样量化编码 & AudioRecord 录制音频

简介: Android 音频(一) | 采样量化编码 & AudioRecord 录制音频

本篇先从概念上剖析了整个音频处理流程:采样-量化-编码-压缩编码。然后通过实例代码,分析如何使用 AudioRecord & MediaRecorder 在安卓手机上录制音频的详细过程。


基础知识


模拟信号


音频承载着声音信息,而声音是连续变化的信息。物理中把承载信息的载体称为信号,把连续变化的信息称为模拟信号,它在坐标轴中表现为如下形态:


image.png


计算机只能处理0和1,即离散值。音频这种模拟信号得转换成离散值才能被计算机处理。这个转化过程称为模拟信号数字化,分为三个步骤:


1. 采样


采样是对连续信号在时间上进行离散,即按照特定的时间间隔在原始的模拟信号上逐点采集瞬时值。采样的可视化效果如下图所示:


image.png


原本连续的曲线被一根根离散的竖直线条代替。这些线条越密集,将它们相连后形成的曲线就越接近原始模拟信号。


物理中用采样频率来表示采样的密集程度,即每秒采样次数(采样数/秒),它用赫兹(Hz)表示


2. 量化


虽然连续值已被采样成若干离散值,但每个离散值的取值可能有无限多个。为了给每一个离散值都对应一个数字码,必须将无限种取值转化为有限种取值(对于只能处理二进制的计算机来说,取值的可能数应该是2的倍数)。物理中把这种通过四舍五入分级取整的方法称为量化。量化后的数字信号如下图所示:


image.png


量化后的音频变得死板有棱角,就好像人类和机器人的差别。


3. 编码


模拟信号经过采样变成离散值,每一个离散值经过量化都对应一个二进制,将这些二进制按时间序列组合在一起就称为编码


经过采样量化编码形成的是音频的原始数据,这种原始数据格式称为PCM(Pulse Code Modulation),即是采样量化编码的英文表示。


.pcm 后缀的文件是非常非常大的,这增加了存储和网络传输的成本。遂 PCM 这样原始的无损音频还得经过一次压缩编码


音频存在冗余信息,才能被压缩。比如人耳能辨识的声音频率范围为20Hz~20KHz,该频率以外的声音都是冗余信息。再比如强弱信号同时出现,强弱差距过大,以至于弱信号完全被掩盖,弱信号就是冗余信息。


音频有很多压缩编码的格式,以下是 Android 官方支持的格式:


image.png


在移动端最为常用的格式是 AAC,即 Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。它采用了更加高效的编码方式,使得它拥有和 MP3 相当的音质及更小的体积。


压缩编码由两种执行方式,交由 GPU 或是 CPU 执行,前者称为硬编码后者称为软编码,硬编码速度快,但兼容差,会存在编码失败的情况。软编码速度慢,但兼容性好。


录制 PCM 音频


Android 提供了两种录制音频的方式:1. MediaRecorder 2. AudioRecord

如果没有优化音频的需求,完全可以使用 MediaRecorder 直接输出 AAC 格式的音频。


而音频优化,比如降噪,增益算法都是基于 PCM 格式的。这就不得不使用 AudioRecord 来录制音频。


构建 AudioRecord 对象


AudioRecord 的构造函数包含 6 个参数:


  1. 音频源:表示从哪里采集音频,通常是麦克风。


  1. 采样频率:即每秒钟采用次数,44100 Hz是目前所有安卓设备都支持的采样频率。


  1. 声道数:表示声音由几个声道组成,单声道是目前所有安卓设备都支持的声道数。


  1. 量化精度:表示采用多少位二进制来表达一次量化的离散值,通常用 16 位。


  1. 缓冲区大小:表示在内存开辟一块多大的缓冲区用于存放硬件采集的音频数据。


构建 AudioRecord 的模板代码如下:


const val SOURCE = MediaRecorder.AudioSource.MIC //通过麦克风采集音频
const val SAMPLE_RATE = 44100 // 采样频率为 44100 Hz
const val CHANNEL_IN_MONO = AudioFormat.CHANNEL_IN_MONO // 单声道
const val ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT //量化精度为 16 位
var bufferSize: Int = 0 // 音频缓冲区大小
val audioRecord by lazy {
    // 计算缓冲区大小
    bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
    // 构建 AudioRecord 实例
    AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize)
}


将构建 AudioRecord 的参数都常量化,以便在其他地方引用。其中缓冲区大小是通过AudioRecord.getMinBufferSize()动态计算的,计算的依据是采样平率、声道数、量化精度。


读取音频数据写入文件


有了 AudioRecord 实例,就可以调用它的方法从硬件设备中读取音频数据了。它提供了 3 个方法来控制音频数据的读取,分别是开始录制startRecording()、读一批音频数据read()、停止录制stop(),这 3 个方法通常用下面的模板来组合:


audioRecord.startRecording()
while(是否继续录制){ audioRecord.read() }
audioRecord.stop()


音频数据的大小以字节为单位,音频数据的读取是一批一批进行的,所以需要一个 while 循环持续不断地读取,每次读取多少字节由申请的缓冲区大小决定。


从硬件设备读取的音频字节先存放在字节数组中,然后再把字节数组写入文件就形成了 PCM 文件:


var bufferSize: Int = 0 // 音频缓冲区大小
val outputFile:File // pcm 文件
val audioRecord: AudioRecord
// 构建 pcm 文件输出流
outputFile.outputStream().use { outputStream ->
    // 开始录制
    audioRecord.startRecording()
    // 构建存放音频数据的字节数组
    val audioData = ByteArray(bufferSize)// 对应 java 中的 byte[]
    // 持续读取音频数据
    while (continueRecord()) {
        // 读一批音频数据到字节数组
        audioRecord.read(audioData, 0, audioData.size)
        // 将字节数组通过输出流写入 pcm 文件
        outputStream.write(audioData)
    }
    // 停止录制
    audioRecord.stop()
}


  • 其中outputStream()是 File 的一个扩展方法,它使得代码语音更清晰,更连续:


public inline fun File.outputStream(): FileOutputStream {
    return FileOutputStream(this)
}


  • use()是一个 Closeable 的扩展方法,不管发生了什么,最终use()都会调用close()来关闭资源。这就避免了流操作的模板代码,降低了代码的复杂度:


public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
        // 在 try 代码块中执行传入的 lambda
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        // 在 finally 中执行 close()
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {}
        }
    }
}


  • IO操作时耗时的,读写音频数据的代码应该在非UI线程中执行。而是否继续录制应该由用户动作触发,即UI线程触发。这里有多线程安全问题,需要一个线程安全的布尔值来控制音频录制:


var isRecording = AtomicBoolean(false) // 线程安全的布尔变量
val audioRecord: AudioRecord
// 是否继续录制
fun continueRecord(): Boolean {
    return isRecording.get() && 
           audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING
}
// 停止录制音频(供业务层调用以停止录音的 while 循环)
fun stop() {
    isRecording.set(false)
}


解耦抽象


将对 AudioRecord 的所有操作都抽象在一个接口中:


interface Recorder {
    var outputFormat: String // 输出音频格式
    fun isRecording(): Boolean // 是否正在录制
    fun getDuration(): Long // 获取音频时长
    fun start(outputFile: File, maxDuration: Int) // 开始录制
    fun stop() // 停止录制
    fun release() // 释放录制资源
}


这个接口提供了录制音频的抽象能力。当上层类和这组接口打交道时,不需要关心录制音频的实现细节,即不和 AudioRecord 耦合。


为啥要多一层这样的抽象?因为具体实现总是易变的,万一哪天业务层需要直接生成 AAC 文件,就可以通过添加一个Recorder的实例方便地地替换原有实现。


给出 AudioRecord 对于Recorder接口的实现:


class AudioRecorder(override var outputFormat: String) : Recorder {
    private var bufferSize: Int = 0 // 音频字节缓冲区大小
    private var isRecording = AtomicBoolean(false) // 用于控制音频录制的线程安全布尔值
    private var startTime = 0L // 记录音频开始录制时间
    private var duration = 0L // 音频时长
    // AudioRecord 实例
    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(): Boolean = isRecording.get()
    // 获取音频时长
    override fun getDuration(): 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()
        return isRecording.get() && 
               audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING && 
               duration < maxDuration
    }
    // 停止录音(在UI线程调用)
    override fun stop() {
        isRecording.set(false)
    }
    // 释放录音资源
    override fun release() {
        audioRecord.release()
    }
}


下面是 MediaRecorder 对于Recorder接口的实现:


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(): Boolean = isRecording.get()
    // 录制音频时长
    override fun getDuration(): 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)
            setOnInfoListener(listener)
            setOnErrorListener(errorListener)
            setMaxDuration(maxDuration)
            prepare()
            start()
        }
    }
    // 停止录制
    override fun stop() {
        recorder.stop()
        isRecording.set(false)
        duration = SystemClock.elapsedRealtime() - starTime.get()
    }
    // 释放录制资源
    override fun release() {
        recorder.release()
    }
}


把和Recorder接口打交道的上层类定义为AudioManager,它是业务层访问音频能力的入口,提供了一组访问接口:


// 构造 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 // 默认最大音频时长为 120 s
    // 根据输出格式实例化对应 Recorder 实例
    private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
    // 开始录制
    fun start(maxDuration: Int = 120) {
        this.maxDuration = maxDuration * 1000
        startRecord()
    }
    // 停止录制
    fun stop(cancel: Boolean = false) {
        stopRecord(cancel)
    }
    // 释放资源
    fun release() {
        recorder.release()
    }
    // 是否正在录制
    fun isRecording() = recorder.isRecording()
}


其中的startRecord()stopRecord()包含了AudioManager层控制播放的逻辑:


class AudioManager(val context: Context, val type: String = AAC) :
    // 为了方便启动协程录制,直接继承 CoroutineScope,并调度协程到一个单线程线程池对应的 Dispatcher
    CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
    private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
    // 开始录制
    private fun startRecord() {
        // 请求音频焦点
        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 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)
        }
    }
}


因为AudioManager是和业务层打交道的类,所以这一层就多了些零碎的控制逻辑,包括音频焦点的获取、存储和录音权限的判断、创建时声音文件、录音状态的回调。


其中录音状态的回调被定义成了若干个 lambda:


class AudioManager(val context: Context, val type: String = AAC) :
    CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
    // 状态常量
    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
    // 主线程 Handler,用于将状态回调在主线程
    private val callbackHandler = Handler(Looper.getMainLooper())
    // 将录音状态回调给业务层的 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
    // 状态变更
    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)
            }
        }
    }
}


将状态分发回调的细节分装在setState()方法中,以降低录音流程控制代码的复杂度。

完整的AudioManager代码如下:


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() {
        recorder.release()
    }
    fun isRecording() = recorder.isRecording()
    private fun startRecord() {
        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(): 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() = 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(): Long {
        if (Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()) {
            return 0L
        }
        return try {
            val stat = StatFs(context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath)
            stat.run { blockSizeLong * availableBlocksLong }
        } catch (e: Exception) {
            0L
        }
    }
    private fun getAudioFile(): File? {
        val audioFilePath = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath
        if (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(): Boolean
        /**
         * the length of audio
         */
        fun getDuration(): Long
        /**
         * start audio recording, it is time-consuming
         */
        fun start(outputFile: File, maxDuration: Int)
        /**
         * stop audio recording
         */
        fun stop()
        /**
         * release the resource of audio recording
         */
        fun release()
    }
    /**
     * 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(): Boolean = isRecording.get()
        override fun getDuration(): 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() {
            recorder.stop()
            isRecording.set(false)
            duration = SystemClock.elapsedRealtime() - starTime.get()
        }
        override fun release() {
            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(): Boolean = isRecording.get()
        override fun getDuration(): 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()
            return isRecording.get() && audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING && duration < maxDuration
        }
        override fun stop() {
            isRecording.set(false)
        }
        override fun release() {
            audioRecord.release()
        }
    }
}


下一篇会接着这个主题,继续分析如何利用 MediaCodec 将 PCM 文件进行硬编码转换成 AAC 文件。


talk is cheap, show me the code


完整代码在这个repo中的AudioManager类中

目录
相关文章
|
5月前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
|
5月前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
2月前
|
XML IDE 开发工具
🔧Android Studio高级技巧大公开!效率翻倍,编码不再枯燥无味!🛠️
【9月更文挑战第11天】在软件开发领域,Android Studio凭借其强大的功能成为Android开发者的首选IDE。本文将揭示一些提升开发效率的高级技巧,包括自定义代码模板、重构工具、高级调试技巧及多模块架构。通过对比传统方法,这些技巧不仅能简化编码流程,还能显著提高生产力。例如,自定义模板可一键插入常用代码块;重构工具能智能分析并安全执行代码更改;高级调试技巧如条件断点有助于快速定位问题;多模块架构则提升了大型项目的可维护性和团队协作效率。掌握这些技巧,将使你的开发之旅更加高效与愉悦。
66 5
|
3月前
|
Java Android开发 芯片
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
本文介绍了如何将基于全志H713芯片的AOSP Android源码导入Android Studio以解决编译和编码问题,通过操作步骤的详细说明,展示了在Android Studio中利用代码提示和补全功能快速定位并修复编译错误的方法。
139 0
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
|
4月前
|
XML IDE 开发工具
🔧Android Studio高级技巧大公开!效率翻倍,编码不再枯燥无味!🛠️
【7月更文挑战第28天】在软件开发领域, Android Studio作为首选IDE, 其高级技巧能显著提升开发效率与乐趣。掌握这些技巧, 让开发旅程更高效有趣!
46 2
|
5月前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。
|
5月前
|
存储 Android开发 Kotlin
开发安卓app OKhttp下载后使用MediaPlayer播放
在Android Jetpack Compose应用程序中,要使用OkHttp下载远程音频文件并在本地播放,你需要完成以下几个步骤: 1. **添加依赖**:确保`build.gradle`文件包含OkHttp和Jetpack Compose的相关依赖。 2. **下载逻辑**:创建一个`suspend`函数,使用OkHttp发起网络请求下载音频文件到本地。 3. **播放逻辑**:利用`MediaPlayer`管理音频播放状态。 4. **Compose UI**:构建用户界面,包含下载和播放音频的按钮。
|
5月前
|
存储 Android开发
安卓app,MediaPlayer播放本地音频 | 按钮控制播放和停止
在Jetpack Compose中,不直接操作原生Android组件如`Button`和`MediaPlayer`,而是使用Compose UI构建器定义界面并结合ViewModel管理音频播放逻辑。以下示例展示如何播放本地音频并用按钮控制播放/停止:创建一个`AudioPlayerViewModel`管理`MediaPlayer`实例和播放状态,然后在Compose UI中使用`Button`根据`isPlaying`状态控制播放。记得在`MainActivity`设置Compose UI,并处理相关依赖和权限。
|
5月前
|
Android开发 Kotlin
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【本地】音频,播放完随机播放下一首,遇到播放错误,也自动播放下一首
使用Kotlin和Jetpack Compose开发的安卓应用中,实现了两个EvoPlayer同时播放res/raw目录下的音频。一个音轨播放人声(顺序播放),另一个播放背景音乐(随机播放)。每个音轨都有独立的播放和停止控制,且在播放结束或遇到错误时会自动切换到下一首。MediaPlayer置于ViewModel中,UI界面包含播放和停止按钮,控制两个音轨。每次切换音频前,还会随机调整播放速度在0.9到1.2之间。代码示例展示了如何创建ViewModel和UI以实现这一功能。
|
5天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。