Windows Multimedia API (winmm.dll) 中的 waveIn*/waveOut* 系列函數,是 Windows 早期音訊輸入輸出的基石。雖然 Windows Vista 引入 Core Audio API (WASAPI) 提供更低延遲、更豐富的功能,但波形 API 因其簡單、穩定、跨版本相容 (Win95~Win11)、無需複雜 COM 介面,至今仍廣泛應用於教學、舊專案維護、嵌入式 Windows、簡易錄音工具等場景。本文系統講解 waveIn 錄音完整流程、核心結構體、三種回呼模式、緩衝區管理與無限長度錄製技巧。
WAVEFORMATEX — 音訊格式描述typedef struct {
WORD wFormatTag; // 格式標記,WAVE_FORMAT_PCM = 1
WORD nChannels; // 聲道數:1=單聲道, 2=立體聲
DWORD nSamplesPerSec; // 採樣率:8000, 11025, 22050, 44100 等
DWORD nAvgBytesPerSec; // 平均位元率 = nSamplesPerSec * nBlockAlign
WORD nBlockAlign; // 區塊對齊 = nChannels * wBitsPerSample / 8
WORD wBitsPerSample; // 位元深度:8, 16
WORD cbSize; // 擴充資料大小 (PCM 為 0)
} WAVEFORMATEX;
WAVEHDR — 緩衝區標頭typedef struct {
LPSTR lpData; // 緩衝區記憶體指標
DWORD dwBufferLength; // 緩衝區總長度 (bytes)
DWORD dwBytesRecorded; // 實際錄入/播放位元組數
DWORD dwUser; // 使用者自訂資料
DWORD dwFlags; // 旗標:WHDR_DONE, WHDR_PREPARED, WHDR_INQUEUE...
DWORD dwLoops; // 播放迴圈次數
struct wavehdr_tag* lpNext; // 驅動程式保留
DWORD reserved; // 保留
} WAVEHDR;
| 功能 | 錄音 (waveIn*) | 播放 (waveOut*) |
|---|---|---|
| 開啟裝置 | waveInOpen | waveOutOpen |
| 準備標頭 | waveInPrepareHeader | waveOutPrepareHeader |
| 加入緩衝區 | waveInAddBuffer | — |
| 寫入/播放 | — | waveOutWrite |
| 開始 | waveInStart | — (Write 即開始) |
| 停止/重設 | waveInReset | waveOutReset |
| 釋放標頭 | waveInUnprepareHeader | waveOutUnprepareHeader |
| 關閉裝置 | waveInClose | waveOutClose |
waveInOpen 的 dwCallback 參數指定處理方式,由 fdwOpen 旗標決定:
HWND (MM_WIM_*)。PostThreadMessage)。__stdcall,不可呼叫非安全 API)。MM_WIM_*)MM_WIM_OPEN:裝置開啟完成,可進行初始化。MM_WIM_DATA:緩衝區滿或錄音停止,lParam 指向 WAVEHDR*,核心處理點。MM_WIM_CLOSE:裝置關閉,釋放資源。MM_WOM_*):對應 OPEN/DONE/CLOSE。// 1. 定義格式
WAVEFORMATEX wfx = { WAVE_FORMAT_PCM, 1, 22050, 22050, 1, 8, 0 }; // 22.05kHz/8bit/單聲道
// 2. 開啟錄音裝置 (視窗回呼模式)
HWAVEIN hWaveIn;
waveInOpen(&hWaveIn, WAVE_MAPPER, &wfx, (DWORD_PTR)hWnd, 0, CALLBACK_WINDOW);
// 3. 分配兩個緩衝區 (雙緩衡輪換)
#define BUF_SIZE 8192
WAVEHDR hdr1 = {0}, hdr2 = {0};
BYTE buf1[BUF_SIZE], buf2[BUF_SIZE];
hdr1.lpData = (LPSTR)buf1; hdr1.dwBufferLength = BUF_SIZE;
hdr2.lpData = (LPSTR)buf2; hdr2.dwBufferLength = BUF_SIZE;
waveInPrepareHeader(hWaveIn, &hdr1, sizeof(WAVEHDR));
waveInPrepareHeader(hWaveIn, &hdr2, sizeof(WAVEHDR));
waveInAddBuffer(hWaveIn, &hdr1, sizeof(WAVEHDR));
waveInAddBuffer(hWaveIn, &hdr2, sizeof(WAVEHDR));
// 4. 開始錄音
waveInStart(hWaveIn);
// 5. 訊息迴圈處理 MM_WIM_DATA (見下文)
// 6. 停止與清理
waveInReset(hWaveIn);
waveInUnprepareHeader(hWaveIn, &hdr1, sizeof(WAVEHDR));
waveInUnprepareHeader(hWaveIn, &hdr2, sizeof(WAVEHDR));
waveInClose(hWaveIn);
MM_WIM_DATA 處理當緩衝區填滿,驅動程式發送 MM_WIM_DATA,lParam 指向已填滿的 WAVEHDR。此時必須:
waveInAddBuffer 同一個 WAVEHDR (資料已處理完畢),實現緩衝區循環使用。
BEGIN_MESSAGE_MAP(CRecordWnd, CWnd)
ON_MESSAGE(MM_WIM_OPEN, OnWimOpen)
ON_MESSAGE(MM_WIM_DATA, OnWimData)
ON_MESSAGE(MM_WIM_CLOSE, OnWimClose)
END_MESSAGE_MAP()
LRESULT CRecordWnd::OnWimData(WPARAM wParam, LPARAM lParam) {
WAVEHDR* pHdr = (WAVEHDR*)lParam;
if (!pHdr || !(pHdr->dwFlags & WHDR_DONE)) return 0;
// 1. 寫入檔案 (假設 m_file 已開啲)
m_file.Write(pHdr->lpData, pHdr->dwBytesRecorded);
m_dwTotalBytes += pHdr->dwBytesRecorded;
// 2. 重新加入佇列 (循環緩衝)
waveInAddBuffer(m_hWaveIn, pHdr, sizeof(WAVEHDR));
return 0;
}
CWinThread) 範例
class CRecordThread : public CWinThread {
DECLARE_DYNCREATE(CRecordThread)
HWAVEIN m_hWaveIn;
CFile m_file;
WAVEHDR m_hdr[2];
BYTE* m_buf[2];
public:
BOOL InitInstance() {
// 開啟裝置,指定執行緒 ID 回呼
waveInOpen(&m_hWaveIn, WAVE_MAPPER, &wfx, m_nThreadID, 0, CALLBACK_THREAD);
// 準備緩衝區...
return TRUE;
}
// 訊息映射
ON_THREAD_MESSAGE(MM_WIM_DATA, OnWimData)
ON_THREAD_MESSAGE(MM_WIM_CLOSE, OnWimClose)
// 處理函數同視窗模式
};
void WriteWaveHeader(CFile& file, const WAVEFORMATEX& wfx, DWORD dwDataSize) {
DWORD dwRiffSize = 36 + dwDataSize; // 4(RIFF)+4(size)+4(WAVE)+4(fmt )+24(fmt data)+4(data)+4(size)
file.Write("RIFF", 4);
file.Write(&dwRiffSize, 4);
file.Write("WAVE", 4);
file.Write("fmt ", 4);
DWORD dwFmtSize = 16; // PCM
file.Write(&dwFmtSize, 4);
file.Write(&wfx, sizeof(WAVEFORMATEX)); // 18 bytes, 含 cbSize=0
file.Write("data", 4);
file.Write(&dwDataSize, 4);
}
// 錄音結束時呼叫,寫入標頭並更新 RIFF/data 區塊大小
注意:WAVEFORMATEX 為 18 位元組 (含 cbSize),標準 PCM fmt 區塊為 16 位元組 (不含 cbSize),寫入時需手動控制寫入長度或構造 16 位元組結構。
| 問題 | 原因 | 解決 |
|---|---|---|
| 錄音雜音/斷續 | 緩衝區太小、CPU 佔用高導致緩衝區下溢 | 增大緩衝區 (如 16KB~64KB)、提高執行緒優先級、使用雙/三緩衡 |
| 無法開啟裝置 (MMSYSERR_ALLOCATED) | 裝置被其他程式佔用 | 檢查獨佔模式、關閉其他錄音軟體、嘗試 WAVE_MAPPER 自動選擇 |
| 錄音速度不正確 | 格式結構體填寫錯誤 (nAvgBytesPerSec、nBlockAlign) | 嚴格按公式計算:nBlockAlign = nChannels * wBitsPerSample / 8; nAvgBytesPerSec = nSamplesPerSec * nBlockAlign |
| 回呼函數崩潰 | CALLBACK_FUNCTION 中呼叫非安全 API (如 new、MessageBox、檔案 I/O) | 改用 CALLBACK_WINDOW/THREAD,或回呼中僅設旗標/PostMessage 通知主執行緒處理 |
| WAV 檔案播放長度錯誤 | RIFF/data 區塊大小未正確寫入 | 錄音結束前計算總位元組數,SeekToBegin 回寫標頭大小欄位 |
waveInGetNumDevs() 取得裝置數。waveInGetDevCaps(uDeviceID, &caps, sizeof(caps)) 取得裝置名稱、支援格式 (caps.dwFormats 位元旗標)。waveInMessage(hWaveIn, DRV_QUERYFUNCTIONINSTANCEID, ...) 進階控制。波形音訊 API 雖屬舊世代,但其「開啟→準備緩衝→加入佇列→啟動→訊息循環處理→清理」的清晰流程,構成了理解現代音訊管線 (WASAPI、Audio Graph) 的最佳入門模型。掌握雙緩衡循環、MM_WIM_DATA 即時寫入、WAV 標頭構建等核心技巧,即可開發出穩定、低延遲、無長度限制的錄音應用。若專案需超低延遲 (<10ms)、裝置獨佔、多通道同步、音訊端點切換通知等進階功能,請遷移至 WASAPI (共享/獨佔模式) 或 Audio Client API。