原文:SignalR+NAudio实现语音会话[WPF]

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lordwish/article/details/51811761

NAudio是一个功能很丰富的.NET语音处理类库,SignalR则微软推出的实时通信框架,结合两者来实现简单的语音会话聊天应用,主要功能包括在线终端列表刷新、会话请求、会话拒绝、会话繁忙、会话结束。和之前写的视频会话示例类似,只不过上个是视频,这个是语音。


1 服务端及辅助类

1.1 创建服务端

[ 使用WPF创建SignalR服务端]

1.2 在线终端列表刷新


[WPF+SignalR实现用户列表实时刷新]

1.3 通信辅助类

类似之前的视频会话,再添加几个枚举

    public enum MessageState
{
AudioApply,//语音会话请求
AudioOpen,//语音会话开始
AudioRefuse,//语音会话拒绝
AudioBusy,//语音会话繁忙
AudioOver,//语音会话结束
VideoApply,//视频会话请求
VideoOpen,//视频会话开始
VideoRefuse,//视频会话拒绝
VideoBusy,//视频会话繁忙
VideoOver,//视频会话结束
Null
}

1.4 服务端方法

依然沿用视频会话中的方法,完全不用变

public Task MessageConnect(ClientModel sender,ClientModel receiver,string messageid,MessageState status)
{
return Clients.Client(receiver.ConnectionId).messageconnect(receiver, sender, messageid, status);
}

2 客户端方法

客户端方法主要包括:发送会话请求、会话状态监听、打开语音、发送语音、接收语音、关闭语音。对语音进行操作的方法参考了NAudio的源码,有兴趣的可以到GitHub上查看(NAudio)。

1.2 在线终端列表刷新

[WPF+SignalR实现用户列表实时刷新]

    <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="240"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Border Name="bdConnect" Visibility="Hidden" Width="320" Height="320" BorderThickness="2" BorderBrush="Khaki">
<TextBlock FontSize="24"><Run>正在与</Run><Run Name="txtName"></Run><Run>语音会话中...</Run></TextBlock>
</Border>
<Button Name="btnClose" Visibility="Hidden" Width="90" Height="30" Click="btnClose_Click">关闭</Button>
<ComboBox Grid.Row="1" Visibility="Hidden" x:Name="ComboDevices"></ComboBox>
</Grid>
<DataGrid Grid.Column="1" Name="dgList" AutoGenerateColumns="False" MouseDoubleClick="dgList_MouseDoubleClick">
<DataGrid.Columns>
<DataGridTextColumn Header="IP" Binding="{Binding ClientIP,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
<DataGridTextColumn Header="MAC" Binding="{Binding ClientMac,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
<DataGridTextColumn Header="Name" Binding="{Binding ClientName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>

2.2 发送会话请求

        private void dgList_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (dgList.SelectedItem == null)
{
return;
}
client = (ClientModel)dgList.SelectedItem;
var result = MessageBox.Show("确定要进行语音会话?", "会话提醒", MessageBoxButton.OKCancel, MessageBoxImage.Question, MessageBoxResult.OK);
if (result == MessageBoxResult.OK && client != null)
{
string messageid = Guid.NewGuid().ToString();
MyHub.Invoke("MessageConnect", MyClient, client, messageid, MessageState.AudioApply);
}
}

2.3 监听会话状态

包含了接受会话、拒绝会话、会话繁忙、结束会话状态的监听。

        private void ConnectListener()
{
try
{
MyHub.On<ClientModel, ClientModel, string, MessageState>("messageconnect", (sender, receiver, id, state) =>
{
switch (state)
{
case MessageState.AudioApply:
{
if (messageid == "")
{
var result = MessageBox.Show("是否接收" + sender.ClientName + "的会话请求?", "会话提醒", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes);
if (result == MessageBoxResult.Yes)
{
this.Dispatcher.Invoke(delegate
{
client = sender;
messageid = id;
txtName.Text = sender.ClientName;
_addressReturn = sender.ClientIP;
bdConnect.Visibility = Visibility.Visible;
btnClose.Visibility = Visibility.Visible;
OpenAudio();
});
}
else
{
MyHub.Invoke("MessageConnect", receiver, sender, id, MessageState.AudioRefuse);
}
}
else
{
MyHub.Invoke("MessageConnect", receiver, sender, id, MessageState.AudioBusy);
}
}
break;
case MessageState.AudioOpen:
{
this.Dispatcher.Invoke(delegate
{
client = sender;
messageid = id;
txtName.Text = sender.ClientName;
_addressReturn = sender.ClientIP;
bdConnect.Visibility = Visibility.Visible;
btnClose.Visibility = Visibility.Visible;
OpenAudio();
});
}
break;
case MessageState.AudioRefuse:
{
MessageBox.Show(sender.ClientName+"拒绝了您的会话请求!");
}
break;
case MessageState.AudioBusy:
{
MessageBox.Show(sender.ClientName + "正忙,请稍后连接!");
}
break;
case MessageState.AudioOver:
{
this.Dispatcher.Invoke(delegate
{
CloseAudio();
});
}
break;
}
});
}
catch (Exception)
{ throw;
} }

