C++用winapi socket實現局域網語音通話功能

前幾天看書瞭解瞭語音通話的原理,就很想自己嘗試一下,然後就——做出來瞭,嘻嘻,先看看效果吧!

因為這邊沒辦法上傳視頻,所以隻能錄制一個gif大傢看一下效果,但是是可以聽到聲音的。

源碼下載鏈接:http://xiazai.jb51.net/202206/yuanma/luyinj_jb51.rar

功能介紹: 1.支持錄音設備查找以及播放設備查找 2.支持局域網語音通話 3.通話包含語音來電提醒和掛斷電話的提示信息,還能實時的獲取在線用戶的數量以及對應的id,其他功能正在開發,期待大傢一起進步。

一、socket通信

唔,感覺全部放在這裡面感覺會很長,以後寫一篇其他的文章詳細介紹這個內容吧。

二、waveIn和WaveOut的Win32API

1.音頻設備的的信息獲取

首先是輸入音頻設備個數的獲取,僅僅通過調用下面的函數就可以瞭,沒有輸入參數,輸出設備個數獲取的函數調用方式一樣,名稱略有不同。 waveInGetNumDevs(); //獲取輸入音頻設備個數 waveOutGetNumDevs(); //獲取輸出音頻設備個數 然後便是獲取具體的設備信息瞭,具體調用如下面所示:

	//音頻輸入設備信息的獲取
	tstring sDevText = L"";		//這裡用的是unicode的寬字符串的tstring對象
	WAVEINCAPS waveCaps;	//用於獲取設備信息的結構體
	int res = waveInGetDevCaps(dwID, &waveCaps, sizeof(WAVEINCAPS));
	if (res == MMSYSERR_NOERROR)
	{
		sDevText = waveCaps.szPname;	//此處保存的是設備名稱
	}
	//音頻輸出設備信息的獲取,方式和上面類似,隻不過函數名稱略有不同
	tstring sDevText = L"";
	WAVEOUTCAPS waveCaps;
	int res = waveOutGetDevCaps(dwID, &waveCaps, sizeof(WAVEOUTCAPS));
	if (res == MMSYSERR_NOERROR)
	{
		sDevText = waveCaps.szPname;
	}

2.音頻設備的初始化

首先是打開音頻設備: waveInOpen函數參數說明: m_hWaveIn 表示的是音頻輸入的設備句柄,參數為該句柄的地址 iWaveInDevID 表示的是音頻輸入設備的ID,ID默認是從0開始的,如果是在不知道音頻ID的話,可以將該參數設置為 WAVE_MAPPER ,即默認選擇。 m_soundFormat 表示的是打開設備的格式,這個就略微復雜一點瞭,常用的參數有幾個吧: m_soundFormat.wFormatTag = WAVE_FORMAT_PCM; //這個是采樣數據的格式,其他的咱也不懂,就用默認的 PCM 脈沖采樣的格式。 m_soundFormat.nChannels = 1; //通道數 m_soundFormat.nSamplesPerSec = 11025; //采樣率,常用的有11.025 kHz、22.05 kHz和44.1 kHz,其他的不建議設一些不規則的數。 m_soundFormat.nAvgBytesPerSec = 11025; //不懂,和采樣率一般設置為一樣的數 m_soundFormat.wBitsPerSample = 8; //表示的是數據位數,8或者16位 m_soundFormat.cbSize = 0; //一般為0 hWnd 這個是用於接收錄音通知消息的句柄,填做主窗口句柄就行,註意一個類型轉換。 0L 它的名字是 dwInstance,不太懂,沒什麼關系其實 CALLBACK_WINDOW 表示的是 dwCallback(也就是hWnd) 是個窗口句柄,指定的是 dwCallback(也就是hWnd) 參數是什麼東西。給大傢看一下原版的英文解釋,這個有好多種內容,不想看的可以跳過瞭,看起來挺晦澀的:

fdwOpen: Flags for opening the device. The following values are defined: CALLBACK_EVENT The dwCallback parameter is an event handle. CALLBACK_FUNCTION The dwCallback parameter is a callback procedure address. CALLBACK_NULL No callback mechanism. This is the default setting. CALLBACK_THREAD The dwCallback parameter is a thread identifier. CALLBACK_WINDOW The dwCallback parameter is a window handle. WAVE_FORMAT_DIRECT If this flag is specified, the ACM driver does not perform conversions on the audio data. WAVE_FORMAT_QUERY The function queries the device to determine whether it supports the given format, but it does not open the device. WAVE_MAPPED The uDeviceID parameter specifies a waveform-audio device to be mapped to by the wave mapper.

返回值為 MMSYSERR_NOERROR 表示失敗瞭,然後這裡對返回值做一個判斷。

	HWAVEIN m_hWaveIn;							//音頻輸入的句柄
	//打開錄音設備,采用窗口方式接收音頻消息
	int res = waveInOpen(&m_hWaveIn, iWaveInDevID, &m_soundFormat, (DWORD)hWnd, 0L, CALLBACK_WINDOW);
	if (res != MMSYSERR_NOERROR)
		return false;

輸出設備的打開啊方式類似,此處也不做過多解釋瞭,大傢看一下就差不多能懂瞭,相信能認認真真看這篇博客的應該都是很棒的人。 (註意:此處的 m_hWaveOut 類型是 HWAVEOUT,和上面的那個不一樣,註意區分哦)

	//======================== 播放 ==========================
	res = waveOutOpen(&m_hWaveOut, iWaveOutDevID, &m_soundFormat, (DWORD)hWnd,
		0L, CALLBACK_WINDOW);
	if (res != MMSYSERR_NOERROR)
		return false;

3.輸入輸出設備緩沖區的準備和添加

老樣子瞭,先從音頻輸入設備講起: MAX_BUFFER_SIZE 是自己設置的一個宏定義,給大傢一個大概的數量大小參考吧,10240 或者 20480 都可以的,這個其實是一個平衡,如果緩沖區過大,那麼通話延遲比較高,如果比較少,則通話的連續性質量不高,自己看著試試就行 m_pWaveHdrIn.dwBytesRecorded 表示的是在準備這個緩沖區的時候,裡面的初始數據占多少字節,填個 0 就行。 m_pWaveHdrIn.dwFlags 參數有好多內容,有興趣的可以看看下面的參考內容:

方法

提供緩沖區信息的標志。定義瞭以下值: WHDR_BEGINLOOP 這個緩沖區是循環中的第一個緩沖區。該標志僅用於輸出緩沖區。 WHDR_DONE 由設備驅動程序設置,表示緩沖區已用完,正在將其返回給應用程序。 WHDR_ENDLOOP 這個緩沖區是循環中的最後一個緩沖區。該標志僅用於輸出緩沖區。 WHDR_INQUEUE 由窗口設置,表示緩沖區已排隊等待回放。 WHDR_PREPARED 由窗口設置,表示緩沖區已用波形預預熱器或波形輸出預預熱器功能準備好。

waveInPrepareHeader 函數主要是準備音頻輸入設備的緩沖區(其實翻譯一下看名字大概就能猜出來),大概參數介紹: m_hWaveIn 音頻輸入設備的句柄 m_pWaveHdrIn 緩沖區的地址 sizeof(WAVEHDR) 這個參數麼,不用多說瞭哈哈

