语音聊天_语音聊天系统 - CSDN
  • 语音视频聊天系统(大集合)

    热门讨论 2020-07-29 14:21:00
    有关于语音视频聊天系统的大合集,有MFC、java、C#的,还有模仿qq界面的,亲们,赶紧下,分数给我吧
  • 语音聊天

    千次阅读 2017-05-11 16:17:14
    之前做过文本转语音聊天机器人,昨天又恰好做了关于音频处理的。借此机会,整合一下,来做个有界面的语音文本聊天机器人好了。先来看看最终的效果图。 对这些基础内容不是很了解的可以参考我之前的

    前言

    这几天看了点PyQt相关的知识,感觉可以结合之前得一些内容做出点什么好玩的东西。

    之前做过文本转语音的聊天机器人,昨天又恰好做了关于音频处理的。借此机会,整合一下,来做个有界面的语音文本聊天机器人好了。

    先来看看最终的效果图。
    文本语音聊天机器人效果图

    对这些基础内容不是很了解的可以参考我之前的文章。

    环境

    环境搭建是个坑,之前一直在用的pyttsx语音引擎竟然不支持Python36,只能在Python27版本使用。所以无奈只能选用微软的提供的win32com.client了。

    本机环境

    本机环境如下

    • Windows10 64位
    • Python36
    • PyCharm pro

    所需包

    所需包一开始我是手动统计的,但是后来觉得版本这块最好还是精确一下,于是使用了pip的一个freeze命令。

    pip freeze > requirements.txt

    得到了下面的这些所需的库文件(我删除了一些没用到的)。

    PyAudio==0.2.11
    PyQt5==5.8.2
    pyttsx==1.1
    pywin32==221
    requests==2.13.0
    sip==4.19.2

    各大模块

    下面开始针对各大模块简要的介绍一下。

    百度语音接口

    百度语音接口是用来处理本地音频到文本内容转换而使用的。需要用到标准库中的wave库,来处理.wav音频文件。

    # coding: utf8
    
    # @Author: 郭 璞
    # @File: baiduyuyin.py                                                                 
    # @Time: 2017/5/11                                   
    # @Contact: 1064319632@qq.com
    # @blog: http://blog.csdn.net/marksinoberg
    # @Description: 借助百度语音识别接口实现本地语音解析
    
    import pyaudio
    import wave
    import requests
    import json
    
    class BaiDuYuYin(object):
    
        def __init__(self):
            # get the token
            self.token = self.gettoken()
    
        def gettoken(self):
            try:
                apiKey = "Ll0c嘿嘿2ZSGAU"
                secretKey = "44c8a这个不能说34936227d4a19dc2"
    
                auth_url = "https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=" + apiKey + "&client_secret=" + secretKey
                response = requests.get(url=auth_url)
                jsondata = response.text
                return json.loads(jsondata)['access_token']
            except Exception as e:
                raise Exception("Cannot get the token, the reason is {}".format(e))
    
        def parse(self, wavefile='local.wav'):
            """
            返回音频文件对应的文本内容。
            注意返回的是列表类型的数据,待会处理的时候要格外的小心。
            :param wavefile:
            :return:
            """
            try:
                fp = wave.open(wavefile, 'rb')
                # 已经录好音的音频片段内容
                nframes = fp.getnframes()
                filelength = nframes * 2
                audiodata = fp.readframes(nframes)
    
                # 百度语音接口的产品ID
                cuid = '7519663'
                server_url = 'http://vop.baidu.com/server_api' + '?cuid={}&token={}'.format(cuid, self.token)
                headers = {
                    'Content-Type': 'audio/pcm; rete=8000',
                    'Content-Length': '{}'.format(filelength),
                }
    
                response = requests.post(url=server_url, headers=headers, data=audiodata)
                print(response.text)
                data = json.loads(response.text)
                if data['err_msg'] == 'success.':
                    return data['result']
                else:
                    return '你说的啥啊,听不清听不清!'
            except Exception as e:
                raise Exception("Parsing wave file failed. The reason is {}".format(e))
    
    if __name__ == '__main__':
        yuyinclient = BaiDuYuYin()
        result = yuyinclient.parse(wavefile='local.wav')
        print(result)

    图灵机器人接口

    然后是图灵机器人接口,这个用于处理文本对话。免费版其实已经够用了。有需要的自己去申请吧。

    # coding: utf8
    
    # @Author: 郭 璞
    # @File: turing.py
    # @Time: 2017/5/11                                   
    # @Contact: 1064319632@qq.com
    # @blog: http://blog.csdn.net/marksinoberg
    # @Description: 文字对话接口实现
    import requests
    import json
    
    class TuringRobot(object):
    
        def __init__(self):
            self.apikey = '2a220b3哟哟哟b74c54'
            self.userid = '产品ID不能说'
            self.url = 'http://www.tuling123.com/openapi/api'
    
        def talk(self, text):
            payload = {
                'key': self.apikey,
                'userid': self.userid,
                'info': text
            }
    
            response = requests.post(url=self.url, data=payload)
            return json.loads(response.text)['text']
    
    
    if __name__ == '__main__':
        turing = TuringRobot()
        answer = turing.talk('你好吗,我是小黄鸡!')
        print(answer)

    音频处理

    昨天对于音频处理这块做了一点点的研究,今天还是那个套路。默认录音五秒,保存为同一级目录下的local.wav文件。

    # coding: utf8
    
    # @Author: 郭 璞
    # @File: recorder.py                                                                 
    # @Time: 2017/5/11                                   
    # @Contact: 1064319632@qq.com
    # @blog: http://blog.csdn.net/marksinoberg
    # @Description: 记录本地录音,默认保存为local.wav, 留作解析引擎备用。
    import pyaudio
    import wave
    
    class Recorder(object):
    
        def __init__(self):
            self.CHUNK = 1024
            self.FORMAT = pyaudio.paInt16
            self.CHANNELS = 2
            self.RATE = 44100
            self.RECORD_SECONDS = 5
            self.WAVE_OUTPUT_FILENAME = 'local.wav'
    
            self.engine = pyaudio.PyAudio()
    
        def record(self):
            try:
                # 提示语句可以使用一下语音方式,这里先打印算了。
                print("Begin Recoding ...")
    
                stream = self.engine.open(format=self.FORMAT,
                                          channels=self.CHANNELS,
                                          rate=self.RATE,
                                          input=True,
                                          frames_per_buffer=self.CHUNK)
                # 记录到的音频总数据帧
                frames = []
                for i in range(0, int(self.RATE / self.CHUNK * self.RECORD_SECONDS)):
                    data = stream.read(self.CHUNK)
                    frames.append(data)
    
                # 音频记录完毕
                print('Recording Over!')
                # 释放资源,接触阻塞监听。
                stream.stop_stream()
                stream.close()
                self.engine.terminate()
    
                # 并将音频数据保存到本地音频文件中
                wf = wave.open(self.WAVE_OUTPUT_FILENAME, 'wb')
                wf.setnchannels(self.CHANNELS)
                wf.setsampwidth(self.engine.get_sample_size(self.FORMAT))
                wf.setframerate(self.RATE)
                wf.writeframes(b''.join(frames))
                wf.close()
            except Exception as e:
                raise Exception("Recording failed. The reason is {}".format(e))
    
    
    if __name__ == '__main__':
        recorder = Recorder()
        recorder.record()
    

    本地朗读模块

    本地语音朗读相当于是一个加分项,之前一直在用的pyttsx这下尴尬了,无奈只能试用第二个方式,不过使用pyttsx的代码我还是留出来吧。万一哪天它支持了Python36,就有更多可选项的丰富功能了。

    # coding: utf8
    
    # @Author: 郭 璞
    # @File: localvoicer.py                                                                 
    # @Time: 2017/5/11                                   
    # @Contact: 1064319632@qq.com
    # @blog: http://blog.csdn.net/marksinoberg
    # @Description: 本地语音朗读实现。
    import win32com.client
    
    class Reader(object):
        """
        尴尬的是pyttsx不支持Python36,要不然还可以有更多可选项。
        """
    
        def __init__(self):
            import pyttsx
            self.engine = pyttsx.init()
            # optional property
            self.rate = self.engine.getProperty('rate')
            self.voices = self.engine.getProperty('voices')
            self.volume = self.engine.getProperty('volume')
    
        def read(self, text="", rate=200, voices="", volume=""):
            self.engine.say(text)
            self.engine.runAndWait()
    
    
    class Speaker(object):
        def __init__(self):
            self.engine = win32com.client.Dispatch("SAPI.SpVoice")
    
        def speak(self, text):
            self.engine.Speak(text)
    
    
    
    if __name__ == '__main__':
        # reader = Reader()
        # reader.read(text='Hello World!')
        speaker = Speaker()
        speaker.speak("hello world! 你好世界")
    
    

    GUI 模块

    做完了前面的部分,就差界面了。测试完毕之后发现,各大模块均能正常工作,虽然音频解析那块特别地依赖于网速,校园网这网速我也是醉了。

    下面简单的写个界面来“打包美化”一下吧。

    # coding: utf8
    
    # @Author: 郭 璞
    # @File: audioui.py                                                                 
    # @Time: 2017/5/11                                   
    # @Contact: 1064319632@qq.com
    # @blog: http://blog.csdn.net/marksinoberg
    # @Description: 外部界面
    from PyQt5 import QtCore, QtGui, QtWidgets
    from audiorobot.dispatcher import Dispatcher
    from audiorobot.baiduyuyin import BaiDuYuYin
    from audiorobot.turing import TuringRobot
    from audiorobot.localvoicer import Speaker
    
    class ClientUI(QtWidgets.QWidget):
    
        def __init__(self):
            super(ClientUI, self).__init__()
            self.dispatcher = Dispatcher()
            self.baiduyuyin = BaiDuYuYin()
            self.turingrobot = TuringRobot()
            self.speaker = Speaker()
            self.initui()
    
    
        def initui(self):
            self.setWindowTitle("图灵·聊天室")
            self.setGeometry(20, 20, 400, 500)
    
            # 顶部布局
            toplayout = QtWidgets.QHBoxLayout()
            self.textarea = QtWidgets.QTextBrowser()
            toplayout.addWidget(self.textarea)
            # 中间布局
            centerlayut = QtWidgets.QHBoxLayout()
            self.editline = QtWidgets.QLineEdit()
            self.voicebutton = QtWidgets.QPushButton("发语音")
            self.textbutton = QtWidgets.QPushButton("发文字")
            centerlayut.addWidget(self.editline)
            centerlayut.addWidget(self.voicebutton)
            centerlayut.addWidget(self.textbutton)
    
            mainlayout = QtWidgets.QVBoxLayout()
            mainlayout.addLayout(toplayout)
            mainlayout.addLayout(centerlayut)
    
            self.setLayout(mainlayout)
            # 关于事件处理,交给handler来处理即可
            self.eventhandler()
    
        def eventhandler(self):
            self.voicebutton.clicked.connect(self.pushvoice)
            self.textbutton.clicked.connect(self.pushtext)
    
        def pushvoice(self):
            print('voice')
            # 先保存到本地,再调用语音接口上传
            self.dispatcher.record()
            response = self.baiduyuyin.parse()
            print('百度语音接口解析到的数据为:{}'.format(response))
            self.speaker.speak(text=response)
            # 更新一下窗体文本域的内容
            text = self.textarea.toPlainText()+"\n"+"<<< "+"上传音频中..."
            self.textarea.setText(text)
            text =  text +"\n>>> " +response
            self.textarea.setText(text)
    
    
        def pushtext(self):
            inputtext = self.editline.text()
            print(inputtext)
            trans = self.turingrobot.talk(text=inputtext)
            self.speaker.speak(text=trans)
            # 更新文本域内容
            text = self.textarea.toPlainText() + "\n<<<"+inputtext
            self.textarea.setText(text)
            text = self.textarea.toPlainText() + "\n>>> " + trans
            self.textarea.setText(text)
            self.editline.clear()
    
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        ui = ClientUI()
        ui.show()
        sys.exit(app.exec_())
    

    演示

    好了,大功告成。运行代码的时候仅仅需要下面的这个命令就可以了。

    Python audioui.py

    文本

    先来看看对于文本的处理,这其实跟之前的聊天机器人没啥区别。仅仅是带了语音朗读罢了。
    文本测试

    语音

    然后是语音测试,我本人在图书馆。所以为了不打扰别人,录音的时候只能假装咳嗽来掩盖测试录音的事实,所以效果不是很好。但是如果是标准的普通话,测试的结果还是差强人意的。
    音频处理


    总结

    最后还是来总结一下。

    本次也算是整合的比较多的内容了。模块内测试都是用的

    if __name__ == "__main__":
        # testing code
        pass

    看起来还算不错,单元测试倒是没什么必要,毕竟代码量还很少。集成测试也算是马马虎虎,功能这块倒是还能满足需求,但是以后如果代码量大了的话,还是要好好测测的,以防万一。

    已知的缺点就是界面这块。录音的时候要是能加一个statusBar实时提醒录音进度就好了,而且录音的时候是阻塞线程的,不是很优雅。

    展开全文
  • 原创性申明 此博文的出处 为 ...概述 之前发过一篇文章c#中关于udp实现可靠地传输(数据包的分组发送) 已经实现过了UDP的分包发送数据的功能,而这篇文章主要是一个应用,使用ud

    原创性申明

    此博文的出处 为 http://blog.csdn.net/zhujunxxxxx/article/details/40124773如果进行转载请注明出处。本文作者原创,邮箱zhujunxxxxx@163.com,如有问题请联系作者

    概述

    之前发过一篇文章【C#使用UDP实现可靠的传输传输(数据包的分组发送)】以及【基于事件模型的UDP通讯框架(适用于网络包编解码)已经实现过了UDP的分包发送数据的功能,而这篇文章主要是一个应用,使用UDP协议传送语音和文本等信息。在这个系统中没有服务端和客户端,相互通讯都是直接相互联系的,能够很好的实现效果。

    具体实现

    1、语言数据源获取

    要想发送语音信息,首先得获取语音,这里有几种方法,一种是使用DirectX的DirectXsound来录音,我为了简便使用一个开源的插件NAudio来实现语音录取。 首先在项目中引用NAudio.dll组件,具体的实现代码如下所示:

    //------------------录音相关-----------------------------
            private IWaveIn waveIn;
            private WaveFileWriter writer;
    
    
            private void LoadWasapiDevicesCombo()
            {
                var deviceEnum = new MMDeviceEnumerator();
                var devices = deviceEnum.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active).ToList();
                comboBox1.DataSource = devices;
                comboBox1.DisplayMember = "FriendlyName";
            }
            private void CreateWaveInDevice()
            {
    
                waveIn = new WaveIn();
                waveIn.WaveFormat = new WaveFormat(8000, 1);
                waveIn.DataAvailable += OnDataAvailable;
                waveIn.RecordingStopped += OnRecordingStopped;
            }
            void OnDataAvailable(object sender, WaveInEventArgs e)
            {
                if (this.InvokeRequired)
                {
                    this.BeginInvoke(new EventHandler<WaveInEventArgs>(OnDataAvailable), sender, e);
                }
                else
                {
                    writer.Write(e.Buffer, 0, e.BytesRecorded);
                    int secondsRecorded = (int)(writer.Length / writer.WaveFormat.AverageBytesPerSecond);
                    if (secondsRecorded >= 10)//最大10s
                    {
                        StopRecord();
                    }
                    else
                    {
                        l_sound.Text = secondsRecorded + " s";
                    }
                }
            }
            void OnRecordingStopped(object sender, StoppedEventArgs e)
            {
                if (InvokeRequired)
                {
                    BeginInvoke(new EventHandler<StoppedEventArgs>(OnRecordingStopped), sender, e);
                }
                else
                {
                    FinalizeWaveFile();
                }
            }
            void StopRecord()
            {
                AllChangeBtn(btn_luyin, true);
                AllChangeBtn(btn_stop, false);
                AllChangeBtn(btn_sendsound, true);
                AllChangeBtn(btn_play, true);
    
    
                //btn_luyin.Enabled = true;
                //btn_stop.Enabled = false;
                //btn_sendsound.Enabled = true;
                //btn_play.Enabled = true;
                if (waveIn != null)
                    waveIn.StopRecording();
                //Cleanup();
            }
    		private void Cleanup()
            {
                if (waveIn != null)
                {
                    waveIn.Dispose();
                    waveIn = null;
                }
                FinalizeWaveFile();
            }
            private void FinalizeWaveFile()
            {
                if (writer != null)
                {
                    writer.Dispose();
                    writer = null;
                }
            }
    		 //开始录音
            private void btn_luyin_Click(object sender, EventArgs e)
            {
                btn_stop.Enabled = true;
                btn_luyin.Enabled = false;
                if (waveIn == null)
                {
                    CreateWaveInDevice();
                }
                if (File.Exists(soundfile))
                {
                    File.Delete(soundfile);
                }
    
                writer = new WaveFileWriter(soundfile, waveIn.WaveFormat);
                waveIn.StartRecording();
            }

    上面的代码实现了录音,并且写入本地文件p2psound_A.wav中存储

    语音发送

    当录好音后,需要将录好的文件发送出去,具体的实现代码如下所示:

     MsgTranslator tran = null;
     public Form1()
            {
                InitializeComponent();
                LoadWasapiDevicesCombo();//显示音频设备
    
                Config cfg = SeiClient.GetDefaultConfig();
                cfg.Port = 7777;
                UDPThread udp = new UDPThread(cfg);
                tran = new MsgTranslator(udp, cfg);
                tran.MessageReceived += tran_MessageReceived;
                tran.Debuged += new EventHandler<DebugEventArgs>(tran_Debuged);
            }
    		private void btn_sendsound_Click(object sender, EventArgs e)
            {
                if (t_ip.Text == "")
                {
                    MessageBox.Show("请输入ip");
                    return;
                }
                if (t_port.Text == "")
                {
                    MessageBox.Show("请输入端口号");
                    return;
                }
                string ip = t_ip.Text;
                int port = int.Parse(t_port.Text);
                string nick = t_nick.Text;
                string msg = "语音消息";
    
                IPEndPoint remote = new IPEndPoint(IPAddress.Parse(ip), port);
                Msg m = new Msg(remote, "zz", nick, Commands.SendMsg, msg, "Come From A");
                m.IsRequireReceive = true;
                m.ExtendMessageBytes = FileContent(soundfile);
                m.PackageNo = Msg.GetRandomNumber();
                m.Type = Consts.MESSAGE_BINARY;
                tran.Send(m);
            }
    		private byte[] FileContent(string fileName)
            {
                FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                try
                {
                    byte[] buffur = new byte[fs.Length];
                    fs.Read(buffur, 0, (int)fs.Length);
    
                    return buffur;
                }
                catch (Exception ex)
                {
                    return null;
                }
                finally
                {
                    if (fs != null)
                    {
    
                        //关闭资源
                        fs.Close();
                    }
                }
            }
    如上面代码所示,我们就把产生的语音文件发送出去了,发送端的处理结束。


    语音的接收与播放

    语音的接收和文本消息的接收没有什么不同,只不过语音发送的时候是以二进制发送的,因此我们在收到语音后 就应该写入到一个文件里面去,接收完成后,播放这段语音就行了。

    下面这段代码主要是把收到的数据保存到文件中去,这个函数式我的NetFrame里收到消息时所触发的事件,在文章前面提过的那篇文章里。

    void tran_MessageReceived(object sender, MessageEventArgs e)
            {
                Msg msg = e.msg;
    
                if (msg.Type == Consts.MESSAGE_BINARY)
                {
                    string m = msg.Type + "->" + msg.UserName + "发来二进制消息!";
                    AddServerMessage(m);
                    if (File.Exists(recive_soundfile))
                    {
                        File.Delete(recive_soundfile);
                    }
                    FileStream fs = new FileStream(recive_soundfile, FileMode.Create, FileAccess.Write);
                    fs.Write(msg.ExtendMessageBytes, 0, msg.ExtendMessageBytes.Length);
                    fs.Close();
                    //play_sound(recive_soundfile);
                    ChangeBtn(true);
    
                }
                else
                {
                    string m = msg.Type + "->" + msg.UserName + "说:" + msg.NormalMsg;
                    AddServerMessage(m);
                }
            }

    收到语音消息后,我们要进行播放,播放时仍然用刚才那个插件播放,具体的实现代码如下所示:
    //--------播放部分----------
            private IWavePlayer wavePlayer;
            private WaveStream reader;
    
    
            public void play_sound(string filename)
            {
                if (wavePlayer != null)
                {
                    wavePlayer.Dispose();
                    wavePlayer = null;
                }
                if (reader != null)
                {
                    reader.Dispose();
                }
                reader = new MediaFoundationReader(filename, new MediaFoundationReader.MediaFoundationReaderSettings() { SingleReaderObject = true });
    
                if (wavePlayer == null)
                {
    
                    wavePlayer = new WaveOut();
                    wavePlayer.PlaybackStopped += WavePlayerOnPlaybackStopped;
                    wavePlayer.Init(reader);
                }
                wavePlayer.Play();
            }
            private void WavePlayerOnPlaybackStopped(object sender, StoppedEventArgs stoppedEventArgs)
            {
                if (stoppedEventArgs.Exception != null)
                {
                    MessageBox.Show(stoppedEventArgs.Exception.Message);
                }
                if (wavePlayer != null)
                {
                    wavePlayer.Stop();
                }
                btn_luyin.Enabled = true;
            }private void btn_play_Click(object sender, EventArgs e)
            {
                btn_luyin.Enabled = false;
                play_sound(soundfile);
            }

    界面展示:



    技术总结

    整个系统实现的过程中,主要用到的技术就是UDP和NAudio的录音和播放功能,其中用到的UDP传输类我放在了github上面 地址在我的博客左边的个人介绍里有地址  项目地址 https://github.com/zhujunxxxxx/ZZNetFrame希望这篇文章能够提供一个思路。


    更新程序下载地址 http://download.csdn.net/detail/zhujunxxxxx/8061125 很好的代码,希望大家喜欢!


    展开全文
  • 多人语音聊天,或语音聊天室,是即时通信应用中常见的功能之一,比如,QQ的语音讨论组就是我们用得比较多的。本文将实现一个简单的语音聊天室,让多个人可以进入同一个房间进行语音沟通。

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

          本文将基于最新版本的OMCS(V3.5)实现一个简单的语音聊天室,让多个人可以进入同一个房间进行语音沟通。当然,在此之前,您必须对OMCS有所了解,并且已经阅读、理解了OMCS 开发手册(08) -- 多人语音/视频 这篇文章的内容。先看看Demo运行效果截图:

            

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

    一. C/S结构

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

           

       该项目的服务端不需要编写任何代码,直接把OMCS服务端拿过来用;客户端就比较麻烦些,下面我们就重点讲客户端的开发。

    二. 客户端控件式开发

      客户端开发了多个自定义控件,然后将它们组装到一起,以完成语音聊天室的功能。为了便于讲解,我主界面的图做了标注,以指示出各个自定义控件。  

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

    1. 分贝显示器 

      分贝显示器用于显示声音的大小,比如麦克风采集到的声音的大小,或扬声器播放的声音的大小。如上图中3标注的。

    (1)傅立叶变换

      将声音数据转换成分贝强度使用的是傅立叶变换。其对应的是客户端项目中的FourierTransformer静态类。源码比较简单,就不贴出来了,大家自己去看。

    (2)声音强度显示控件 DecibelDisplayer

      DecibelDisplayer 使用的是PrograssBar来显示声音强度的大小。

      每当有声音数据交给DecibelDisplayer显示时,首先,DecibelDisplayer会调用上面的傅立叶变换将其转换为分贝,然后,将其映射为PrograssBar的对应的Value。

    2.发言者控件 SpeakerPanel

      SpeakerPanel 用于表示聊天室中的一个成员,如上图中1所示。它显示了成员的ID,成员的声音的强度(使用DecibelDisplayer控件),以及其麦克风的状态(启用、引用)。

      这个控件很重要,我将其源码贴出来:

    复制代码
        public partial class SpeakerPanel : UserControl ,IDisposable
        {
            private IChatUnit chatUnit;     
    
            public SpeakerPanel()
            {
                InitializeComponent();
                this.SetStyle(ControlStyles.ResizeRedraw, true);//调整大小时重绘
                this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);// 双缓冲
                this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);// 禁止擦除背景.
                this.SetStyle(ControlStyles.UserPaint, true);//自行绘制            
                this.UpdateStyles();
            }
    
            public string MemberID
            {
                get
                {
                    if (this.chatUnit == null)
                    {
                        return null;
                    }
    
                    return this.chatUnit.MemberID;
                }
            }
    
            public void Initialize(IChatUnit unit)
            {
                this.chatUnit = unit;
                this.skinLabel_name.Text = unit.MemberID;
                       
                this.chatUnit.MicrophoneConnector.ConnectEnded += new CbGeneric<OMCS.Passive.ConnectResult>(MicrophoneConnector_ConnectEnded);
                this.chatUnit.MicrophoneConnector.OwnerOutputChanged += new CbGeneric(MicrophoneConnector_OwnerOutputChanged);
                this.chatUnit.MicrophoneConnector.AudioDataReceived += new CbGeneric<byte[]>(MicrophoneConnector_AudioDataReceived);
                this.chatUnit.MicrophoneConnector.BeginConnect(unit.MemberID);
            }
    
            public void Initialize(string curUserID)
            {
                this.skinLabel_name.Text = curUserID;
                this.skinLabel_name.ForeColor = Color.Red;
                this.pictureBox_Mic.Visible = false;
                this.decibelDisplayer1.Visible = false;
            }
    
            void MicrophoneConnector_AudioDataReceived(byte[] data)
            {
                this.decibelDisplayer1.DisplayAudioData(data);
            }
    
            void MicrophoneConnector_OwnerOutputChanged()
            {
                if (this.InvokeRequired)
                {
                    this.BeginInvoke(new CbGeneric(this.MicrophoneConnector_OwnerOutputChanged));
                }
                else
                {
                    this.ShowMicState();
                }
            }
    
            private ConnectResult connectResult;
            void MicrophoneConnector_ConnectEnded(ConnectResult res)
            {            
                if (this.InvokeRequired)
                {
                    this.BeginInvoke(new CbGeneric<ConnectResult>(this.MicrophoneConnector_ConnectEnded), res);
                }
                else
                {
                    this.connectResult = res;
                    this.ShowMicState();
                }
            }
    
            public void Dispose()
            {
                this.chatUnit.Close();
            }
    
            private void ShowMicState()
            {
                if (this.connectResult != OMCS.Passive.ConnectResult.Succeed)
                {
                    this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[2];
                    this.toolTip1.SetToolTip(this.pictureBox_Mic, this.connectResult.ToString());
                }
                else
                {
                    this.decibelDisplayer1.Working = false;
                    if (!this.chatUnit.MicrophoneConnector.OwnerOutput)
                    {
                        this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[1];
                        this.toolTip1.SetToolTip(this.pictureBox_Mic, "好友禁用了麦克风");
                        return;
                    }
    
                    if (this.chatUnit.MicrophoneConnector.Mute)
                    {
                        this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[1];
                        this.toolTip1.SetToolTip(this.pictureBox_Mic, "静音");
                    }
                    else
                    {
                        this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[0];
                        this.toolTip1.SetToolTip(this.pictureBox_Mic, "正常");
                        this.decibelDisplayer1.Working = true;
                    }
                }
    
            }
    
            private void pictureBox_Mic_Click(object sender, EventArgs e)
            {
                if (!this.chatUnit.MicrophoneConnector.OwnerOutput)
                {
                    return;
                }
    
                this.chatUnit.MicrophoneConnector.Mute = !this.chatUnit.MicrophoneConnector.Mute;
                this.ShowMicState();
            }
        }
    复制代码

    (1)在代码中,IChatUnit就代表当前这个聊天室中的成员。我们使用其MicrophoneConnector连接到目标成员的麦克风。

    (2)预定MicrophoneConnector的AudioDataReceived事件,当收到语音数据时,将其交给DecibelDisplayer去显示声音的大小。

    (3)预定MicrophoneConnector的ConnectEnded和OwnerOutputChanged事件,根据其结果来显示SpeakerPanel空间上麦克风图标的状态(对应ShowMicState方法)。

    3. MultiAudioChatContainer 控件

      MultiAudioChatContainer对应上图中2标注的控件,它主要做了以下几件事情:

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

    (2)使用FlowLayoutPanel将聊天室中每个成员对应的SpeakerPanel罗列出来。

    (3)当有成员加入或退出聊天室时(对应IChatGroup的SomeoneJoin和SomeoneExit事件),动态添加或移除对应的SpeakerPanel实例。

    (4)通过CheckBox将自己设备(麦克风和扬声器)的控制权暴露出来。我们可以启用或禁用我们自己的麦克风或扬声器。

    (5)注意,其提供了Close方法,这意味着,在关闭包含了该控件的宿主窗体时,要调用其Close方法以释放其内部持有的麦克风连接器等资源。

      在完成MultiAudioChatContainer后,我们这个聊天室的核心就差不多了。接下来就是弄个主窗体,然后把MultiAudioChatContainer拖上去,初始化IMultimediaManager,并传递给MultiAudioChatContainer就大功告成了。

    三. 源码下载

      上面只是讲了实现多人语音聊天室中的几个重点,并不全面,大家下载下面的源码可以更深入的研究。

      AudioChatRoom.rar  

      最后,跟大家说说部署的步骤:

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

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

    (3)在多台机器上运行客户端,以不同的帐号登录到同一个房间(如默认的R1000)。

    (4)如此,多个用户就处于同一个聊天室进行语音聊天了。


    展开全文
  • 基于H5的实时语音聊天

    万次阅读 多人点赞 2018-10-21 12:32:02
    基于H5的实时语音聊天 业务需求:网页和移动端的通讯,移动端播放g711alaw,难点如下: 网页如何调用系统api录音 录音后的数据是什么格式?如何转码? 如何实时通讯 &quot;text&quot; id=&quot...

    #基于H5的实时语音聊天

    业务需求:网页和移动端的通讯,移动端播放g711alaw,难点如下:

    • 网页如何调用系统api录音
    • 录音后的数据是什么格式?如何转码?
    • 如何实时通讯

    <input type="text" id="a"/>
    <button id="b">buttonB</button>
    <button id="c">停止</button>
    
    ******************************************
    
    
    var a = document.getElementById('a');
    var b = document.getElementById('b');
    var c = document.getElementById('c');
     
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
     
    var gRecorder = null;
    var audio = document.querySelector('audio');
    var door = false;
    var ws = null;
     
    b.onclick = function() {
        if(a.value === '') {
            alert('请输入用户名');
            return false;
        }
        if(!navigator.getUserMedia) {
            alert('抱歉您的设备无法语音聊天');
            return false;
        }
     
        SRecorder.get(function (rec) {
            gRecorder = rec;
        });
     
        ws = new WebSocket("wss://x.x.x.x:8888");
     
        ws.onopen = function() {
            console.log('握手成功');
            ws.send('user:' + a.value);
        };
     
        ws.onmessage = function(e) {
            receive(e.data);
        };
     
        document.onkeydown = function(e) {
            if(e.keyCode === 65) {
                if(!door) {
                    gRecorder.start();
                    door = true;
                }
            }
        };
     
        document.onkeyup = function(e) {
            if(e.keyCode === 65) {
                if(door) {
                    ws.send(gRecorder.getBlob());
                    gRecorder.clear();
                    gRecorder.stop();
                    door = false;
                }
            }
        }
    }
     
    c.onclick = function() {
        if(ws) {
            ws.close();
        }
    }
     
    var SRecorder = function(stream) {
        config = {};
     
        config.sampleBits = config.smapleBits || 8;             //输出采样位数
        config.sampleRate = config.sampleRate || (44100 / 6);   //输出采样频率
     
        var context = new AudioContext();
        var audioInput = context.createMediaStreamSource(stream);
        var recorder = context.createScriptProcessor(4096, 1, 1); //录音缓冲区大小,输入通道数,输出通道数
     
        var audioData = {
            size: 0          //录音文件长度
            , buffer: []    //录音缓存
            , inputSampleRate: context.sampleRate    //输入采样率
            , inputSampleBits: 16      //输入采样数位 8, 16
            , outputSampleRate: config.sampleRate    //输出采样率
            , oututSampleBits: config.sampleBits      //输出采样数位 8, 16
            , clear: function() {
                this.buffer = [];
                this.size = 0;
            }
            , input: function (data) {
                this.buffer.push(new Float32Array(data));
                this.size += data.length;
            }
            , compress: function () { //合并压缩
                //合并
                var data = new Float32Array(this.size);
                var offset = 0;
                for (var i = 0; i < this.buffer.length; i++) {
                    data.set(this.buffer[i], offset);
                    offset += this.buffer[i].length;
                }
                //压缩
                var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
                var length = data.length / compression;
                var result = new Float32Array(length);
                var index = 0, j = 0;
                while (index < length) {
                    result[index] = data[j];
                    j += compression;
                    index++;
                }
                return result;
            }
            , encodeWAV: function () {
                var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
                var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
                var bytes = this.compress();
                var dataLength = bytes.length * (sampleBits / 8);
                var buffer = new ArrayBuffer(44 + dataLength);
                var data = new DataView(buffer);
     
                var channelCount = 1;//单声道
                var offset = 0;
     
                var writeString = function (str) {
                    for (var i = 0; i < str.length; i++) {
                        data.setUint8(offset + i, str.charCodeAt(i));
                    }
                };
                
                // 资源交换文件标识符
                writeString('RIFF'); offset += 4;
                // 下个地址开始到文件尾总字节数,即文件大小-8
                data.setUint32(offset, 36 + dataLength, true); offset += 4;
                // WAV文件标志
                writeString('WAVE'); offset += 4;
                // 波形格式标志
                writeString('fmt '); offset += 4;
                // 过滤字节,一般为 0x10 = 16
                data.setUint32(offset, 16, true); offset += 4;
                // 格式类别 (PCM形式采样数据)
                data.setUint16(offset, 1, true); offset += 2;
                // 通道数
                data.setUint16(offset, channelCount, true); offset += 2;
                // 采样率,每秒样本数,表示每个通道的播放速度
                data.setUint32(offset, sampleRate, true); offset += 4;
                // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
                data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
                // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
                data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
                // 每样本数据位数
                data.setUint16(offset, sampleBits, true); offset += 2;
                // 数据标识符
                writeString('data'); offset += 4;
                // 采样数据总数,即数据总大小-44
                data.setUint32(offset, dataLength, true); offset += 4;
                // 写入采样数据
                if (sampleBits === 8) {
                    for (var i = 0; i < bytes.length; i++, offset++) {
                        var s = Math.max(-1, Math.min(1, bytes[i]));
                        var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
                        val = parseInt(255 / (65535 / (val + 32768)));
                        data.setInt8(offset, val, true);
                    }
                } else {
                    for (var i = 0; i < bytes.length; i++, offset += 2) {
                        var s = Math.max(-1, Math.min(1, bytes[i]));
                        data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                    }
                }
     
                return new Blob([data], { type: 'audio/wav' });
            }
        };
     
        this.start = function () {
            audioInput.connect(recorder);
            recorder.connect(context.destination);
        }
     
        this.stop = function () {
            recorder.disconnect();
        }
     
        this.getBlob = function () {
            return audioData.encodeWAV();
        }
     
        this.clear = function() {
            audioData.clear();
        }
     
        recorder.onaudioprocess = function (e) {
            audioData.input(e.inputBuffer.getChannelData(0));
        }
    };
     
    SRecorder.get = function (callback) {
        if (callback) {
            if (navigator.getUserMedia) {
                navigator.getUserMedia(
                    { audio: true },
                    function (stream) {
                        var rec = new SRecorder(stream);
                        callback(rec);
                    })
            }
        }
    }
     
    function receive(e) {
        audio.src = window.URL.createObjectURL(e);
    }
    

    上面是一段调用H5 Api发送语音的代码片段,按住A键录音,松开A键发送,实现功能类似于微信,下面先对上面代码进行分析和理解,便于实现我们的实时语音需求。

    首先创建AudioContext对象,有这个对象才有后面的H5调用底层硬件功能,
    var recorder = context.createScriptProcessor(4096, 1, 1);
    这句话创建的4096是录音一次录多少个字节,录到4096字节后会走后面的回调:

      recorder.onaudioprocess = function (e) {
                audioData.input(e.inputBuffer.getChannelData(0));
      }
    

    按住A键后调用gRecorder.start();

     this.start = function () {
                audioInput.connect(recorder);
                recorder.connect(context.destination);  //把麦克风的输入和音频采集相连起来 context.destination返回代表在环境中的音频的最终目的地。
            }
    

    然后走上面的回调不停的采集麦克风的音频数据input进去,input在这里:

    input: function (data) {
                    this.buffer.push(new Float32Array(data));   //buffer存储float32,size为字节大小,buffer大小为float大小
                    this.size += data.length;
                }
    

    这里将字节流数据,存成Float32Array存在buffer中,也就是说成员变量里面的buffer实际上是多个Float32Array组成的

    合并和压缩

    因为不同的业务可能要将录制的PCM转成需要的音频格式,所以了解录制的音频是什么格式很有必要

    compress: function () { //合并压缩
                    //合并
                    var data = new Float32Array(this.size);
                    var offset = 0;
                    for (var i = 0; i < this.buffer.length; i++) {
                        data.set(this.buffer[i], offset);       //把buffer中的各个Float32数组合并成一个
                        offset += this.buffer[i].length;
                    }
                    //压缩
                    var compression = parseInt(this.inputSampleRate / this.outputSampleRate);   //6
                    var length = data.length / compression;    //600/6 = 10
                    var result = new Float32Array(length);    // 32 位浮点值的类型化数组
                    var index = 0, j = 0;
                    while (index < length) {
                        result[index] = data[j];
                        j += compression;     //j+=6
                        index++;
                    }
                    return result;
                }
    

    合并,不多说,就是将buffer中的各个Float32Array合并成一个,压缩,根据输入采样率和输出采样率计算出一个比例,像我的项目中输入采样频率为48000 我计划生成的音频采样频率为8000 那这里的比值为6,计算出比值后,每隔这个比值6采样一次,平均取点,(经测试这样的简单取点不会有杂音)。
    这里再啰嗦下音频的基本知识:
    采样频率:一秒钟采样多少次
    采样位数:一次采样多大 如果16bit那就是一次采样2byte
    声道数(通道数):几个通道采样
    这样计算下来你一秒钟采样的大小为:声道数 * 采样频率 * 采样位数/8 (单位:byte)
    所以上面的4096字节收集一次数据大概用了多少秒就可以计算出来了 :4096/一秒钟采样大小 (单位:s)

    编码

    前言:
    PCM:电脑录制出的原声,未经压缩,声音还原度高,文件较大
    wav:符合RIFF标准的音频文件,不具体只某一种音频编码算法,如wav可以有PCM编码的wav,g711编码的wav…等等,只要他符合RIFF标准
    wav头:那么如何叫符合RIFF呢?自己百度。。。wav文件有44个字节的头,头里面告诉你音频是什么格式的,音频数据有多大。。。其实就是一对符合RIFF的结构体
    这里面编码成wav,为什么要编码成wav呢,因为他符合RIFF标准,在H5的页面能播放,转成其他音频也方便。因为我们录制的声音文件是PCM格式的
    我们录制的PCM数据太大了,也不方便传输,那么就根据需要压缩了一下,再编码成wav,根据自己需要,如果不需要转成wav,那么直接将PCM发送出去就好,后台再对数据进行处理。

    ###发送方式与后台接收

    发送方式基于websocket,我这里后台接收用的java,需要注意两点:1.注意接收缓冲区大小设置足够2.注意捕获onclose里面的reson
    
     @OnMessage(maxMessageSize=160000)      //最大160000字节
        public void OnMessage2(byte[] message, Session session) {
            logger.info("byte:" + message.length);
            System.out.println("转化为16进制:"+byteArrayToHexStr(message));
        }
    @OnClose
        public void onClose(Session session, CloseReason reason) {
    
            connList.remove(this);
            logger.error("onclose被调用"+reason.toString());
        }
    

    如何实时播放语音?

    有两种播放声音的方法:

    1. audio.src = window.URL.createObjectURL(e);
    
    2.audioContext.decodeAudioData(play_queue[index_play], function(buffer) {//解码成pcm流
                var audioBufferSouceNode = audioContext.createBufferSource();
                audioBufferSouceNode.buffer = buffer;
                audioBufferSouceNode.connect(audioContext.destination);
                audioBufferSouceNode.start(0);
            }, function(e) {
                console.log("failed to decode the file");
            });
    

    这里很明显第一种对于实时播放并不管用,因为语音包来的太频繁了,不停得变换src是有声音卡顿的,第二种亲测,有效
    这里注意啦:decodeAudioData这个Api很强大,从后台回传来的数据,用前台那个encodeWAV编码成wav后,传入decodeAudioData,是可以转换成声卡直接播放的数据的,也就是说大家不用绞尽脑汁思考如何把后台传来的数据变成Float32Array了,没那个必要。
    再一个这里用了个循环缓冲队列,将回传来编码后的音频存在循环缓冲队列中,而播放线程丛这个队列里取数据:play_queue[index_play],给大家提供个思路,有人有需要再贴代码吧。。

    老东家代码不能上传,只有测试demo
    前后端demo: https://download.csdn.net/download/qq422243639/10734859

    如不清楚转码是否正确,在使用转码方法转码后,写入文件,用coolpro2 这个工具打开,选择码率,比特率等,然后打开,听听语音是否清晰,工具下载地址:
    https://download.csdn.net/download/qq422243639/10734845

    附赠IE基于ActiveX插件的实时语音解决方式:
    https://download.csdn.net/download/qq422243639/10734877

    展开全文
  • C#实现多人语音聊天

    万次阅读 2015-09-05 18:11:43
    在上一篇文章 《实现一个简单的语音聊天室(多人语音聊天系统)》中,我用C#实现了一个简单的语音聊天室,并给出了源码下载。虽然有源码,但是很多朋友反映,理解起来还是有些模糊、不够清楚。现在想来,是因为我忘...
  • 为什么要利用语音聊天系统源码做语音聊天app开发?因为语言的力量是强大的,说出来和写出来、打出来、做出来……感觉是不同的——小编昨晚做了一个噩梦,梦到被人剥下了脸皮上的一层薄膜,并文了一个巨丑无比的眉毛...
  • C++语音聊天源码 C++源码 语音聊天

    千次下载 热门讨论 2020-07-30 23:33:08
    C++语音聊天源码 很好很强大!!!
  • 在微信统治下的社交领域,越来越多的互联网企业想要挑战微信...当然,除了比较常见的一对一语音聊天以外,还有一种形式也备受关注,那就是多人语音聊天室,它的应用十分广泛,在纯语音社交APP中发挥了重要作用,像音...
  • Qt编写语音聊天机器人详解

    千次阅读 2018-11-21 13:37:54
    现在语音聊天机器人是一度火热,网上也有其他编程软件的语音聊天机器人,但是没有Qt的(或许是博主没找到),故在此发一篇关于Qt如何编写语音聊天机器人详解。代码中运用到了一些常见的命令,比如正则表达式、获取...
  • 前言:本文将简要分享几个语音聊天室的应用场景,并讲述基于声网SDK,实现语音聊天室的步骤。 语音聊天在泛娱乐社交行业中有着重要的地位,行业中很多佼佼者也都为用户提供了语音聊天室,甚至有些平台最初就是依托...
  • 主要在界面上进行了美化,并添加了语音聊天的功能。具体功能有: 1.采用了全新的界面风格(新增) 2.实现了基本文字聊天功能 3.实现了基本文件传送功能 4.实现了发送窗口抖动的功能 5.实现了语音聊天的功能(新增) ...
  • 0. 背景语音聊天是游戏中重要的功能,它能便捷用户的聊天,增强用户的粘性。本文会讲述如何在Unity5.x中,使用素材[github地址]中的Unity5.x工程项目如何实现在windows中的简单语音聊天。1. 聊天服务本节实现的聊天...
  • 前言:本文将简要分享几个语音聊天室的应用场景,并讲述基于声网SDK,实现语音聊天室的步骤。 语音聊天在泛娱乐社交行业中有着重要的地位,行业中很多佼佼者也都为用户提供了语音聊天室,甚至有些平台最初就是依托...
  • 语音聊天室这个名词可能有点陌生,实际上相关的产品还是很多的,例如游戏里的开黑语音、在线课堂等。语音聊天室可以认为视频直播的前身,很多音视频平台的架构是从语音聊天室演进为视频直播室的。本文主要介绍语音...
  • c++实现语音聊天

    热门讨论 2020-07-30 23:33:36
    mfc实现多人语音聊天
  • unity for 语音聊天Demo

    2020-07-28 23:32:14
    unity for 语音聊天Demo实时进行语音聊天。。。。。。。
  • java 语言写的语音聊天程序(续)

    千次阅读 2018-06-07 17:18:00
    java 语言写的语音聊天程序(续) 接上文java 语言写的语音聊天程序,我今天把下半部分写完给大家一个完成的结局。 在上文中,我们讲到了聊天程序的基本的过程,下面我们来继续在本程序中最关键的部分,语音聊天部分...
  • 语音聊天插件,PHP BS程序都能用

    热门讨论 2020-07-26 23:33:54
    语音聊天MEchat语音聊天MEchat语音聊天MEchat语音聊天MEchat语音聊天MEchat语音聊天MEchat语音聊天MEchat语音聊天MEchat语音聊天MEchat
  • 不靠颜值靠音值,有些人天生就是声控。相对的有些人有一副好嗓子,天生就是声优。不露脸,只靠声音就能俘获一大批粉丝。...语音聊天系统源码是能够实现语音聊天的一段程序源码,语音交流相较于文字符号相比,更为生动,
1 2 3 4 5 ... 20
收藏数 34,069
精华内容 13,627
关键字:

语音聊天