2.4 打开音频

定义变量:

        public int Port
{
get { return _port; }
set { _port = value; }
} private string _addressReturn = ""; public string AddressReturn
{
get { return _addressReturn; }
set { _addressReturn = value; }
} private WaveIn _waveIn;
private UdpClient _udpSender;
private UdpClient _udpListener;
private IWavePlayer _waveOut;
private BufferedWaveProvider _waveProvider;
private INetworkChatCodec _selectedCodec;
private volatile bool _connected;
IPEndPoint _endPoint;
int _inputDeviceNumber;

具体方法:

        private void OpenAudio()
{
FindDevices();
_selectedCodec = new UncompressedPcmChatCodec();
//制定服务端的IP和端口
_endPoint = new IPEndPoint(IPAddress.Parse(AddressReturn), Port);
//发送消息
Connect(_endPoint, _inputDeviceNumber, _selectedCodec);
}
private void FindDevices()
{
for (int n = 0; n < WaveIn.DeviceCount; n++)
{
var capabilities = WaveIn.GetCapabilities(n);
ComboDevices.Items.Add(capabilities.ProductName);
}
if (ComboDevices.Items.Count > 1)
{
_inputDeviceNumber = 1;
}
else if (ComboDevices.Items.Count == 0)
{
_inputDeviceNumber = 0;
}
else
{
_inputDeviceNumber = -1;
}
}

2.5 进行语音通话

        private void Connect(IPEndPoint endPoint, int inputDeviceNumber, INetworkChatCodec codec)
{
try
{
_waveIn = new WaveIn();
_waveIn.BufferMilliseconds = 50;//延时 n
_waveIn.DeviceNumber = inputDeviceNumber;
_waveIn.WaveFormat = codec.RecordFormat;
_waveIn.DataAvailable += waveIn_DataAvailable;
_waveIn.StartRecording(); _udpSender = new UdpClient();
_udpListener = new UdpClient(endPoint.Port); // To allow us to talk to ourselves for test purposes:
// http://stackoverflow.com/questions/687868/sending-and-receiving-udp-packets-between-two-programs-on-the-same-computer
_udpListener.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
//udpListener.Client.Bind(endPoint); _udpSender.Connect(endPoint); _waveOut = new WaveOut();
_waveProvider = new BufferedWaveProvider(codec.RecordFormat);
_waveOut.Init(_waveProvider);
_waveOut.Play(); _connected = true;
var state = new ListenerThreadState { Codec = codec, EndPoint = endPoint };
ThreadPool.QueueUserWorkItem(ListenerThread, state);
//this.btnAudio.Content = "断开语音";
}
catch
{
MessageBox.Show("请检查您的设备连接是否正常~!");
}
} private void waveIn_DataAvailable(object sender, WaveInEventArgs e)
{
try
{
byte[] encoded = _selectedCodec.Encode(e.Buffer, 0, e.BytesRecorded);
int num = _udpSender.Send(encoded, encoded.Length);
}
catch (System.Exception ex)
{
System.Console.WriteLine("Exception:" + ex.ToString());
} }

2.6 监听端口接收语音

private void ListenerThread(object state)
{
var listenerThreadState = (ListenerThreadState)state;
var endPoint = listenerThreadState.EndPoint;
try
{
while (_connected)
{
try
{
byte[] b = _udpListener.Receive(ref endPoint);
byte[] decoded = listenerThreadState.Codec.Decode(b, 0, b.Length);
_waveProvider.AddSamples(decoded, 0, decoded.Length);
}
catch
{
// ignored
}
}
}
catch (SocketException)
{
}
} class ListenerThreadState
{
public IPEndPoint EndPoint { get; set; }
public INetworkChatCodec Codec { get; set; }
}

2.7 关闭语音

        private void btnClose_Click(object sender, RoutedEventArgs e)
{
CloseAudio();
} private void CloseAudio()
{
client = null;
messageid = "";
bdConnect.Visibility = Visibility.Hidden;
btnClose.Visibility = Visibility.Hidden; _connected = false;
_waveIn.DataAvailable -= waveIn_DataAvailable;
_waveIn.StopRecording();
_waveOut.Stop(); _udpSender.Close();
_udpListener.Close();
if (_waveIn != null)
{
_waveIn.Dispose();
}
if (_waveOut != null)
{
_waveOut.Dispose();
}
if (_selectedCodec != null)
{
_selectedCodec.Dispose();
}
}