下一個函數 waveInAddBuffer 也簡單,不多說瞭,相信大傢的實力。

	char m_cBufferIn[MAX_BUFFER_SIZE];	//這個是實際的緩沖區空間
	WAVEHDR m_pWaveHdrIn;				//這是一個結構體,用於函數調用參數的一個內容
	
	//準備內存塊錄音
	m_pWaveHdrIn.lpData = m_cBufferIn;
	m_pWaveHdrIn.dwBufferLength = MAX_BUFFER_SIZE; 
	m_pWaveHdrIn.dwBytesRecorded = 0;
	m_pWaveHdrIn.dwFlags = 0;
	res = waveInPrepareHeader(m_hWaveIn, &m_pWaveHdrIn, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;
	//增加內存塊
	res = waveInAddBuffer(m_hWaveIn, &m_pWaveHdrIn, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;

音頻輸出設備的緩沖區準備和添加類似,參考下面代碼:

	//準備內存塊播放
	m_pWaveHdrout.lpData = m_cBufferout;
	m_pWaveHdrout.dwBufferLength = MAX_BUFFER_SIZE;
	m_pWaveHdrout.dwBytesRecorded = 0;
	m_pWaveHdrout.dwFlags = 0;
	res = waveOutPrepareHeader(m_hWaveOut, &m_pWaveHdrout, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;
	//指定數據塊到音頻播放緩沖區
	res = waveOutWrite(m_hWaveOut, &m_pWaveHdrout, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;

4.播放和錄音的開始和終止

先寫這幾個比較簡單的操作: 開始錄音:waveInStart(m_hWaveIn); 停止錄音:waveInStop(m_hWaveIn); 停止播放:waveOutReset(m_hWaveOut); 然後播放錄音的操作略微復雜一點,需要把數據放到播放緩沖區,緩沖區的內容會自動播放: m_cBufferout 播放緩沖區的首地址 pData 要播放的聲音數據流首地址 dwDataLen 聲音數據流的長度

memcpy(m_cBufferout, pData, dwDataLen); (函數不唯一啊,這個windows有好多,例如CopyMemory也可以實現這個功能,這裡用的是 memcpy 函數)

5.錄音通知消息的獲取和處理

開始錄音後,緩沖區不斷地增加捕獲到的音頻數據,當音頻數據接受滿瞭之後,就會向前文說的那個窗口句柄的窗口發送通知消息 MM_WIM_DATA ,收到這個消息之後程序就要對這些數據進行處理,處理完畢後最最重要一件事是清空緩沖區,windows並不會自己清理緩沖區內容。 清理緩沖區用到的函數是 waveInUnprepareHeader 這個參數其實差不多,

waveInPrepareHeader函數清理由WaveInPrepareHeader函數執行的準備。該函數必須在設備驅動程序填充緩沖區並將其返回給應用程序後調用。在釋放緩沖區之前,您必須調用此函數。

這個是官方的解釋,感覺這個挺詳細的,放在這裡大傢看看。清空緩沖區之後,重新準備緩沖區,和上面的操作一樣。

	int res = waveInUnprepareHeader(m_hWaveIn, &m_pWaveHdrIn, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;
		
	//準備內存塊錄音
	m_pWaveHdrIn.lpData = m_cBufferIn;
	m_pWaveHdrIn.dwBufferLength = MAX_BUFFER_SIZE;
	m_pWaveHdrIn.dwFlags = 0;
	res = waveInPrepareHeader(m_hWaveIn, &m_pWaveHdrIn, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;
	//增加內存塊
	res = waveInAddBuffer(m_hWaveIn, &m_pWaveHdrIn, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
		return false;

清空輸出緩沖區的函數(程序結束,記得清空緩沖區內容):

int res = waveOutUnprepareHeader(m_hWaveOut, &m_pWaveHdrout[0], sizeof(WAVEHDR));

6.關閉音頻輸入和輸出設備

調用兩個特別簡單的函數實現最終的收尾工作,哦耶!

	if (m_hWaveIn)
	{
		waveInClose(m_hWaveIn);
		m_hWaveIn = NULL;
	}
	if (m_hWaveOut)
	{
		waveOutClose(m_hWaveOut);
		m_hWaveOut = NULL;
	}

三、通信數據包的設計以及客戶端服務器邏輯

這個怎麼說呢,感覺就要從實際出發瞭,這裡簡單的說一下思路吧。 功能分析: 1.客戶端登陸ID分配以及其他客戶端的廣播 可以用靜態變量++來為客戶端賦值ID,以此保證每個用戶ID不重復,然後廣播就遍歷所有的客戶端。包括登陸包,反饋包,廣播包。 2.撥打電話提示 這個就是撥打電話請求包和撥打電話的回復包兩個是吧。 3.聲音數據的傳輸 必須指定誰的語音信息發到哪個客戶端,所以語音包必須包含發送用戶的ID和接收用戶的ID。 4.掛斷通知 需要掛斷包對吧。用戶掛斷情況可能是主動掛斷,或者是程序異常關閉,所以掛斷包可以添加一點掛斷信息等等。

推薦閱讀: