音頻采集

1 音頻采集流程

聲音是由物體振動(dòng)產(chǎn)生的聲波,是通過(guò)介質(zhì)(空氣或固體、液體)傳播并能被人或動(dòng)物聽覺器官所感知的波動(dòng)現(xiàn)象。聲波是一種在時(shí)間和振幅上連續(xù)的模擬量,麥克風(fēng)就是一種采集聲波并將其轉(zhuǎn)換成模擬電壓信號(hào)輸出的裝置,有了聲波的模擬電壓信號(hào),下一步需要將模擬信號(hào)數(shù)字化,即將模擬信號(hào)通過(guò)模數(shù)轉(zhuǎn)換器(A/D)后轉(zhuǎn)換成數(shù)字信號(hào),最常見的模數(shù)轉(zhuǎn)換方式就是脈沖編碼調(diào)制PCM(Pulse Code Modulation),PCM編碼過(guò)程如下圖所示:


PCM編碼過(guò)程

從上圖中可以看到PCM編碼主要有三個(gè)過(guò)程:采樣、量化、編碼。

1> 采樣
將時(shí)間連續(xù)的模擬信號(hào)按照采樣率提取樣值,變?yōu)闀r(shí)間軸上離散的抽樣信號(hào)的過(guò)程。采樣率是每秒從模擬信號(hào)中提取樣值的次數(shù)。Nyquist–Shannon(奈奎斯特-香農(nóng))采樣定律表明如果至少以模擬信號(hào)最高頻率2倍的采樣率對(duì)模擬信號(hào)進(jìn)行均勻采樣,那么原始模擬信號(hào)才能不失真的從采樣產(chǎn)生的離散值中完全恢復(fù)。人耳可以聽到的聲波頻率范圍是 20Hz~22.05kHz,因此44.1kHz/16bit的音頻數(shù)據(jù)被認(rèn)為是無(wú)損音頻。

2> 量化
抽樣信號(hào)雖然是時(shí)間軸上離散的信號(hào),但仍然是模擬信號(hào),其樣值在一定的取值范圍內(nèi),可有無(wú)限多個(gè)值。顯然,對(duì)無(wú)限個(gè)樣值給出數(shù)字碼組來(lái)對(duì)應(yīng)是不可能的。為了實(shí)現(xiàn)以數(shù)字碼表示樣值,必須采用“四舍五入”的方法把樣值分級(jí)“取整”,使一定取值范圍內(nèi)的樣值由無(wú)限多個(gè)值變?yōu)橛邢迋€(gè)值。這一過(guò)程稱為量化。

量化后的抽樣信號(hào)與量化前的抽樣信號(hào)相比較,當(dāng)然有所失真,且不再是模擬信號(hào)。這種量化失真在接收端還原模擬信號(hào)時(shí)表現(xiàn)為噪聲,并稱為量化噪聲。量化噪聲的大小取決于把樣值分級(jí)“取整”的方式,分的級(jí)數(shù)越多,即量化級(jí)差或間隔越小,量化噪聲也越小。

3> 編碼
量化后的抽樣信號(hào)就轉(zhuǎn)化為按抽樣時(shí)序排列的一串十進(jìn)制數(shù)字碼流,即十進(jìn)制數(shù)字信號(hào)。簡(jiǎn)單高效的數(shù)據(jù)系統(tǒng)是二進(jìn)制碼系統(tǒng),因此應(yīng)將十進(jìn)制數(shù)字代碼變換成二進(jìn)制編碼。這種把量化的抽樣信號(hào)變換成給定字長(zhǎng)(采樣位數(shù))的二進(jìn)制碼流的過(guò)程稱為編碼

經(jīng)過(guò)上面的PCM編碼過(guò)程得到的數(shù)字信號(hào)就是 PCM音頻數(shù)據(jù)。

在PCM編碼過(guò)程中主要用3個(gè)參數(shù)表現(xiàn)PCM音頻數(shù)據(jù):采樣率、采樣位數(shù)以及聲道數(shù),
其中采樣率、采樣位數(shù)上面已經(jīng)講解過(guò),通道數(shù)即采集聲音的通道數(shù),有單聲道(mono)和立體聲(雙聲道stereo)等,聲道數(shù)越多越能體現(xiàn)聲音的空間立體效果。

2 PCM音頻數(shù)據(jù)的存儲(chǔ)方式

