xuyue111 发表于 2022-1-11 15:01:28

QQ的语音聊天室,或多人语音聊天(组图)

语音聊天室,或多人语音聊天,是即时通讯应用中的常用功能之一。比如QQ的语音讨论群,我们用的比较多。

本文将实现一个简单的语音聊天室,允许多人进入同一个房间进行语音交流。先看一下运行效果的截图:

http://tt.ccoox.cn/data/attachment/forum/20220111/1641884488946_0.png

http://tt.ccoox.cn/data/attachment/forum/20220111/1641884488946_2.png

三张图从左到右分别是:登录界面、语音聊天室主界面、标有各个控件的主界面。

(如果觉得界面太丑也没关系,后面下载源码后,自己美化一下吧~~)

一. C/S 结构

很明显,我的语音聊天室采用的是C/S结构,整个项目结构比较简单,如下图:

http://tt.ccoox.cn/data/attachment/forum/20220111/1641884488946_3.png

该项目的底层是建立在 OMCS 之上的。这样,服务端基本不用写代码,直接使用OMCS服务端;客户端比较麻烦,所以我将重点放在客户端的开发上。

二. 客户端控制开发

客户端开发了几个自定义控件,然后将它们组装在一起以完成语音聊天室功能。为方便说明,我的主界面的示意图标明了各个自定义控件。

现在我们分别介绍各个控件:

1. 分贝监视器

分贝显示用于显示声音的音量,例如麦克风拾取的声音的音量聊天室代码,或扬声器播放的声音的音量。如上图3所示。

(1)傅里叶变换

傅里叶变换用于将声音数据转换为分贝强度。它对应于客户端项目中的静态类。源码比较简单,这里就不贴了,大家自己看吧。

(2)声强显示控件

用于显示声音强度的大小。

每当有声音数据需要显示时,首先会调用上面的傅里叶变换将其转换为分贝,然后再映射到对应的Value。

2.扬声器控制

用于表示聊天室中的成员天外神坛,如上图 1 所示。它显示成员的 ID、成员的声音强度(使用控件)以及他们的麦克风状态(启用、引用)。

这个控件很重要,我贴一下它的源码:

<p><pre>    <span style="color: rgba(0, 0, 255, 1)">    public</span> <span style="color: rgba(0, 0, 255, 1)">partial</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(0, 128, 128, 1)"> SpeakerPanel</span> : <span style="color: rgba(0, 128, 128, 1)">UserControl</span> ,<span style="color: rgba(0, 128, 128, 1)">IDisposable</span>
    {
      </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(0, 128, 128, 1)"> ChatUnit</span> chatUnit;   
      </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> SpeakerPanel()
      {
            InitializeComponent();
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.SetStyle(<span style="color: rgba(0, 128, 128, 1)">ControlStyles</span>.ResizeRedraw, <span style="color: rgba(0, 0, 255, 1)">true</span>);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">调整大小时重绘</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.SetStyle(<span style="color: rgba(0, 128, 128, 1)">ControlStyles</span>.OptimizedDoubleBuffer, <span style="color: rgba(0, 0, 255, 1)">true</span>);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 双缓冲</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.SetStyle(<span style="color: rgba(0, 128, 128, 1)">ControlStyles</span>.AllPaintingInWmPaint, <span style="color: rgba(0, 0, 255, 1)">true</span>);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 禁止擦除背景.</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.SetStyle(<span style="color: rgba(0, 128, 128, 1)">ControlStyles</span>.UserPaint, <span style="color: rgba(0, 0, 255, 1)">true</span>);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">自行绘制            </span>
            <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.UpdateStyles();
      }
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)"> MemberID
      {
            </span><span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.chatUnit == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
                }
                </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.MemberID;
            }
      }
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Initialize(<span style="color: rgba(0, 128, 128, 1)">ChatUnit</span> unit)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.chatUnit =<span style="color: rgba(0, 0, 0, 1)"> unit;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.skinLabel_name.Text =<span style="color: rgba(0, 0, 0, 1)"> unit.MemberID;
                  
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.chatUnit.MicrophoneConnector.<span style="color: rgba(255, 0, 0, 1)">ConnectEnded</span> += <span style="color: rgba(0, 0, 255, 1)">new</span> <span style="color: rgba(0, 128, 128, 1)">CbGeneric</span><OMCS.Passive.<span style="color: rgba(0, 128, 128, 1)">ConnectResult</span>><span style="color: rgba(0, 0, 0, 1)">(MicrophoneConnector_ConnectEnded);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.chatUnit.MicrophoneConnector.<span style="color: rgba(255, 0, 0, 1)">OwnerOutputChanged</span> += <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(0, 128, 128, 1)"> CbGeneric</span>(MicrophoneConnector_OwnerOutputChanged);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.chatUnit.MicrophoneConnector.<span style="color: rgba(255, 0, 0, 1)">AudioDataReceived</span> += <span style="color: rgba(0, 0, 255, 1)">new</span> <span style="color: rgba(0, 128, 128, 1)">CbGeneric</span><<span style="color: rgba(0, 0, 255, 1)">byte</span>[]><span style="color: rgba(0, 0, 0, 1)">(MicrophoneConnector_AudioDataReceived);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.MicrophoneConnector.BeginConnect(unit.MemberID);
      }
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Initialize(<span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)"> curUserID)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.skinLabel_name.Text =<span style="color: rgba(0, 0, 0, 1)"> curUserID;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.skinLabel_name.ForeColor =<span style="color: rgba(0, 0, 0, 1)"> Color.Red;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic.Visible = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.decibelDisplayer1.Visible = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
      }
      </span><span style="color: rgba(0, 0, 255, 1)">void</span> MicrophoneConnector_AudioDataReceived(<span style="color: rgba(0, 0, 255, 1)">byte</span><span style="color: rgba(0, 0, 0, 1)">[] data)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.decibelDisplayer1.DisplayAudioData(data);
      }
      </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> MicrophoneConnector_OwnerOutputChanged()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.InvokeRequired)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.BeginInvoke(<span style="color: rgba(0, 0, 255, 1)">new</span> <span style="color: rgba(0, 128, 128, 1)">CbGeneric</span>(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.MicrophoneConnector_OwnerOutputChanged));
            }
            </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.ShowMicState();
            }
      }
      </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ConnectResult connectResult;
      </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> MicrophoneConnector_ConnectEnded(<span style="color: rgba(0, 128, 128, 1)">ConnectResult</span> res)
      {            
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.InvokeRequired)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.BeginInvoke(<span style="color: rgba(0, 0, 255, 1)">new</span> <span style="color: rgba(0, 128, 128, 1)">CbGeneric</span><<span style="color: rgba(0, 128, 128, 1)">ConnectResult</span>>(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.MicrophoneConnector_ConnectEnded), res);
            }
            </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.connectResult =<span style="color: rgba(0, 0, 0, 1)"> res;
                </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.ShowMicState();
            }
      }
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Dispose()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.Close();
      }
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> ShowMicState()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.connectResult !=<span style="color: rgba(0, 0, 0, 1)"> OMCS.Passive.<span style="color: rgba(0, 128, 128, 1)">ConnectResult</span>.Succeed)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic.BackgroundImage = <span style="color: rgba(0, 0, 255, 1)">this</span>.imageList1.Images[<span style="color: rgba(128, 0, 128, 1)">2</span><span style="color: rgba(0, 0, 0, 1)">];
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.toolTip1.SetToolTip(<span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic, <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.connectResult.ToString());
            }
            </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">this</span>.decibelDisplayer1.Working = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.MicrophoneConnector.OwnerOutput)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic.BackgroundImage = <span style="color: rgba(0, 0, 255, 1)">this</span>.imageList1.Images[<span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">];
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.toolTip1.SetToolTip(<span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">好友禁用了麦克风</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                  </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
                }
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.MicrophoneConnector.Mute)
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic.BackgroundImage = <span style="color: rgba(0, 0, 255, 1)">this</span>.imageList1.Images[<span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">];
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.toolTip1.SetToolTip(<span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">静音</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                }
                </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
                {
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic.BackgroundImage = <span style="color: rgba(0, 0, 255, 1)">this</span>.imageList1.Images[<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">];
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.toolTip1.SetToolTip(<span style="color: rgba(0, 0, 255, 1)">this</span>.pictureBox_Mic, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">正常</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                  </span><span style="color: rgba(0, 0, 255, 1)">this</span>.decibelDisplayer1.Working = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
                }
            }
      }
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> pictureBox_Mic_Click(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, <span style="color: rgba(0, 128, 128, 1)">EventArgs</span> e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.MicrophoneConnector.OwnerOutput)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
            }
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.chatUnit.MicrophoneConnector.Mute = !<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.chatUnit.MicrophoneConnector.Mute;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.ShowMicState();
      }
    }</span></pre></p>
(1)在代码中,它代表了这个聊天室的当前成员。我们用它来连接目标成员的麦克风。

(2)预定事件,当接收到语音数据时,移交给显示声音的音量。

(3)预定sum事件聊天室代码,根据其结果在空间上显示麦克风图标的状态(对应方法)。

3.ner 控制

ner对应上图中标为2的控件,主要做了以下几件事:

(1)初始化时加入聊天室:通过调用属性的Join方法。

(2)用于对应列出聊天室中的每个成员。

(3)当成员加入或离开聊天室(对应和事件)时,动态添加或移除对应的实例。

(4)通过公开对我们自己的设备(麦克风和扬声器)的控制。我们可以启用或禁用我们自己的麦克风或扬声器。

(5)注意它提供了一个Close方法,也就是说当包含该控件的宿主窗体关闭时,应该调用它的Close方法来释放内部持有的麦克风连接器等资源。

完成ner之后,我们聊天室的核心就差不多了。下一步是获取一个主窗体,然后将 ner 向上拖动,对其进行初始化,并将其传递给 ner,您就完成了。

三. 源码下载

以上只谈了多人语音聊天室实现中的几个关键点,并不全面。您可以下载下面的源代码进行更深入的研究。

.rar

最后说一下部署步骤:

(1)在一台机器上部署服务器并启动服务器。

(2)在客户端配置文件中修改刚才服务器的IP。

(3)在多台机器上运行客户端,用不同的账号登录同一个房间(就像默认的R1000).

(4)这样,多个用户在同一个聊天室进行语音聊天。

请理解:

通信框架OMCS网络语音和视频框架MFile语音和视频录制组件语音和视频捕获组件轻量级通信引擎
页: [1]
查看完整版本: QQ的语音聊天室,或多人语音聊天(组图)