Windows基礎-實時錄音程序(WaveXXX)


寫在前面

一開始是打算用這個老接口做訊飛語音識別的程序,在轉移到UWP時發現,這玩意在Windows Runtime中屏蔽(棄用)了,將來會更新使用WASAPI的程序

  WaveRecorder類代碼下載地址

錄音無非兩種需求: 
       1. 非實時獲得音頻,也就是停止錄音了你才需要處理它; 
       2. 實時獲取音頻,比如QQ電話這種這邊講話那邊馬上就聽到的。 
       后者實現起來比較啰嗦,但也很定式。既然啰嗦那就乖乖地寫成類吧,管別的大仙怎么說你low呢 ( ͡° ͜ʖ ͡°) 。 
              這里寫圖片描述

  首先你要知道…

Windows API 可用的實現方式 用處
Windows Multimedia API(WaveXxxAPI) 定式且啰嗦地實現音頻流的實時獲取。
Media Control Interface (MCI) 讓你簡單地用字符串命令實現錄音和放音

——MMAPI可以讓你訪問音頻設備的緩沖區,發揮比較自由,接近系統底層。 
——MCI是發送字符串指令給這個界面,內部的無法干涉,記得《少年電腦世界》之類的雜志教你打開關閉光驅什么的就是用寫一行 
          mciSendString("set cdaudio door open",NULL,0,NULL);//關閉把open改為close。 
          注意:MCI簡稱媒體控制接口,較為高層,為的是讓你不用關心設備的具體操作,快速上手,簡單地操作多媒體設備。WaveXxxAPI是接近底層的應用程序接口,為的是靈活地控制設備,但設備的操作還是比較定式的,靈活在於設備狀態配置和數據處理的時機,所以M$給你了兩個控制方式。

     下面我們來看看如何用這兩種方式實現錄音:


使用MMAPI(WaveXxxAPI)

  怎么和音頻流打交道

       用MMAPI的估計都是想實時獲得音頻數據的,MMAPI可以把音頻流緩沖起來並一塊一塊地發送給你,在這里我們暫把這種固定大小的音頻裸數據簡稱為AudioFrame(數據塊,代碼中別名叫ChunkData)。這一塊數據需要你一次性處理完(你甚至需要轉移這個數據到另一個線程以保證緩沖區讀寫的穩定性),數據有多少字節可以根據實際情況來設定。在性能和延遲之間均衡考慮一下,200ms的數據可以應付大多數情況。 
       PS:用過Kinect V2麥克風的同學對此應該比有印象,AudioFrame顧名思義,音頻幀。

  操作流程

       MMAPI的操作十分定式: 
       開始錄音的流程為:以一種格式打開波形輸入設備,發送WIM_OPEN消息給回調函數,准備緩沖區,添加緩沖區到設備,告訴設備錄音開始; 
       錄音期間循環發送WIM_DATA給回調函數; 
       結束錄音的流程為:告訴設備錄音結束並發送WIM_DATA給回調函數讓它處理最后的數據,重置錄音設備,釋放緩沖區,至此可以重新設置緩沖區到設備並開始新的錄音,關閉設備並發送WIM_CLOSE給回調函數。 
  
       關於MMAPI的回調函數: 
       這個回調函數是來處理消息的,一開始收到WIM_OPEN,最后收到WIM_CLOSE唯一頻繁收到的消息是WIM_DATA,得到這個消息時我們需要轉移緩沖區里的數據並把緩沖區壓入到設備緩沖隊列中,你可以理解為自動pop手動push。 
       我寫的類里面的回調函數是屬於這個回調函數的,阻塞的話還是會直接影響MMAPI的回調函數 
  
       這里用到雙緩沖乃至多緩沖技術: 
       假設一個實時接水的任務,聽起來奇怪但與MMAPI的處理流程相似,這里需要你用杯子連續接水,杯子相當於你開辟的緩沖區: 
       根據上面的​流程,你的身份是MMAPI的回調函數,在飲水機面前要拿着杯子執行這個任務:任務的基本指標是滴水不漏地連續用杯子把水接到一個存儲區域里,你要接指定量的水,還要負責轉移走這杯水。為了能騰出時間把水倒在存儲區域里,你肯定需要用不止一個杯子輪流接水。飲水機有一個功能:每當杯子灌滿后,飲水機會通知你,並自動去接下一個杯子,如果后面沒有杯子則終止任務。(不會講故事的我啊TT,這個奇葩例子能看懂就行)可以見得:1.你會被及時地通知去轉移數據2.緩沖區用完了要及時放回緩沖區隊列后端以保證任務能夠繼續3.如果轉移並處理數據的時間不是很穩定,你可能需要准備多個緩沖區而不是單純增加緩沖區容量,為的是確保任務中能夠預留足夠多的容忍時間供你使用。 
       這里的多緩沖技術淺顯地解釋就是多個緩沖區排成一個隊列(或者理解為放成一摞)來抵消這個任務中那些耗時不穩定的處理過程對整個實時處理任務的連續性帶來的負面影響。其實生活中有很多事情也是用到多緩沖這個概念。

  代碼

    這一部分我把自己寫的類里面的函數拿了出來,完整代碼請見鏈接,免下載積分。

    需要添加:

#include "mmsystem.h" // using namespace std; #pragma comment(lib, "winmm.lib")

    檢查一下是否有音頻輸入設備:

    if (!waveInGetNumDevs()) cout << "沒有找到音頻輸入設備" << endl;

     寫一個WaveXxxAPI的回調函數:

DWORD WaveRecorder::WaveXAPI_Callback(HWAVEIN hwavein, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
    // 消息switch switch (uMsg) { case WIM_OPEN: // 設備成功已打開 // 在這里添加打開時要做的 break; case WIM_DATA: // 緩沖區數據填充完畢 // 把緩沖區數據拷貝出來 memcpy(ChunkData.data(), ((LPWAVEHDR)dwParam1)->lpData, CHUNCK_SIZE); waveInAddBuffer(hwavein, (LPWAVEHDR)dwParam1, sizeof(WAVEHDR));//用完了添加到緩沖區 // 沒有錄進去的被填充為0xcd,改成0來避免末尾出現爆音【只在結束錄音時進行,不影響添加緩存效率】 // 你需要在這里檢查dwBytesRecorded的數值,小於CHUNCK_SIZE則需要你把后面的0xcd改為0或者在寫入文件的時候忽略后面【只在剛執行stop的時候出現,不必擔心占用時間】 // 拷貝出來供外部使用 RawData.push_back(ChunkData);; // 如果需要停止錄音則不繼續添加緩存 if (!stop) { waveInAddBuffer(hwavein, (LPWAVEHDR)dwParam1, sizeof(WAVEHDR));// 壓入緩沖區 } else { // 防止結束時重復記錄數據 dat_ignore = true; } break; case WIM_CLOSE: // 操作成功完成 // 在這里添加關閉設備時要做的 break; default: break; } return 0; } 

 

     配置Wave波形格式:

WAVEFORMATEX pwfx;
void WaveRecorder::WaveInitFormat(LPWAVEFORMATEX WaveFormat, WORD Ch, DWORD SampleRate, WORD BitsPerSample) { // 自動配置參數 WaveFormat->wFormatTag = WAVE_FORMAT_PCM; // 簡直是廢話,就一種類型 WaveFormat->nChannels = Ch; // 聲道數 WaveFormat->nSamplesPerSec = SampleRate; // 采樣率 WaveFormat->nAvgBytesPerSec = SampleRate * Ch * BitsPerSample / 8; // 每秒平均數據量 WaveFormat->nBlockAlign = Ch * BitsPerSample / 8; // 單幀數據量 WaveFormat->wBitsPerSample = BitsPerSample; // 采樣位深,也就是采樣精度 WaveFormat->cbSize = 0; // 保留 }

     嘗試打開輸入設備並准備緩沖區:

    HWAVEIN hwi;                        // 音頻輸入設備 static WAVEHDR pwh[BUFFER_LAYER]; // 硬件緩沖區 waveInOpen(&hwi, WAVE_MAPPER, &pwfx,(DWORD)WaveXAPI_Callback,NULL,CALLBACK_FUNCTION); for (int layer = 0; layer < BUFFER_LAYER; layer++) { // 配置緩沖區 pwh[layer].lpData = new char[CHUNCK_SIZE]; // 8位緩沖區 pwh[layer].dwBufferLength = CHUNCK_SIZE; // 緩沖區大小 pwh[layer].dwBytesRecorded = 0; // 這里是已填充字節的計數,對於結束時未填充的可以自行處理 pwh[layer].dwUser = layer; // 這是用戶自定義數據,這里我將其設為緩沖層數的編號 pwh[layer].dwFlags = 0; // 用不着,保留 pwh[layer].dwLoops = 0; // 用不着,保留 pwh[layer].lpNext = NULL; // 用不着,保留 pwh[layer].reserved = 0; // 保留的保留(笑) // 部署緩存 waveInPrepareHeader(hwi, &pwh[layer], sizeof(WAVEHDR)); // 配置頭數據 waveInAddBuffer(hwi, &pwh[layer], sizeof(WAVEHDR)); // 壓入緩沖區 } // 初始化裸數據緩存 RawData.clear(); RawData.reserve(10); // 發送錄音開始消息 waveInStart(hwi);

     這時我們可以干其他的事情,因為錄音階段所有的數據都由剛設置的回調函數處理:

     停止錄制:

    // 停止標記 stop = true; // 設備停止 waveInStop(hwi); waveInReset(hwi); // 釋放緩沖區 for (int layer = 0; layer<BUFFER_LAYER; layer++) { waveInUnprepareHeader(hwi, &pwh[layer], sizeof(WAVEHDR)); delete pwh[layer].lpData; } // 關閉設備並發出WIM_CLOSE waveInClose(hwi);
  •      我們還可以寫入wav文件保存起來

    /* wav音頻頭部格式 */ typedef struct WAVEPCMHDR { char riff[4]; // = "RIFF" UINT32 size_8; // = FileSize - 8 char wave[4]; // = "WAVE" char fmt[4]; // = "fmt " UINT32 fmt_size; // = PCMWAVEFORMAT的大小 : //PCMWAVEFORMAT UINT16 format_tag; // = PCM : 1 UINT16 channels; // = 通道數 : 1 UINT32 samples_per_sec; // = 采樣率 : 8000 | 6000 | 11025 | 16000 UINT32 avg_bytes_per_sec; // = 每秒平均字節數 : samples_per_sec * bits_per_sample / 8 UINT16 block_align; // = 每采樣點字節數 : wBitsPerSample / 8 UINT16 bits_per_sample; // = 量化精度: 8 | 16 char data[4]; // = "data"; //DATA UINT32 data_size; // = 裸數據長度 } WAVEPCMHDR; /* 默認wav音頻頭部數據 */ WAVEPCMHDR WavHeader = { { 'R', 'I', 'F', 'F' }, 0, { 'W', 'A', 'V', 'E' }, { 'f', 'm', 't', ' ' }, sizeof(PCMWAVEFORMAT) , WAVE_FORMAT_PCM, 1, SAMPLE_RATE, SAMPLE_RATE*(SAMPLE_BITS / 8), SAMPLE_BITS / 8, SAMPLE_BITS, { 'd', 'a', 't', 'a' }, 0 }; // 編輯並寫入Wave頭信息 WavHeader.data_size = CHUNCK_SIZE*RawData.size(); WavHeader.size_8 = WavHeader.data_size + 32; fwrite(&WavHeader, sizeof(WavHeader), 1, fp); // 追加RawData fwrite(RawData.data(), CHUNCK_SIZE*RawData.size(), 1, fp); // 寫入結束 fclose(fp);

    詳細的函數說明及理解可以參考另一人個的博客 
    自己很倉促地寫了一個WaveRecorder類,想直接用的可以回到頂部下載,可以滿足大多數人的需求,但里面用到了STL。 
    MMAPI的實時獲取音頻數據的實現就寫到這


使用MCI(字符串命令)

MCI看起來比較文藝(笑),用起來也是簡單到幾句話而已,雖然是非實時的,但是行數跟MMAPI差距也太大了吧!

//  設置位深: mciSendString ("set wave bitpersample 8", "", 0, 0); //  設置采樣率: mciSendString ("set wave samplespersec 16000", "", 0, 0); //  設置聲道數: mciSendString ("set wave channels 1", "", 0, 0); //  設置WAVEPCM: mciSendString ("set wave format tag pcm","", 0, 0); //  打開設備: mciSendString ("open new type WAVEAudio alias WREC",0&,0,0); // 我見很多人寫alias movie,這里就是alias個代號 //  開始錄音: mciSendString ("record WREC",0&,0,0); //  停止錄音: mciSendString ("stop WREC",0&,0,0); //  保存錄音 mciSendString ("save WREC C:\\test.wav",0&,0,0); // 絕對路徑 //  關閉設備: mciSendString ("close WREC",0&,0,0);

 

        這里寫圖片描述

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM