原文:
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是个好东西,对于程序员来讲就不用多说了吧!