采集的PCM音頻數(shù)據(jù)是需要保存到本地文件中,如果用單聲道采集的,則按時(shí)間的先后順序依次存入,如果是雙聲道的話則按時(shí)間先后順序交叉地存入,如下圖所示:


PCM音頻數(shù)據(jù)存儲(chǔ)格式

PCM音頻數(shù)據(jù)一般無(wú)法通過(guò)播放器直接播放。可以使用ffplay工具進(jìn)行播放

ffplay -f s16le -ar 44100 -ac 1 -i raw.pcm
參數(shù)解釋
-f s16le: 設(shè)置音頻格式為有符號(hào)16位小端格式(signed 16 bits little endian),對(duì)應(yīng)Android中的AudioFormat.ENCODING_PCM_16BIT
-ar 44100 :設(shè)置音頻采樣率(audiorate)為44100
-ac 1:設(shè)置聲道數(shù)(audiochannels)1,單聲道為1,雙聲道為2
-i raw.pcm :設(shè)置輸入的pcm音頻文件

通常將PCM音頻數(shù)據(jù)轉(zhuǎn)化為WAVE文件就可以用播放器直接解析播放,WAVE是微軟公司專門為Windows開發(fā)的一種標(biāo)準(zhǔn)數(shù)字音頻文件,該文件能記錄各種單聲道或立體聲的聲音信息,并能保證聲音不失真。它符合資源互換文件格式(RIFF)規(guī)范

RIFF文件(符合RIFF規(guī)范的文件)是windows環(huán)境下大部分多媒體文遵循的一種文件結(jié)構(gòu),RIFF文件所包含的數(shù)據(jù)類型由該文件的擴(kuò)展名來(lái)標(biāo)識(shí),能以RIFF文件存儲(chǔ)的數(shù)據(jù)包括:音頻視頻交錯(cuò)格式數(shù)據(jù)(.AVI)、 波形格式數(shù)據(jù)(.WAV) 、位圖格式數(shù)據(jù)(.RDI)、 MIDI格式數(shù)據(jù)(.RMI)、調(diào)色板格式(.PAL)、多媒體電影(.RMN)、動(dòng)畫光標(biāo)(.ANI)等,RIFF文件結(jié)構(gòu)如下圖所示:

RIFF文件結(jié)構(gòu)

如上圖所示,chunk是構(gòu)成RIFF文件的基本單元,RIFF文件是由chunk嵌套構(gòu)成,RIFF文件首先存放的必須是一個(gè)RIFF chunk,并且只能有這一個(gè)標(biāo)志為RIFF的chunk。chunk的詳細(xì)說(shuō)明如下:

ID: 塊的唯一標(biāo)識(shí),其值可為RIFF,LIST,fmt,fact,data等。
Size: 塊中Data的大小,以字節(jié)為單位。
Data: 塊中的實(shí)際數(shù)據(jù)。

只有ID為RIFF或者LIST的chunk才能包含其他的chunk,ID為RIFF的chunk中Data的起始位置的FormType用于標(biāo)識(shí)Data中的chunk的數(shù)據(jù)類型。

WAVE文件中chunk的排列方式依次是:RIFF chunk(FormType 為 WAVE),F(xiàn)ormat sub-chunk,F(xiàn)act sub-chunk(附加塊,可選,采用壓縮編碼的WAVE文件,必須要有Fact chunk,該塊中只有一個(gè)數(shù)據(jù),為每個(gè)聲道的采樣總數(shù)),Data chunk。接下來(lái)我們看看WAVE文件結(jié)構(gòu),如下圖所示:


WAV音頻文件結(jié)構(gòu)

Data sub-chunk中的Data中存放具體的音頻數(shù)據(jù),將PCM音頻數(shù)據(jù)轉(zhuǎn)換成WAV音頻文件實(shí)際上就是把PCM音頻數(shù)據(jù)放到該位置。

3 Android上采集和播放PCM音頻數(shù)據(jù)

有了上面的理論基礎(chǔ),接下來(lái)就在Android手機(jī)上實(shí)現(xiàn)一下,使用AudioRecord采集PCM音頻數(shù)據(jù)的代碼實(shí)現(xiàn):

private var audioRecord: AudioRecord? = null
private const val sampleRateInHz: Int = 44100
private const val bitsPerSample: Int = 16
private const val channelConfig = AudioFormat.CHANNEL_IN_MONO
private const val audioFormat = AudioFormat.ENCODING_PCM_16BIT
private val bufferSize =
    AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)

private var pcmFile: File? = null
private var mScope: CoroutineScope? = null

/**
 * 創(chuàng)建音頻錄制器
 *
 * @author cytmxk
 * @since 2021/10/12
 */
private fun createAudioRecord(): AudioRecord {
    Log.d(TAG, "createAudioRecord: bufferSize = $bufferSize");

    // audioSource: 音頻來(lái)源,MediaRecorder.AudioSource.MIC 代表來(lái)源于麥克風(fēng)
    // sampleRateInHz: 采樣率,每秒取得聲音樣本的次數(shù),采樣頻率越高,聲音的質(zhì)量也就越好,還原的聲音就越真實(shí),但同時(shí)它占用的資源越多。常見的采樣率為44100 即44.1KHZ
    // channelConfig: 聲道配置,分為單聲道和立體聲道,CHANNEL_IN_MONO代表單聲道,CHANNEL_IN_STEREO代表立體聲道
    // audioFormat: 音頻格式,ENCODING_PCM_16BIT代表通過(guò)PCM進(jìn)行采樣編碼,采樣的大小為16位
    // bufferSizeInBytes: 音頻采集緩沖區(qū)大小,計(jì)算公式為 采樣率 x 位寬 x 采樣時(shí)間 x 通道數(shù),采樣時(shí)間一般取 2.5ms~120ms 之間,
    // 由廠商或者具體的應(yīng)用決定,采樣時(shí)間取得越短碎片化的數(shù)據(jù)也就會(huì)越多,開發(fā)中使用getMinBufferSize()方法的返回值,
    // 使用比getMinBufferSize()小的值則會(huì)導(dǎo)致初始化失敗。
    return AudioRecord(
        MediaRecorder.AudioSource.MIC, sampleRate,
        channelConfig, audioFormat,
        bufferSize
    )
}

/**
 * 開始音頻錄制
 *
 * @author cytmxk
 * @since 2021/10/12
 */
public fun startRecord() {
    stopRecord()

    audioRecord = createAudioRecord()
    Log.d(TAG, "captureByAudioRecord: state = ${audioRecord!!.state}")
    // 判斷視頻錄制器是否初始化成功
    if (AudioRecord.STATE_INITIALIZED != audioRecord!!.state) {
        Log.d(TAG, "AudioRecord無(wú)法初始化,請(qǐng)檢查錄制權(quán)限或者是否其他app沒有釋放錄音器")
    }

    // 創(chuàng)建用于保存采集的pcm音頻數(shù)據(jù)的文件
    pcmFile = MediaFileUtils.getAudioFile("test.pcm")
    Log.d(TAG, "initPCMFile: pcmFile=$pcmFile")
    pcmFile ?: return

    if (pcmFile!!.exists()) {
        pcmFile!!.delete()
    }

    // 開始采集pcm音頻數(shù)據(jù)
    val buffer = ByteArray(bufferSize)
    audioRecord!!.startRecording()

    // 在IO線程中采集pcm音頻數(shù)據(jù)
    GlobalScope.launch(Dispatchers.IO) {
        mScope = this
        var fileOutputStream: FileOutputStream? = null
        try {
            fileOutputStream = FileOutputStream(pcmFile)
            while (isActive) {
                val readStatus = audioRecord!!.read(buffer, 0, bufferSize)
                Log.d(TAG, "scope: readStatus = $readStatus")
                fileOutputStream.write(buffer)
            }
        } catch (exception: IOException) {
            Log.d(TAG, "scope: exception = $exception")
        } finally {
            fileOutputStream?.also {
                try {
                    it.close()
                } catch (exception: IOException) {
                }
            }
        }
    }
}

/**
 * 暫停音頻錄制
 *
 * @author cytmxk
 * @since 2021/10/12
 */
public fun stopRecord() {
    mScope ?: return

    mScope!!.cancel()
    mScope = null

    if (AudioRecord.STATE_UNINITIALIZED != audioRecord!!.state) {
        audioRecord!!.stop()
        // 調(diào)用release方法之后該對(duì)象不可以再次被使用,因此必須將該對(duì)象置null
        audioRecord!!.release()
        audioRecord = null
    }
}

上面的代碼都有注釋,就不在這里詳細(xì)講解了,執(zhí)行完成之后會(huì)生成一個(gè)用于保存PCM音頻數(shù)據(jù)的test.pcm文件。

為了讓手機(jī)上的播放器可以播放采集的PCM音頻數(shù)據(jù),那么接下來(lái)通過(guò)下面的代碼就可將test.pcm文件中保存的PCM音頻數(shù)據(jù)轉(zhuǎn)換成WAVE文件格式并且保存到convert.wav文件中:

private fun convertPcmToWav() {
    val wavFile = MediaFileUtils.getAudioFile("convert.wav")
    wavFile ?: return

    if (wavFile.exists()) {
        wavFile.delete()
    }

    var fileInputStream: FileInputStream? = null
    var fileOutputStream: FileOutputStream? = null
    try {
        fileInputStream = FileInputStream(pcmFile)
        fileOutputStream = FileOutputStream(wavFile)
        val audioByteLen = fileInputStream.channel.size()
        val wavByteLen = audioByteLen + 36
        addWavHeader(fileOutputStream, audioByteLen, wavByteLen)
        val buffer = ByteArray(bufferSize)
        while (fileInputStream.read(buffer) != -1) {
            fileOutputStream.write(buffer)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    } finally {
        try {
            fileInputStream?.close()
            fileOutputStream?.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
}

private fun addWavHeader(
    fileOutputStream: FileOutputStream, audioByteLen: Long, wavByteLen: Long
) {
    val header = ByteArray(44)
    // WAVE chunk
    // header[0] ~ header[3] 內(nèi)容為"RIFF"
    header[0] = 'R'.code.toByte()
    header[1] = 'I'.code.toByte()
    header[2] = 'F'.code.toByte()
    header[3] = 'F'.code.toByte()
    // header[4] ~ header[7] 存儲(chǔ)文件的字節(jié)數(shù)(不包含ChunkID和ChunkSize這8個(gè)字節(jié))
    header[4] = (wavByteLen and 0xff).toByte()
    header[5] = (wavByteLen shr 8 and 0xff).toByte()
    header[6] = (wavByteLen shr 16 and 0xff).toByte()
    header[7] = (wavByteLen shr 24 and 0xff).toByte()
    // header[8] ~ header[11] 內(nèi)容為"WAVE"
    header[8] = 'W'.code.toByte()
    header[9] = 'A'.code.toByte()
    header[10] = 'V'.code.toByte()
    header[11] = 'E'.code.toByte()

    // "fmt " 子chunk 4個(gè)字節(jié)
    // header[12] ~ header[15] 內(nèi)容為 "fmt "
    header[12] = 'f'.code.toByte()
    header[13] = 'm'.code.toByte()
    header[14] = 't'.code.toByte()
    header[15] = ' '.code.toByte()
    // header[16] ~ header[19] 存儲(chǔ)該子塊的字節(jié)數(shù)(不包含Subchunk1ID和Subchunk1Size這8個(gè)字節(jié))
    header[16] = 16
    header[17] = 0
    header[18] = 0
    header[19] = 0
    // header[20] ~ header[21] 存儲(chǔ)音頻文件的編碼格式,例如若為PCM則其存儲(chǔ)值為1,若為其他非PCM格式的則有一定的壓縮。
    header[20] = 1
    header[21] = 0
    // header[22] ~ header[23] 通道數(shù),單通道(CHANNEL_IN_MONO)值為1,雙通道(CHANNEL_IN_STEREO)值為2
    val channelSize = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2
    header[22] = channelSize.toByte()
    header[23] = 0
    // header[24] ~ header[27] 采樣頻率
    header[24] = (sampleRateInHz and 0xff).toByte()
    header[25] = (sampleRateInHz shr 8 and 0xff).toByte()
    header[26] = (sampleRateInHz shr 16 and 0xff).toByte()
    header[27] = (sampleRateInHz shr 24 and 0xff).toByte()
    // header[28] ~ header[31] 每秒采集的音頻字節(jié)數(shù)
    val byteRate = (audioFormat * sampleRateInHz * channelSize).toLong()
    header[28] = (byteRate and 0xff).toByte()
    header[29] = (byteRate shr 8 and 0xff).toByte()
    header[30] = (byteRate shr 16 and 0xff).toByte()
    header[31] = (byteRate shr 24 and 0xff).toByte()
    // header[32] ~ header[33] 塊對(duì)齊大小,每個(gè)采樣(包含所有聲道)需要的字節(jié)數(shù)
    header[32] = (channelSize * bitsPerSample / 8).toByte()
    header[33] = 0
    // header[34] ~ header[35] 每個(gè)采樣需要的 bit 數(shù)
    header[34] = bitsPerSample.toByte()
    header[35] = 0

    //data 子chunk
    // header[36] ~ header[39] 內(nèi)容為“data”
    header[36] = 'd'.code.toByte()
    header[37] = 'a'.code.toByte()
    header[38] = 't'.code.toByte()
    header[39] = 'a'.code.toByte()
    // header[40] ~ header[43] pcm字節(jié)數(shù)
    header[40] = (audioByteLen and 0xff).toByte()
    header[41] = (audioByteLen shr 8 and 0xff).toByte()
    header[42] = (audioByteLen shr 16 and 0xff).toByte()
    header[43] = (audioByteLen shr 24 and 0xff).toByte()
    try {
        fileOutputStream.write(header, 0, 44)
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

通過(guò)系統(tǒng)文件夾應(yīng)用找到convert.wav文件的位置,點(diǎn)擊就可以播放了。

其實(shí)PCM音頻數(shù)據(jù)也可以直接使用AudioTrack播放,實(shí)現(xiàn)代碼如下:

private var audioTrack: AudioTrack? = null
private const val sampleRateInHz: Int = 44100
private const val channelConfig = AudioFormat.CHANNEL_OUT_MONO // 錯(cuò)誤的寫成了CHANNEL_IN_MONO
private const val audioFormat = AudioFormat.ENCODING_PCM_16BIT
private val bufferSize =
    AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)

private var pcmFile: File? = null
private var mScope: CoroutineScope? = null


public fun playAudioByAudioTrack() {
    pcmFile = MediaFileUtils.getAudioFile("test.pcm")
    Log.d(TAG, "initPCMFile: pcmFile=${pcmFile}")
    pcmFile ?: return

    if (!pcmFile!!.exists()) {
        return
    }

    stopPlayAudio()
    initAudioTrackWithMode(AudioTrack.MODE_STREAM)
    if (audioTrack!!.state == AudioTrack.STATE_UNINITIALIZED) {
        Log.e(TAG, "state is uninit")
        return
    }

    // 在IO線程中播放采集的pcm音頻數(shù)據(jù)
    GlobalScope.launch(Dispatchers.IO) {
        mScope = this
        var fileInputStream: FileInputStream? = null
        try {
            fileInputStream = FileInputStream(pcmFile)
            val buffer = ByteArray(bufferSize / 2)
            //stream模式,可以先調(diào)用play
            audioTrack!!.play()
            while (isActive && fileInputStream.available() > 0) {
                val readCount = fileInputStream.read(buffer)
                if (readCount == AudioTrack.ERROR_BAD_VALUE || readCount == AudioTrack.ERROR_INVALID_OPERATION) {
                    continue
                }

                if (readCount > 0 && audioTrack!!.playState == AudioTrack.PLAYSTATE_PLAYING && audioTrack!!.state == AudioTrack.STATE_INITIALIZED) {
                    audioTrack!!.write(buffer, 0, readCount)
                }
            }
        } catch (exception: IOException) {
            Log.d(TAG, "scope: exception = $exception")
        } finally {
            fileInputStream?.also {
                try {
                    it.close()
                } catch (exception: IOException) {
                }
            }
        }
    }
}

private fun initAudioTrackWithMode(mode: Int) {
    audioTrack = AudioTrack(
        AudioAttributes.Builder()
            .setLegacyStreamType(AudioManager.STREAM_MUSIC)
            .build(),
        AudioFormat.Builder()
            .setChannelMask(channelConfig)
            .setEncoding(audioFormat)
            .setSampleRate(sampleRateInHz)
            .build(),
        bufferSize,
        mode, AudioManager.AUDIO_SESSION_ID_GENERATE
    )
}

public fun stopPlayAudio() {
    mScope ?: return
    mScope!!.cancel()

    if (AudioTrack.STATE_UNINITIALIZED != audioTrack!!.state) {
        audioTrack!!.stop()
        audioTrack!!.release()
    }
}

通過(guò)執(zhí)行上面playAudioByAudioTrack方法就可以播放上面生成的test.pcm音頻文件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容