NAudio是个功能很丰富的语音处理类库,但比较遗憾的是没有回音消除及降噪之类的高级功能。NAudio包含了NSpeex项目,都是基于C#语言的,而NSpeex则是源自于C++的Speex项目,有兴趣的可以到GitHub上找。

GitHub是个好东西,对于程序员来讲就不用多说了吧!

SignalR+NAudio实现语音会话[WPF]的更多相关文章

  1. SignalR+AForge实现视频会话[WPF]

    原文:SignalR+AForge实现视频会话[WPF] 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/lordwish/article/detai ...

  2. 【SignalR学习系列】5. SignalR WPF程序

    首先创建 WPF Server 端,新建一个 WPF 项目 安装 Nuget 包 替换 MainWindows 的Xaml代码 <Window x:Class="WPFServer.M ...

  3. AngularJS+ASP.NET MVC+SignalR实现消息推送

    原文:AngularJS+ASP.NET MVC+SignalR实现消息推送 背景 OA管理系统中,员工提交申请单,消息实时通知到相关人员及时进行审批,审批之后将结果推送给用户. 技术选择 最开始发现 ...

  4. 如何使用Microsoft技术栈

    Microsoft技术栈最近有大量的变迁,这使得开发人员和领导者都想知道他们到底应该关注哪些技术.Microsoft自己并不想从官方层面上反对Silverlight这样的技术,相对而言他们更喜欢让这种 ...

  5. being词典案例分析

    一.调研评测: 1.软件bug: 1.输入空格分号回车之后并不给用户报错,说明他的异常处理机制有问题. 2.对于中文的很多口头语和方言,并不能给出翻译或者说,也并没有给出网络搜索后的结果. 3.添加生 ...

  6. QQ 腾讯QQ(简称“QQ”)是腾讯公司开发的一款基于Internet的即时通信(IM)软件

    QQ 编辑 腾讯QQ(简称“QQ”)是腾讯公司开发的一款基于Internet的即时通信(IM)软件.腾讯QQ支持在线聊天.视频通话.点对点断点续传文件.共享文件.网络硬盘.自定义面板.QQ邮箱等多种功 ...

  7. PHP微信公众号开发之自动回复

    先把源码类发出来 <?php /** 自己封装 微信 开发api */ header('Content-type: text/html; charset=utf-8');#设置头信息 class ...

  8. [SIP00]SIP 概念总结

    SIP ---------------------------   Session Initiation Protocol ---------------------------   create, ...

  9. chrome jssip

    WebRTC 实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供简单的javascript就可以达到实时通讯(Real-Time Communications (RTC))能力 ...

随机推荐

  1. java list 容器的ConcurrentModificationException

    java中的很多容器在遍历的同时进行修改里面的元素都会ConcurrentModificationException,包括多线程情况和单线程的情况.多线程的情况就用说了,单线程出现这个异常一般是遍历( ...

  2. 38.IntelliJ IDEA中创建Web聚合项目(Maven多模块项目)

    转自:https://blog.csdn.net/u012702547/article/details/77431765 Eclipse用多了,IntelliJ中创建Maven聚合项目可能有小伙伴还不 ...

  3. h.264硬件解码

    // H264HWDecoder.m // H264EncoderDecoder // // Created by lujunjie on 2016/11/28. // Copyright © 201 ...

  4. javascript中if条件

    1.布尔变量 true/false 2.数字非0.非NaN/0.NaN 3.对象非null/null.nudefined 4.字符串非空串/空串 if(!!str){ //do something } ...

  5. 水题ing

    T1: https://www.luogu.org/problemnew/show/P1724幻想乡,东风谷早苗是以高达控闻名的高中生宅巫女.某一天,早苗终于入手了最新款的钢达姆模型.作为最新的钢达姆 ...

  6. LuceneIndexFileDeleter会保留初始的commit

    给实时索引加入了merge策略,持续更新时发现有做merge,但索引文件夹中的段数远远大于RealTimeIndexWriter中的段数,就是有些merge的段应该删除,文件夹中没有删除.而关闭sea ...

  7. win7管理工具不可用

    看看这个路径的文件夹是否还在C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Administrative Tools如果有缺失的文件夹就自己新 ...

  8. LVM 常用命令

    fdisk -l 查看硬盘信息 df -h查看文件系统使用量 fdisk /dev/sdb处理硬盘,删除分区,新建分区之类 partprobe将核心的 partition table 更新 mkfs ...

  9. python3 格式化输出给定时间的下一秒

    # 功能:输入一个时间,格式化输出该时间的下一秒 #!/usr/bin/env python # -*- coding:utf-8 -*- # Author:Hiuhung Wan # 功能:输入一个 ...

  10. 9.9 Binder系统_Java实现_Android里java程序的编译启动

    如果知道了进程号:通过ls /proc/进程号/task 可以看到所有线程    cat /proc/进程号/task/线程号/comm  可以达到线程名字(主线程是main,主线程号就是进程号) d ...