前几天看书了解了语音通话的原理,就很想自己尝试一下,然后就——做出来了,嘻嘻,先看看效果吧!
因为这边没办法上传视频,所以只能录制一个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.挂断通知 需要挂断包对吧。用户挂断情况可能是主动挂断,或者是程序异常关闭,所以挂断包可以添加一点挂断信息等等。