Laravel 專案中 PCM 音波檔案轉 WAV 音訊檔案案例【經驗分享】

Jade發表於2019-03-05

需求

前段時間,產品經理告訴一個需求,說是客戶端錄音之後實時轉換成文字,後臺上傳一個錄音檔案後轉換成文字,兩個功能點屬於個一個功能模組(其實就是後臺和客戶端產生的資料互通),我想了下說可以做,對接阿里雲服務,客戶端使用實時語音識別服務,並保留錄音檔案;後天使用錄音檔案識別服務,並保留音訊檔案且轉碼成MP3市客戶端能播放該資料。

問題

功能上第一版的時候實現,但是由於客戶端在錄音時需要呼叫阿里雲的 SDK(別人的 SDK 一點不香),產生的錄音檔案就是音波,客戶端需要轉換成WAV或者其他格式(後臺會統一走佇列轉碼成 MP3 格式)。客戶端請求阿里雲得到文字資料後,然後提交時資料時需要將音波檔案轉換成 WAV 並上傳給後端,這些流程走下來之後整個體驗的流程就有點忙了,畢竟檔案越大,上傳介面就慢(頻寬是最大的影響)。

優化背景

知道問題後那就說下優化方案,客戶端說錄音的音波檔案我們不轉,讓後段轉,客戶端一段一段上傳音波檔案給後臺,後臺合併後轉換成 WAV 格式,成了 WAV 格式後客戶端和後臺所使用的播放器在佇列沒有完成轉碼任務時就已經可以播放該音訊檔案並可看文字資訊

開幹

三前端開發圍著我讓實現,我只能擼了,最終擼了出來,然後產品讓我整理下文件(程式設計師最討厭給自己的程式碼寫文件,也討厭被人寫的程式碼沒文件)

結果

具體使用方法這裡就不做介紹,本文只是總結,具體資訊可跳至倉庫查閱

GitHub :PcmToWav

原理

什麼是 PCMWAV

PCM :PCM(Pulse Code Modulation----脈碼調製錄音)。所謂 PCM 錄音就是將聲音等模擬訊號變成符號化的脈衝列,再予以記錄。 PCM 訊號是由 10 等符號構成的數字訊號,而未經過任何編碼和壓縮處理。與模擬訊號比,它不易受傳送系統的雜波及失真的影響。動態範圍寬,可得到音質相當好的影響效果。

WAVWAV 是一種無損的音訊檔案格式, WAV 符合 PIFF(Resource Interchange File Format) 規範。所有的 WAV 都有一個檔案頭,這個檔案頭音訊流的編碼引數。WAV對音訊流的編碼沒有硬性規定,除了 PCM 之外,還有幾乎所有支援 ACM 規範的編碼都可以為WAV的音訊流進行編碼。

PCMWAV 的關係

簡單地說, PCM 是音訊的原始資料, WAV 則是一種封裝音訊資料的容器, 而且它的格式還很簡單, 只是在資料開頭新增一些和音訊資料相關的頭資訊。

規則

首先我們看一下WAV的格式規則, 下圖所示

解決

瞭解這些規則後,我們就可以擼程式碼吧

1、 ChunkID 佔4byte, 固定值"RIFF"

$ChunkID = array(0x52, 0x49, 0x46, 0x46); // RIFF 16進位制的0x52等於10進位制中的82,82對應的ASCII碼為R

2、 ChunkSize 佔4byte, 值為4 + (8 + SubChunk1Size) + (8 + SubChunk2Size), 其中如果原始資料是PCM, 簡化為36 + SubChunk2Size

$ChunkSize = array(0x0, 0x0, 0x0, 0x0);
$ChunkSize = self::getLittleEndianByteArray(36 + $dataSize);

3、 Format 佔4byte, 固定值"WAVE"

$FileFormat = array(0x57, 0x41, 0x56, 0x45); // WAVE

4、 Subchunk1ID 佔4byte, 固定值"ftm "(注意空格補全4位)

$Subchunk1ID = array(0x66, 0x6D, 0x74, 0x20); // fmt

5、 Subchunk1Size 佔4byte, 資料為PCM時, 值為16

$Subchunk1Size = array(0x10, 0x0, 0x0, 0x0); // 16 little endian

6、 AudioFormat 佔2byte, 資料為PCM時, 值為1, 其他值表示資料進行過某種壓縮

$AudioFormat = array(0x1, 0x0); // PCM = 1 little endian

7、 NumChannels 佔2byte, 對應AudioRecord中的channelConfig, 單聲道Mono = 1, 立體聲Stereo = 2

if ($numchannels == 2) {
    $fmt->NumChannels = array(0x2, 0x0); // 立體聲為2
} else {
    $fmt->NumChannels = array(0x1, 0x0); // 單聲道為1
}

8、 SampleRate 佔4byte, 對應AudioRecord中的sampleRateInHz, 即取樣頻率, 例如8000, 16000, 44100

$SampleRate = self::getLittleEndianByteArray($samplerate);

9、 ByteRate 佔4byte, 值為SampleRate * BlockAlign

self::getLittleEndianByteArray($samplerate * $numchannels * ($bitspersample / 8));

10、 BlockAlign 佔2byte, 值為NumChannels * BitsPerSample / 8

self::getLittleEndianByteArray($numchannels * ($bitspersample / 8), true);

11、 BitsPerSample 佔2byte, 對應AudioRecord中的audioFormat, 8bits = 8, 16bits = 16

self::getLittleEndianByteArray($bitspersample, true);

12、 Subchunk2ID 佔4byte, 固定值"data", 即

$Subchunk2ID = array(0x64, 0x61, 0x74, 0x61); // data

13、 Subchunk2Size 佔4byte, 描述音訊資料的長度, 就是pcm檔案大小

self::getLittleEndianByteArray(filesize($filename));

14、 Data 佔pcm檔案大小個byte, 表示原始的PCM音訊資料

getLittleEndianByteArray 方法說明

getLittleEndianByteArray主要是將傳遞過來的數進行處理已轉換成需要使用的資料,站幾位元組,就返回多少長度的陣列

private static function getLittleEndianByteArray($lValue, $short = false)
    {
        $b = array(0, 0, 0, 0);
        $running = $lValue / pow(16, 6);
        $b[3] = floor($running);
        $running -= $b[3];
        $running *= 256;
        $b[2] = floor($running);
        $running -= $b[2];
        $running *= 256;
        $b[1] = floor($running);
        $running -= $b[1];
        $running *= 256;
        $b[0] = round($running);

        if ($short) { // 為 `true` 時返回長度為2的陣列
            $tmp = array_slice($b, 0, 2);
            $b = $tmp;
        }

        return $b;
    }

整體思路

整個檔案的開頭44位元組資訊也基本說明完了,下面就說下處理類檔案的實現,這邊處理的邏輯先臨時建立一個只有44位元組的檔案,然後將 PCM 檔案的資料追加進該檔案,最終根據WAV的格式規則實際計算出真實的頭部44位元組資訊並將檔案修改指標指向檔案開頭,然後修改為新產生的資料

結語

整體上好像跟 Laravel 不搭嘎,但是公司的產品確實是基於 Laravel 開發,專案中引入了 composer 安裝。PHP 版面人少我就發這哈了 :joy: :joy:
寫的不好,大佬們輕點虐 :relaxed::relaxed:

相關文章